Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3ccd67dc authored by Charlie Tsai's avatar Charlie Tsai Committed by Android (Google) Code Review
Browse files

Merge "Replace rectangle shadow algorithm"

parents 6e867d49 e18bf49b
Loading
Loading
Loading
Loading
+107 −143
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 * Copyright (C) 2015, 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
@@ -16,178 +16,142 @@

package android.view;

import com.android.layoutlib.bridge.impl.GcSnapshot;
import com.android.layoutlib.bridge.impl.ResourceHelper;

import android.annotation.NonNull;
import android.graphics.Canvas;
import android.graphics.Canvas_Delegate;
import android.graphics.LinearGradient;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Path.FillType;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region.Op;
import android.graphics.Shader.TileMode;

import java.awt.Rectangle;
import com.android.layoutlib.bridge.shadowutil.SpotShadow;
import com.android.layoutlib.bridge.shadowutil.ShadowBuffer;

/**
 * Paints shadow for rounded rectangles. Inspiration from CardView. Couldn't use that directly,
 * since it modifies the size of the content, that we can't do.
 */
public class RectShadowPainter {

    private static final float SHADOW_STRENGTH = 0.1f;
    private static final int LIGHT_POINTS = 8;

    private static final int START_COLOR = ResourceHelper.getColor("#37000000");
    private static final int END_COLOR = ResourceHelper.getColor("#03000000");
    private static final float PERPENDICULAR_ANGLE = 90f;
    private static final int QUADRANT_DIVIDED_COUNT = 8;

