100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta/* 200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Copyright (C) 2015 The Android Open Source Project 300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * 400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Licensed under the Apache License, Version 2.0 (the "License"); 500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * you may not use this file except in compliance with the License. 600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * You may obtain a copy of the License at 700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * 800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * http://www.apache.org/licenses/LICENSE-2.0 900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * 1000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Unless required by applicable law or agreed to in writing, software 1100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * distributed under the License is distributed on an "AS IS" BASIS, 1200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * See the License for the specific language governing permissions and 1400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * limitations under the License. 1500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta */ 1600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 1700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptapackage android.view; 1800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 1900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport com.android.layoutlib.bridge.impl.ResourceHelper; 2000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 2100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Canvas; 220aa004c3cff627167e302d7320629ccb41cab585Deepanshu Guptaimport android.graphics.Canvas_Delegate; 2300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.LinearGradient; 2400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Outline; 2500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Paint; 2600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Paint.Style; 2700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Path; 2800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Path.FillType; 2900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.RadialGradient; 3000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Rect; 3100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.RectF; 3200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Region.Op; 3300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptaimport android.graphics.Shader.TileMode; 3400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 3500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta/** 3600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Paints shadow for rounded rectangles. Inspiration from CardView. Couldn't use that directly, 3700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * since it modifies the size of the content, that we can't do. 3800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta */ 3900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Guptapublic class RectShadowPainter { 4000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 4100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 4200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta private static final int START_COLOR = ResourceHelper.getColor("#37000000"); 4300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta private static final int END_COLOR = ResourceHelper.getColor("#03000000"); 4400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta private static final float PERPENDICULAR_ANGLE = 90f; 4500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 4600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas) { 47a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez Rect outline = new Rect(); 48a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez if (!viewOutline.getRect(outline)) { 49a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez throw new IllegalArgumentException("Outline is not a rect shadow"); 50a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez } 51a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez 5200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta float shadowSize = elevationToShadow(elevation); 5300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta int saved = modifyCanvas(canvas, shadowSize); 5400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta if (saved == -1) { 5500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta return; 5600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 5700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta try { 5800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 5900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta cornerPaint.setStyle(Style.FILL); 6000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta Paint edgePaint = new Paint(cornerPaint); 6100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta edgePaint.setAntiAlias(false); 62a4d7ad8663e624d46f53ac0e5948348348d82204Diego Perez float radius = viewOutline.getRadius(); 6300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta float outerArcRadius = radius + shadowSize; 6400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta int[] colors = {START_COLOR, START_COLOR, END_COLOR}; 6500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors, 6600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP)); 6700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, START_COLOR, END_COLOR, 6800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta TileMode.CLAMP)); 6900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta Path path = new Path(); 7000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta path.setFillType(FillType.EVEN_ODD); 7100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // A rectangle bounding the complete shadow. 7200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta RectF shadowRect = new RectF(outline); 7300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta shadowRect.inset(-shadowSize, -shadowSize); 7400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // A rectangle with edges corresponding to the straight edges of the outline. 7500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta RectF inset = new RectF(outline); 7600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta inset.inset(radius, radius); 7700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // A rectangle used to represent the edge shadow. 7800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta RectF edgeShadowRect = new RectF(); 7900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 8000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 8100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // left and right sides. 8200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta edgeShadowRect.set(-shadowSize, 0f, 0f, inset.height()); 8300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // Left shadow 8400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta sideShadow(canvas, edgePaint, edgeShadowRect, outline.left, inset.top, 0); 8500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // Right shadow 8600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta sideShadow(canvas, edgePaint, edgeShadowRect, outline.right, inset.bottom, 2); 8700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // Top shadow 8800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta edgeShadowRect.set(-shadowSize, 0, 0, inset.width()); 8900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta sideShadow(canvas, edgePaint, edgeShadowRect, inset.right, outline.top, 1); 9000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // bottom shadow. This needs an inset so that blank doesn't appear when the content is 9100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // moved up. 9200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta edgeShadowRect.set(-shadowSize, 0, shadowSize / 2f, inset.width()); 9300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta edgePaint.setShader(new LinearGradient(edgeShadowRect.right, 0, edgeShadowRect.left, 0, 9400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta colors, new float[]{0f, 1 / 3f, 1f}, TileMode.CLAMP)); 9500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta sideShadow(canvas, edgePaint, edgeShadowRect, inset.left, outline.bottom, 3); 9600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 9700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // Draw corners. 9800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta drawCorner(canvas, cornerPaint, path, inset.right, inset.bottom, outerArcRadius, 0); 9900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta drawCorner(canvas, cornerPaint, path, inset.left, inset.bottom, outerArcRadius, 1); 10000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta drawCorner(canvas, cornerPaint, path, inset.left, inset.top, outerArcRadius, 2); 10100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta drawCorner(canvas, cornerPaint, path, inset.right, inset.top, outerArcRadius, 3); 10200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } finally { 10300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.restoreToCount(saved); 10400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 10500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 10600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 10700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta private static float elevationToShadow(float elevation) { 10800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // The factor is chosen by eyeballing the shadow size on device and preview. 10900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta return elevation * 0.5f; 11000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 11100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 11200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta /** 11300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * Translate canvas by half of shadow size up, so that it appears that light is coming 11400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * slightly from above. Also, remove clipping, so that shadow is not clipped. 11500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta */ 11600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta private static int modifyCanvas(Canvas canvas, float shadowSize) { 11700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta Rect clipBounds = canvas.getClipBounds(); 11800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta if (clipBounds.isEmpty()) { 11900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta return -1; 12000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 12100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta int saved = canvas.save(); 12200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // Usually canvas has been translated to the top left corner of the view when this is 12300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // called. So, setting a clip rect at 0,0 will clip the top left part of the shadow. 12400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta // Thus, we just expand in each direction by width and height of the canvas. 12500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.clipRect(-canvas.getWidth(), -canvas.getHeight(), canvas.getWidth(), 12600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.getHeight(), Op.REPLACE); 12700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.translate(0, shadowSize / 2f); 12800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta return saved; 12900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 13000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 13100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta private static void sideShadow(Canvas canvas, Paint edgePaint, 13200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta RectF edgeShadowRect, float dx, float dy, int rotations) { 1330aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta if (isRectEmpty(edgeShadowRect)) { 1340aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta return; 1350aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta } 13600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta int saved = canvas.save(); 13700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.translate(dx, dy); 13800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.rotate(rotations * PERPENDICULAR_ANGLE); 13900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.drawRect(edgeShadowRect, edgePaint); 14000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.restoreToCount(saved); 14100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 14200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta 14300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta /** 14400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * @param canvas Canvas to draw the rectangle on. 14500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * @param paint Paint to use when drawing the corner. 14600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * @param path A path to reuse. Prevents allocating memory for each path. 14700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * @param x Center of circle, which this corner is a part of. 14800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * @param y Center of circle, which this corner is a part of. 14900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * @param radius radius of the arc 15000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta * @param rotations number of quarter rotations before starting to paint the arc. 15100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta */ 15200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta private static void drawCorner(Canvas canvas, Paint paint, Path path, float x, float y, 15300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta float radius, int rotations) { 15400c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta int saved = canvas.save(); 15500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.translate(x, y); 15600c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta path.reset(); 15700c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta path.arcTo(-radius, -radius, radius, radius, rotations * PERPENDICULAR_ANGLE, 15800c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta PERPENDICULAR_ANGLE, false); 15900c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta path.lineTo(0, 0); 16000c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta path.close(); 16100c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.drawPath(path, paint); 16200c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta canvas.restoreToCount(saved); 16300c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta } 1640aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta 1650aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta /** 1660aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks. 1670aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta * <p/> 1680aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta * This is required because {@link Canvas_Delegate#native_drawRect(long, float, float, float, 1690aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta * float, long)} casts the co-ordinates to int and we want to ensure that it doesn't end up 1700aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta * drawing empty rectangles, which results in IllegalArgumentException. 1710aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta */ 1720aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta private static boolean isRectEmpty(RectF rect) { 1730aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta return (int) rect.left >= (int) rect.right || (int) rect.top >= (int) rect.bottom; 1740aa004c3cff627167e302d7320629ccb41cab585Deepanshu Gupta } 17500c2adf5db17ec2ab8c6709c5afde503cf6ea273Deepanshu Gupta} 176