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