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