1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import com.android.layoutlib.bridge.impl.GcSnapshot; 20 import com.android.layoutlib.bridge.impl.ResourceHelper; 21 22 import android.graphics.BaseCanvas_Delegate; 23 import android.graphics.Canvas; 24 import android.graphics.Canvas_Delegate; 25 import android.graphics.Color; 26 import android.graphics.LinearGradient; 27 import android.graphics.Outline; 28 import android.graphics.Paint; 29 import android.graphics.Paint.Style; 30 import android.graphics.Path; 31 import android.graphics.Path.FillType; 32 import android.graphics.RadialGradient; 33 import android.graphics.Rect; 34 import android.graphics.RectF; 35 import android.graphics.Region.Op; 36 import android.graphics.Shader.TileMode; 37 38 import java.awt.Rectangle; 39 40 /** 41 * Paints shadow for rounded rectangles. Inspiration from CardView. Couldn't use that directly, 42 * since it modifies the size of the content, that we can't do. 43 */ 44 public class RectShadowPainter { 45 46 47 private static final int START_COLOR = ResourceHelper.getColor("#37000000"); 48 private static final int END_COLOR = ResourceHelper.getColor("#03000000"); 49 private static final float PERPENDICULAR_ANGLE = 90f; 50 paintShadow(Outline viewOutline, float elevation, Canvas canvas, float alpha)51 public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas, 52 float alpha) { 53 Rect outline = new Rect(); 54 if (!viewOutline.getRect(outline)) { 55 assert false : "Outline is not a rect shadow"; 56 return; 57 } 58 59 // TODO replacing the algorithm here to create better shadow 60 61 float shadowSize = elevationToShadow(elevation); 62 int saved = modifyCanvas(canvas, shadowSize); 63 if (saved == -1) { 64 return; 65 } 66 67 float radius = viewOutline.getRadius(); 68 if (radius <= 0) { 69 // We can not paint a shadow with radius 0 70 return; 71 } 72 73 try { 74 Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 75 cornerPaint.setStyle(Style.FILL); 76 Paint edgePaint = new Paint(cornerPaint); 77 edgePaint.setAntiAlias(false); 78 float outerArcRadius = radius + shadowSize; 79 int[] colors = {START_COLOR, START_COLOR, END_COLOR}; 80 if (alpha != 1f) { 81 // Correct colors using the given component alpha 82 for (int i = 0; i < colors.length; i++) { 83 colors[i] = Color.argb((int) (Color.alpha(colors[i]) * alpha), Color.red(colors[i]), 84 Color.green(colors[i]), Color.blue(colors[i])); 85 } 86 } 87 cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors, 88 new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP)); 89 edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, colors[0], colors[2], 90 TileMode.CLAMP)); 91 Path path = new Path(); 92 path.setFillType(FillType.EVEN_ODD); 93 // A rectangle bounding the complete shadow. 94 RectF shadowRect = new RectF(outline); 95 shadowRect.inset(-shadowSize, -shadowSize); 96 // A rectangle with edges corresponding to the straight edges of the outline. 97 RectF inset = new RectF(outline); 98 inset.inset(radius, radius); 99 // A rectangle used to represent the edge shadow. 100 RectF edgeShadowRect = new RectF(); 101 102 103 // left and right sides. 104 edgeShadowRect.set(-shadowSize, 0f, 0f, inset.height()); 105 // Left shadow 106 sideShadow(canvas, edgePaint, edgeShadowRect, outline.left, inset.top, 0); 107 // Right shadow 108 sideShadow(canvas, edgePaint, edgeShadowRect, outline.right, inset.bottom, 2); 109 // Top shadow 110 edgeShadowRect.set(-shadowSize, 0, 0, inset.width()); 111 sideShadow(canvas, edgePaint, edgeShadowRect, inset.right, outline.top, 1); 112 // bottom shadow. This needs an inset so that blank doesn't appear when the content is 113 // moved up. 114 edgeShadowRect.set(-shadowSize, 0, shadowSize / 2f, inset.width()); 115 edgePaint.setShader(new LinearGradient(edgeShadowRect.right, 0, edgeShadowRect.left, 0, 116 colors, new float[]{0f, 1 / 3f, 1f}, TileMode.CLAMP)); 117 sideShadow(canvas, edgePaint, edgeShadowRect, inset.left, outline.bottom, 3); 118 119 // Draw corners. 120 drawCorner(canvas, cornerPaint, path, inset.right, inset.bottom, outerArcRadius, 0); 121 drawCorner(canvas, cornerPaint, path, inset.left, inset.bottom, outerArcRadius, 1); 122 drawCorner(canvas, cornerPaint, path, inset.left, inset.top, outerArcRadius, 2); 123 drawCorner(canvas, cornerPaint, path, inset.right, inset.top, outerArcRadius, 3); 124 } finally { 125 canvas.restoreToCount(saved); 126 } 127 } 128 elevationToShadow(float elevation)129 private static float elevationToShadow(float elevation) { 130 // The factor is chosen by eyeballing the shadow size on device and preview. 131 return elevation * 0.5f; 132 } 133 134 /** 135 * Translate canvas by half of shadow size up, so that it appears that light is coming 136 * slightly from above. Also, remove clipping, so that shadow is not clipped. 137 */ modifyCanvas(Canvas canvas, float shadowSize)138 private static int modifyCanvas(Canvas canvas, float shadowSize) { 139 Rect clipBounds = canvas.getClipBounds(); 140 if (clipBounds.isEmpty()) { 141 return -1; 142 } 143 int saved = canvas.save(); 144 // Usually canvas has been translated to the top left corner of the view when this is 145 // called. So, setting a clip rect at 0,0 will clip the top left part of the shadow. 146 // Thus, we just expand in each direction by width and height of the canvas, while staying 147 // inside the original drawing region. 148 GcSnapshot snapshot = Canvas_Delegate.getDelegate(canvas).getSnapshot(); 149 Rectangle originalClip = snapshot.getOriginalClip(); 150 if (originalClip != null) { 151 canvas.clipRect(originalClip.x, originalClip.y, originalClip.x + originalClip.width, 152 originalClip.y + originalClip.height, Op.REPLACE); 153 canvas.clipRect(-canvas.getWidth(), -canvas.getHeight(), canvas.getWidth(), 154 canvas.getHeight(), Op.INTERSECT); 155 } 156 canvas.translate(0, shadowSize / 2f); 157 return saved; 158 } 159 sideShadow(Canvas canvas, Paint edgePaint, RectF edgeShadowRect, float dx, float dy, int rotations)160 private static void sideShadow(Canvas canvas, Paint edgePaint, 161 RectF edgeShadowRect, float dx, float dy, int rotations) { 162 if (isRectEmpty(edgeShadowRect)) { 163 return; 164 } 165 int saved = canvas.save(); 166 canvas.translate(dx, dy); 167 canvas.rotate(rotations * PERPENDICULAR_ANGLE); 168 canvas.drawRect(edgeShadowRect, edgePaint); 169 canvas.restoreToCount(saved); 170 } 171 172 /** 173 * @param canvas Canvas to draw the rectangle on. 174 * @param paint Paint to use when drawing the corner. 175 * @param path A path to reuse. Prevents allocating memory for each path. 176 * @param x Center of circle, which this corner is a part of. 177 * @param y Center of circle, which this corner is a part of. 178 * @param radius radius of the arc 179 * @param rotations number of quarter rotations before starting to paint the arc. 180 */ drawCorner(Canvas canvas, Paint paint, Path path, float x, float y, float radius, int rotations)181 private static void drawCorner(Canvas canvas, Paint paint, Path path, float x, float y, 182 float radius, int rotations) { 183 int saved = canvas.save(); 184 canvas.translate(x, y); 185 path.reset(); 186 path.arcTo(-radius, -radius, radius, radius, rotations * PERPENDICULAR_ANGLE, 187 PERPENDICULAR_ANGLE, false); 188 path.lineTo(0, 0); 189 path.close(); 190 canvas.drawPath(path, paint); 191 canvas.restoreToCount(saved); 192 } 193 194 /** 195 * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks. 196 * <p/> 197 * This is required because {@link BaseCanvas_Delegate#native_drawRect(long, float, float, 198 * float, 199 * float, long)} casts the co-ordinates to int and we want to ensure that it doesn't end up 200 * drawing empty rectangles, which results in IllegalArgumentException. 201 */ isRectEmpty(RectF rect)202 private static boolean isRectEmpty(RectF rect) { 203 return (int) rect.left >= (int) rect.right || (int) rect.top >= (int) rect.bottom; 204 } 205 } 206