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