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

Commit e18bf49b authored by Charlie Tsai's avatar Charlie Tsai
Browse files

Replace rectangle shadow algorithm

Test: Rectangle Shadow test
Change-Id: Id9635df8769e85d835dc6f99201b86e5bba110d2
parent 956d00cb
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>