summaryrefslogtreecommitdiff
path: root/android/view/RectShadowPainter.java
blob: 88771a765399b420d741047b248d7bee5e942bf4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/*
 * Copyright (C) 2015 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 android.view;

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

import android.graphics.BaseCanvas_Delegate;
import android.graphics.Canvas;
import android.graphics.Canvas_Delegate;
import android.graphics.Color;
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;

/**
 * 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 int START_COLOR = ResourceHelper.getColor("#37000000");
    private static final int END_COLOR = ResourceHelper.getColor("#03000000");
    private static final float PERPENDICULAR_ANGLE = 90f;

    public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas,
            float alpha) {
        Rect outline = new Rect();
        if (!viewOutline.getRect(outline)) {
            assert false : "Outline is not a rect shadow";
            return;
        }

        // TODO replacing the algorithm here to create better shadow

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

        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};
            if (alpha != 1f) {
                // Correct colors using the given component alpha
                for (int i = 0; i < colors.length; i++) {
                    colors[i] = Color.argb((int) (Color.alpha(colors[i]) * alpha), Color.red(colors[i]),
                            Color.green(colors[i]), Color.blue(colors[i]));
                }
            }
            cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors,
                    new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP));
            edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, colors[0], colors[2],
                    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);
        } 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;
    }

    /**
     * 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;
        }
        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);
        }
        canvas.translate(0, shadowSize / 2f);
        return saved;
    }

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

    /**
     * @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);
    }

    /**
     * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks.
     * <p/>
     * This is required because {@link BaseCanvas_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;
    }
}