    public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas) {
    private static final int RAY_TRACING_RAYS = 180;
    private static final int RAY_TRACING_LAYERS = 10;

    public static void paintShadow(@NonNull Outline viewOutline, float elevation,
            @NonNull Canvas canvas) {
        Rect outline = new Rect();
        if (!viewOutline.getRect(outline)) {
            throw new IllegalArgumentException("Outline is not a rect shadow");
        }

        // TODO replacing the algorithm here to create better shadow

        float shadowSize = elevationToShadow(elevation);
        int saved = modifyCanvas(canvas, shadowSize);
        Rect originCanvasRect = canvas.getClipBounds();
        int saved = modifyCanvas(canvas);
        if (saved == -1) {
            return;
        }

        try {
            float radius = viewOutline.getRadius();
            if (radius <= 0) {
                // We can not paint a shadow with radius 0
                return;
            }

        try {
            Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            cornerPaint.setStyle(Style.FILL);
            Paint edgePaint = new Paint(cornerPaint);
            edgePaint.setAntiAlias(false);
            float outerArcRadius = radius + shadowSize;
            int[] colors = {START_COLOR, START_COLOR, END_COLOR};
            cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors,
                    new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP));
            edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, START_COLOR, END_COLOR,
                    TileMode.CLAMP));
            Path path = new Path();
            path.setFillType(FillType.EVEN_ODD);
            // A rectangle bounding the complete shadow.
            RectF shadowRect = new RectF(outline);
            shadowRect.inset(-shadowSize, -shadowSize);
            // A rectangle with edges corresponding to the straight edges of the outline.
            RectF inset = new RectF(outline);
            inset.inset(radius, radius);
            // A rectangle used to represent the edge shadow.
            RectF edgeShadowRect = new RectF();


            // left and right sides.
            edgeShadowRect.set(-shadowSize, 0f, 0f, inset.height());
            // Left shadow
            sideShadow(canvas, edgePaint, edgeShadowRect, outline.left, inset.top, 0);
            // Right shadow
            sideShadow(canvas, edgePaint, edgeShadowRect, outline.right, inset.bottom, 2);
            // Top shadow
            edgeShadowRect.set(-shadowSize, 0, 0, inset.width());
            sideShadow(canvas, edgePaint, edgeShadowRect, inset.right, outline.top, 1);
            // bottom shadow. This needs an inset so that blank doesn't appear when the content is
            // moved up.
            edgeShadowRect.set(-shadowSize, 0, shadowSize / 2f, inset.width());
            edgePaint.setShader(new LinearGradient(edgeShadowRect.right, 0, edgeShadowRect.left, 0,
                    colors, new float[]{0f, 1 / 3f, 1f}, TileMode.CLAMP));
            sideShadow(canvas, edgePaint, edgeShadowRect, inset.left, outline.bottom, 3);

            // Draw corners.
            drawCorner(canvas, cornerPaint, path, inset.right, inset.bottom, outerArcRadius, 0);
            drawCorner(canvas, cornerPaint, path, inset.left, inset.bottom, outerArcRadius, 1);
            drawCorner(canvas, cornerPaint, path, inset.left, inset.top, outerArcRadius, 2);
            drawCorner(canvas, cornerPaint, path, inset.right, inset.top, outerArcRadius, 3);
            // view's absolute position in this canvas.
            int viewLeft = -originCanvasRect.left + outline.left;
            int viewTop = -originCanvasRect.top + outline.top;
            int viewRight = viewLeft + outline.width();
            int viewBottom = viewTop + outline.height();

            float[][] rectangleCoordinators = generateRectangleCoordinates(viewLeft, viewTop,
                    viewRight, viewBottom, radius, elevation);

            // TODO: get these values from resources.
            float lightPosX = canvas.getWidth() / 2;
            float lightPosY = 0;
            float lightHeight = 1800;
            float lightSize = 200;

            paintGeometricShadow(rectangleCoordinators, lightPosX, lightPosY, lightHeight,
                    lightSize, canvas);
        } finally {
            canvas.restoreToCount(saved);
        }
    }

    private static float elevationToShadow(float elevation) {
        // The factor is chosen by eyeballing the shadow size on device and preview.
        return elevation * 0.5f;
    private static int modifyCanvas(@NonNull Canvas canvas) {
        Rect rect = canvas.getClipBounds();
        canvas.translate(rect.left, rect.top);
        return canvas.save();
    }

    /**
     * Translate canvas by half of shadow size up, so that it appears that light is coming
     * slightly from above. Also, remove clipping, so that shadow is not clipped.
     */
    private static int modifyCanvas(Canvas canvas, float shadowSize) {
        Rect clipBounds = canvas.getClipBounds();
        if (clipBounds.isEmpty()) {
            return -1;
    @NonNull
    private static float[][] generateRectangleCoordinates(float left, float top, float right,
            float bottom, float radius, float elevation) {
        left = left + radius;
        top = top + radius;
        right = right - radius;
        bottom = bottom - radius;

        final double RADIANS_STEP = 2 * Math.PI / 4 / QUADRANT_DIVIDED_COUNT;

        float[][] ret = new float[QUADRANT_DIVIDED_COUNT * 4][3];

        int points = 0;
        // left-bottom points
        for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
            ret[points][0] = (float) (left - radius + radius * Math.cos(RADIANS_STEP * i));
            ret[points][1] = (float) (bottom + radius - radius * Math.cos(RADIANS_STEP * i));
            ret[points][2] = elevation;
            points++;
        }
        int saved = canvas.save();
        // Usually canvas has been translated to the top left corner of the view when this is
        // called. So, setting a clip rect at 0,0 will clip the top left part of the shadow.
        // Thus, we just expand in each direction by width and height of the canvas, while staying
        // inside the original drawing region.
        GcSnapshot snapshot = Canvas_Delegate.getDelegate(canvas).getSnapshot();
        Rectangle originalClip = snapshot.getOriginalClip();
        if (originalClip != null) {
            canvas.clipRect(originalClip.x, originalClip.y, originalClip.x + originalClip.width,
              originalClip.y + originalClip.height, Op.REPLACE);
            canvas.clipRect(-canvas.getWidth(), -canvas.getHeight(), canvas.getWidth(),
              canvas.getHeight(), Op.INTERSECT);
        // left-top points
        for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
            ret[points][0] = (float) (left + radius - radius * Math.cos(RADIANS_STEP * i));
            ret[points][1] = (float) (top + radius - radius * Math.cos(RADIANS_STEP * i));
            ret[points][2] = elevation;
            points++;
        }
        canvas.translate(0, shadowSize / 2f);
        return saved;
        // right-top points
        for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
            ret[points][0] = (float) (right + radius - radius * Math.cos(RADIANS_STEP * i));
            ret[points][1] = (float) (top + radius + radius * Math.cos(RADIANS_STEP * i));
            ret[points][2] = elevation;
            points++;
        }
        // right-bottom point
        for (int i = 0; i < QUADRANT_DIVIDED_COUNT; i++) {
            ret[points][0] = (float) (right - radius + radius * Math.cos(RADIANS_STEP * i));
            ret[points][1] = (float) (bottom - radius + radius * Math.cos(RADIANS_STEP * i));
            ret[points][2] = elevation;
            points++;
        }

    private static void sideShadow(Canvas canvas, Paint edgePaint,
            RectF edgeShadowRect, float dx, float dy, int rotations) {
        if (isRectEmpty(edgeShadowRect)) {
            return;
        return ret;
    }
        int saved = canvas.save();
        canvas.translate(dx, dy);
        canvas.rotate(rotations * PERPENDICULAR_ANGLE);
        canvas.drawRect(edgeShadowRect, edgePaint);
        canvas.restoreToCount(saved);

    private static void paintGeometricShadow(@NonNull float[][] coordinates, float lightPosX,
            float lightPosY, float lightHeight, float lightSize, Canvas canvas) {

        // The polygon of shadow (same as the original item)
        float[] shadowPoly = new float[coordinates.length * 3];
        for (int i = 0; i < coordinates.length; i++) {
            shadowPoly[i * 3 + 0] = coordinates[i][0];
            shadowPoly[i * 3 + 1] = coordinates[i][1];
            shadowPoly[i * 3 + 2] = coordinates[i][2];
        }

    /**
     * @param canvas Canvas to draw the rectangle on.
     * @param paint Paint to use when drawing the corner.
     * @param path A path to reuse. Prevents allocating memory for each path.
     * @param x Center of circle, which this corner is a part of.
     * @param y Center of circle, which this corner is a part of.
     * @param radius radius of the arc
     * @param rotations number of quarter rotations before starting to paint the arc.
     */
    private static void drawCorner(Canvas canvas, Paint paint, Path path, float x, float y,
            float radius, int rotations) {
        int saved = canvas.save();
        canvas.translate(x, y);
        path.reset();
        path.arcTo(-radius, -radius, radius, radius, rotations * PERPENDICULAR_ANGLE,
                PERPENDICULAR_ANGLE, false);
        path.lineTo(0, 0);
        path.close();
        canvas.drawPath(path, paint);
        canvas.restoreToCount(saved);
        // TODO: calculate the ambient shadow and mix with Spot shadow.

        // Calculate the shadow of SpotLight
        float[] light = SpotShadow.calculateLight(lightSize, LIGHT_POINTS, lightPosX,
                lightPosY, lightHeight);

        int stripSize = 3 * SpotShadow.getStripSize(RAY_TRACING_RAYS, RAY_TRACING_LAYERS);
        if (stripSize < 9) {
            return;
        }
        float[] strip = new float[stripSize];
        SpotShadow.calcShadow(light, LIGHT_POINTS, shadowPoly, coordinates.length, RAY_TRACING_RAYS,
                RAY_TRACING_LAYERS, 1f, strip);

    /**
     * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks.
     * <p/>
     * This is required because {@link Canvas_Delegate#native_drawRect(long, float, float, float,
     * float, long)} casts the co-ordinates to int and we want to ensure that it doesn't end up
     * drawing empty rectangles, which results in IllegalArgumentException.
     */
    private static boolean isRectEmpty(RectF rect) {
        return (int) rect.left >= (int) rect.right || (int) rect.top >= (int) rect.bottom;
        ShadowBuffer buff = new ShadowBuffer(canvas.getWidth(), canvas.getHeight());
        buff.generateTriangles(strip, SHADOW_STRENGTH);
        buff.draw(canvas);
    }
}
+181 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.layoutlib.bridge.shadowutil;

import android.annotation.NonNull;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;

public class ShadowBuffer {

    private int mWidth;
    private int mHeight;
    private Bitmap mBitmap;
    private int[] mData;

    public ShadowBuffer(int width, int height) {
        mWidth = width;
        mHeight = height;
        mBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
        mData = new int[mBitmap.getWidth() * mBitmap.getHeight()];
        mBitmap.getPixels(mData, 0, mBitmap.getWidth(), 0, 0, mBitmap.getWidth(),
                mBitmap.getHeight());
    }

    public void generateTriangles(@NonNull float[] strip, float scale) {
        for (int i = 0; i < strip.length - 8; i += 3) {
            float fx3 = strip[i];
            float fy3 = strip[i + 1];
            float fz3 = scale * strip[i + 2];

            float fx2 = strip[i + 3];
            float fy2 = strip[i + 4];
            float fz2 = scale * strip[i + 5];

            float fx1 = strip[i + 6];
            float fy1 = strip[i + 7];
            float fz1 = scale * strip[i + 8];

            if (fx1 * (fy2 - fy3) + fx2 * (fy3 - fy1) + fx3 * (fy1 - fy2) == 0) {
                continue;
            }

            triangleZBuffMin(mData, mWidth, mHeight, fx3, fy3, fz3, fx2, fy2, fz2, fx1, fy1, fz1);
            triangleZBuffMin(mData, mWidth, mHeight, fx1, fy1, fz1, fx2, fy2, fz2, fx3, fy3, fz3);
        }
        mBitmap.setPixels(mData, 0, mBitmap.getWidth(), 0, 0, mBitmap.getWidth(),
                mBitmap.getHeight());
    }

    private void triangleZBuffMin(@NonNull int[] buff, int w, int h, float fx3, float fy3,
            float fz3, float fx2, float fy2, float fz2, float fx1, float fy1, float fz1) {
        if (((fx1 - fx2) * (fy3 - fy2) - (fy1 - fy2) * (fx3 - fx2)) < 0) {
            float tmpX = fx1;
            float tmpY = fy1;
            float tmpZ = fz1;
            fx1 = fx2;
            fy1 = fy2;
            fz1 = fz2;
            fx2 = tmpX;
            fy2 = tmpY;
            fz2 = tmpZ;
        }
        double d = (fx1 * (fy3 - fy2) - fx2 * fy3 + fx3 * fy2 + (fx2 - fx3) * fy1);

        if (d == 0) {
            return;
        }
        float dx = (float) (-(fy1 * (fz3 - fz2) - fy2 * fz3 + fy3 * fz2 + (fy2 - fy3) * fz1) / d);
        float dy = (float) ((fx1 * (fz3 - fz2) - fx2 * fz3 + fx3 * fz2 + (fx2 - fx3) * fz1) / d);
        float zOff = (float) ((fx1 * (fy3 * fz2 - fy2 * fz3) + fy1 * (fx2 * fz3 - fx3 * fz2) +
                (fx3 * fy2 - fx2 * fy3) * fz1) / d);

        int Y1 = (int) (16.0f * fy1 + .5f);
        int Y2 = (int) (16.0f * fy2 + .5f);
        int Y3 = (int) (16.0f * fy3 + .5f);

        int X1 = (int) (16.0f * fx1 + .5f);
        int X2 = (int) (16.0f * fx2 + .5f);
        int X3 = (int) (16.0f * fx3 + .5f);

        int DX12 = X1 - X2;
        int DX23 = X2 - X3;
        int DX31 = X3 - X1;

        int DY12 = Y1 - Y2;
        int DY23 = Y2 - Y3;
        int DY31 = Y3 - Y1;

        int FDX12 = DX12 << 4;
        int FDX23 = DX23 << 4;
        int FDX31 = DX31 << 4;

        int FDY12 = DY12 << 4;
        int FDY23 = DY23 << 4;
        int FDY31 = DY31 << 4;

        int minX = (min(X1, X2, X3) + 0xF) >> 4;
        int maxX = (max(X1, X2, X3) + 0xF) >> 4;
        int minY = (min(Y1, Y2, Y3) + 0xF) >> 4;
        int maxY = (max(Y1, Y2, Y3) + 0xF) >> 4;

        if (minY < 0) {
            minY = 0;
        }
        if (minX < 0) {
            minX = 0;
        }
        if (maxX > w) {
            maxX = w;
        }
        if (maxY > h) {
            maxY = h;
        }
        int off = minY * w;

        int C1 = DY12 * X1 - DX12 * Y1;
        int C2 = DY23 * X2 - DX23 * Y2;
        int C3 = DY31 * X3 - DX31 * Y3;

        if (DY12 < 0 || (DY12 == 0 && DX12 > 0)) {
            C1++;
        }
        if (DY23 < 0 || (DY23 == 0 && DX23 > 0)) {
            C2++;
        }
        if (DY31 < 0 || (DY31 == 0 && DX31 > 0)) {
            C3++;
        }
        int CY1 = C1 + DX12 * (minY << 4) - DY12 * (minX << 4);
        int CY2 = C2 + DX23 * (minY << 4) - DY23 * (minX << 4);
        int CY3 = C3 + DX31 * (minY << 4) - DY31 * (minX << 4);

        for (int y = minY; y < maxY; y++) {
            int CX1 = CY1;
            int CX2 = CY2;
            int CX3 = CY3;
            float p = zOff + dy * y;
            for (int x = minX; x < maxX; x++) {
                if (CX1 > 0 && CX2 > 0 && CX3 > 0) {
                    int point = x + off;
                    float zVal = p + dx * x;
                    buff[point] |= ((int) (zVal * 255)) << 24;
                }
                CX1 -= FDY12;
                CX2 -= FDY23;
                CX3 -= FDY31;
            }
            CY1 += FDX12;
            CY2 += FDX23;
            CY3 += FDX31;
            off += w;
        }
    }

    private int min(int x1, int x2, int x3) {
        return (x1 > x2) ? ((x2 > x3) ? x3 : x2) : ((x1 > x3) ? x3 : x1);
    }

    private int max(int x1, int x2, int x3) {
        return (x1 < x2) ? ((x2 < x3) ? x3 : x2) : ((x1 < x3) ? x3 : x1);
    }

    public void draw(@NonNull Canvas c) {
        c.drawBitmap(mBitmap, 0, 0, null);
    }
}
+630 −0

File added.

Preview size limit exceeded, changes collapsed.

−8.83 KiB (13.3 KiB)
Loading image diff...
+2 −2
Original line number Diff line number Diff line
@@ -65,7 +65,7 @@
            android:layout_height="40dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:elevation="100dp"
            android:elevation="108dp"
            android:stateListAnimator="@null"/>

    </RelativeLayout>
@@ -90,7 +90,7 @@
            android:layout_height="40dp"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:elevation="36dp"
            android:elevation="108dp"
            android:stateListAnimator="@null"/>

    </RelativeLayout>