100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta/*
200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Copyright (C) 2015 The Android Open Source Project
300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta *
400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Licensed under the Apache License, Version 2.0 (the "License");
500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * you may not use this file except in compliance with the License.
600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * You may obtain a copy of the License at
700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta *
800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta *      http://www.apache.org/licenses/LICENSE-2.0
900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta *
1000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Unless required by applicable law or agreed to in writing, software
1100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * distributed under the License is distributed on an "AS IS" BASIS,
1200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * See the License for the specific language governing permissions and
1400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * limitations under the License.
1500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta */
1600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
1700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptapackage android.view;
1800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
1900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport com.android.layoutlib.bridge.impl.ResourceHelper;
2000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
2100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Canvas;
220aa004c3cff627167e302d7320629ccb41cab585Deepanshu Guptaimport android.graphics.Canvas_Delegate;
2300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.LinearGradient;
2400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Outline;
2500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Paint;
2600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Paint.Style;
2700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Path;
2800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Path.FillType;
2900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.RadialGradient;
3000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Rect;
3100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.RectF;
3200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Region.Op;
3300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Shader.TileMode;
3400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
3500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta/**
3600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Paints shadow for rounded rectangles. Inspiration from CardView. Couldn't use that directly,
3700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * since it modifies the size of the content, that we can't do.
3800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta */
3900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptapublic class RectShadowPainter {
4000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
4100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
4200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    private static final int START_COLOR = ResourceHelper.getColor("#37000000");
4300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    private static final int END_COLOR = ResourceHelper.getColor("#03000000");
4400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    private static final float PERPENDICULAR_ANGLE = 90f;
4500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
4600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas) {
47a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez        Rect outline = new Rect();
48a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez        if (!viewOutline.getRect(outline)) {
49a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez            throw new IllegalArgumentException("Outline is not a rect shadow");
50a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez        }
51a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez
5200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        float shadowSize = elevationToShadow(elevation);
5300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        int saved = modifyCanvas(canvas, shadowSize);
5400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        if (saved == -1) {
5500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            return;
5600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        }
5700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        try {
5800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
5900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            cornerPaint.setStyle(Style.FILL);
6000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            Paint edgePaint = new Paint(cornerPaint);
6100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            edgePaint.setAntiAlias(false);
62a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez            float radius = viewOutline.getRadius();
6300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            float outerArcRadius = radius + shadowSize;
6400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            int[] colors = {START_COLOR, START_COLOR, END_COLOR};
6500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors,
6600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta                    new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP));
6700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, START_COLOR, END_COLOR,
6800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta                    TileMode.CLAMP));
6900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            Path path = new Path();
7000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            path.setFillType(FillType.EVEN_ODD);
7100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // A rectangle bounding the complete shadow.
7200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            RectF shadowRect = new RectF(outline);
7300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            shadowRect.inset(-shadowSize, -shadowSize);
7400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // A rectangle with edges corresponding to the straight edges of the outline.
7500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            RectF inset = new RectF(outline);
7600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            inset.inset(radius, radius);
7700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // A rectangle used to represent the edge shadow.
7800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            RectF edgeShadowRect = new RectF();
7900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
8000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
8100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // left and right sides.
8200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            edgeShadowRect.set(-shadowSize, 0f, 0f, inset.height());
8300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // Left shadow
8400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            sideShadow(canvas, edgePaint, edgeShadowRect, outline.left, inset.top, 0);
8500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // Right shadow
8600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            sideShadow(canvas, edgePaint, edgeShadowRect, outline.right, inset.bottom, 2);
8700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // Top shadow
8800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            edgeShadowRect.set(-shadowSize, 0, 0, inset.width());
8900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            sideShadow(canvas, edgePaint, edgeShadowRect, inset.right, outline.top, 1);
9000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // bottom shadow. This needs an inset so that blank doesn't appear when the content is
9100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // moved up.
9200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            edgeShadowRect.set(-shadowSize, 0, shadowSize / 2f, inset.width());
9300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            edgePaint.setShader(new LinearGradient(edgeShadowRect.right, 0, edgeShadowRect.left, 0,
9400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta                    colors, new float[]{0f, 1 / 3f, 1f}, TileMode.CLAMP));
9500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            sideShadow(canvas, edgePaint, edgeShadowRect, inset.left, outline.bottom, 3);
9600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
9700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            // Draw corners.
9800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            drawCorner(canvas, cornerPaint, path, inset.right, inset.bottom, outerArcRadius, 0);
9900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            drawCorner(canvas, cornerPaint, path, inset.left, inset.bottom, outerArcRadius, 1);
10000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            drawCorner(canvas, cornerPaint, path, inset.left, inset.top, outerArcRadius, 2);
10100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            drawCorner(canvas, cornerPaint, path, inset.right, inset.top, outerArcRadius, 3);
10200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        } finally {
10300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            canvas.restoreToCount(saved);
10400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        }
10500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    }
10600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
10700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    private static float elevationToShadow(float elevation) {
10800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        // The factor is chosen by eyeballing the shadow size on device and preview.
10900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        return elevation * 0.5f;
11000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    }
11100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
11200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    /**
11300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * Translate canvas by half of shadow size up, so that it appears that light is coming
11400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * slightly from above. Also, remove clipping, so that shadow is not clipped.
11500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     */
11600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    private static int modifyCanvas(Canvas canvas, float shadowSize) {
11700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        Rect clipBounds = canvas.getClipBounds();
11800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        if (clipBounds.isEmpty()) {
11900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            return -1;
12000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        }
12100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        int saved = canvas.save();
12200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        // Usually canvas has been translated to the top left corner of the view when this is
12300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        // called. So, setting a clip rect at 0,0 will clip the top left part of the shadow.
12400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        // Thus, we just expand in each direction by width and height of the canvas.
12500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.clipRect(-canvas.getWidth(), -canvas.getHeight(), canvas.getWidth(),
12600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta                canvas.getHeight(), Op.REPLACE);
12700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.translate(0, shadowSize / 2f);
12800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        return saved;
12900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    }
13000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
13100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    private static void sideShadow(Canvas canvas, Paint edgePaint,
13200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            RectF edgeShadowRect, float dx, float dy, int rotations) {
1330aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta        if (isRectEmpty(edgeShadowRect)) {
1340aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta            return;
1350aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta        }
13600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        int saved = canvas.save();
13700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.translate(dx, dy);
13800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.rotate(rotations * PERPENDICULAR_ANGLE);
13900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.drawRect(edgeShadowRect, edgePaint);
14000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.restoreToCount(saved);
14100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    }
14200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta
14300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    /**
14400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * @param canvas Canvas to draw the rectangle on.
14500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * @param paint Paint to use when drawing the corner.
14600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * @param path A path to reuse. Prevents allocating memory for each path.
14700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * @param x Center of circle, which this corner is a part of.
14800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * @param y Center of circle, which this corner is a part of.
14900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * @param radius radius of the arc
15000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     * @param rotations number of quarter rotations before starting to paint the arc.
15100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta     */
15200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    private static void drawCorner(Canvas canvas, Paint paint, Path path, float x, float y,
15300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta            float radius, int rotations) {
15400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        int saved = canvas.save();
15500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.translate(x, y);
15600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        path.reset();
15700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        path.arcTo(-radius, -radius, radius, radius, rotations * PERPENDICULAR_ANGLE,
15800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta                PERPENDICULAR_ANGLE, false);
15900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        path.lineTo(0, 0);
16000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        path.close();
16100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.drawPath(path, paint);
16200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta        canvas.restoreToCount(saved);
16300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta    }
1640aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta
1650aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta    /**
1660aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta     * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks.
1670aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta     * <p/>
1680aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta     * This is required because {@link Canvas_Delegate#native_drawRect(long, float, float, float,
1690aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta     * float, long)} casts the co-ordinates to int and we want to ensure that it doesn't end up
1700aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta     * drawing empty rectangles, which results in IllegalArgumentException.
1710aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta     */
1720aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta    private static boolean isRectEmpty(RectF rect) {
1730aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta        return (int) rect.left >= (int) rect.right || (int) rect.top >= (int) rect.bottom;
1740aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta    }
17500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta}
176