/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view; import com.android.layoutlib.bridge.impl.GcSnapshot; import com.android.layoutlib.bridge.impl.ResourceHelper; import android.graphics.BaseCanvas_Delegate; import android.graphics.Canvas; import android.graphics.Canvas_Delegate; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.Path.FillType; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region.Op; import android.graphics.Shader.TileMode; import java.awt.Rectangle; /** * Paints shadow for rounded rectangles. Inspiration from CardView. Couldn't use that directly, * since it modifies the size of the content, that we can't do. */ public class RectShadowPainter { private static final int START_COLOR = ResourceHelper.getColor("#37000000"); private static final int END_COLOR = ResourceHelper.getColor("#03000000"); private static final float PERPENDICULAR_ANGLE = 90f; public static void paintShadow(Outline viewOutline, float elevation, Canvas canvas, float alpha) { Rect outline = new Rect(); if (!viewOutline.getRect(outline)) { assert false : "Outline is not a rect shadow"; return; } // TODO replacing the algorithm here to create better shadow float shadowSize = elevationToShadow(elevation); int saved = modifyCanvas(canvas, shadowSize); if (saved == -1) { return; } float radius = viewOutline.getRadius(); if (radius <= 0) { // We can not paint a shadow with radius 0 return; } try { Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); cornerPaint.setStyle(Style.FILL); Paint edgePaint = new Paint(cornerPaint); edgePaint.setAntiAlias(false); float outerArcRadius = radius + shadowSize; int[] colors = {START_COLOR, START_COLOR, END_COLOR}; if (alpha != 1f) { // Correct colors using the given component alpha for (int i = 0; i < colors.length; i++) { colors[i] = Color.argb((int) (Color.alpha(colors[i]) * alpha), Color.red(colors[i]), Color.green(colors[i]), Color.blue(colors[i])); } } cornerPaint.setShader(new RadialGradient(0, 0, outerArcRadius, colors, new float[]{0f, radius / outerArcRadius, 1f}, TileMode.CLAMP)); edgePaint.setShader(new LinearGradient(0, 0, -shadowSize, 0, colors[0], colors[2], TileMode.CLAMP)); Path path = new Path(); path.setFillType(FillType.EVEN_ODD); // A rectangle bounding the complete shadow. RectF shadowRect = new RectF(outline); shadowRect.inset(-shadowSize, -shadowSize); // A rectangle with edges corresponding to the straight edges of the outline. RectF inset = new RectF(outline); inset.inset(radius, radius); // A rectangle used to represent the edge shadow. RectF edgeShadowRect = new RectF(); // left and right sides. edgeShadowRect.set(-shadowSize, 0f, 0f, inset.height()); // Left shadow sideShadow(canvas, edgePaint, edgeShadowRect, outline.left, inset.top, 0); // Right shadow sideShadow(canvas, edgePaint, edgeShadowRect, outline.right, inset.bottom, 2); // Top shadow edgeShadowRect.set(-shadowSize, 0, 0, inset.width()); sideShadow(canvas, edgePaint, edgeShadowRect, inset.right, outline.top, 1); // bottom shadow. This needs an inset so that blank doesn't appear when the content is // moved up. edgeShadowRect.set(-shadowSize, 0, shadowSize / 2f, inset.width()); edgePaint.setShader(new LinearGradient(edgeShadowRect.right, 0, edgeShadowRect.left, 0, colors, new float[]{0f, 1 / 3f, 1f}, TileMode.CLAMP)); sideShadow(canvas, edgePaint, edgeShadowRect, inset.left, outline.bottom, 3); // Draw corners. drawCorner(canvas, cornerPaint, path, inset.right, inset.bottom, outerArcRadius, 0); drawCorner(canvas, cornerPaint, path, inset.left, inset.bottom, outerArcRadius, 1); drawCorner(canvas, cornerPaint, path, inset.left, inset.top, outerArcRadius, 2); drawCorner(canvas, cornerPaint, path, inset.right, inset.top, outerArcRadius, 3); } finally { canvas.restoreToCount(saved); } } private static float elevationToShadow(float elevation) { // The factor is chosen by eyeballing the shadow size on device and preview. return elevation * 0.5f; } /** * Translate canvas by half of shadow size up, so that it appears that light is coming * slightly from above. Also, remove clipping, so that shadow is not clipped. */ private static int modifyCanvas(Canvas canvas, float shadowSize) { Rect clipBounds = canvas.getClipBounds(); if (clipBounds.isEmpty()) { return -1; } int saved = canvas.save(); // Usually canvas has been translated to the top left corner of the view when this is // called. So, setting a clip rect at 0,0 will clip the top left part of the shadow. // Thus, we just expand in each direction by width and height of the canvas, while staying // inside the original drawing region. GcSnapshot snapshot = Canvas_Delegate.getDelegate(canvas).getSnapshot(); Rectangle originalClip = snapshot.getOriginalClip(); if (originalClip != null) { canvas.clipRect(originalClip.x, originalClip.y, originalClip.x + originalClip.width, originalClip.y + originalClip.height, Op.REPLACE); canvas.clipRect(-canvas.getWidth(), -canvas.getHeight(), canvas.getWidth(), canvas.getHeight(), Op.INTERSECT); } canvas.translate(0, shadowSize / 2f); return saved; } private static void sideShadow(Canvas canvas, Paint edgePaint, RectF edgeShadowRect, float dx, float dy, int rotations) { if (isRectEmpty(edgeShadowRect)) { return; } int saved = canvas.save(); canvas.translate(dx, dy); canvas.rotate(rotations * PERPENDICULAR_ANGLE); canvas.drawRect(edgeShadowRect, edgePaint); canvas.restoreToCount(saved); } /** * @param canvas Canvas to draw the rectangle on. * @param paint Paint to use when drawing the corner. * @param path A path to reuse. Prevents allocating memory for each path. * @param x Center of circle, which this corner is a part of. * @param y Center of circle, which this corner is a part of. * @param radius radius of the arc * @param rotations number of quarter rotations before starting to paint the arc. */ private static void drawCorner(Canvas canvas, Paint paint, Path path, float x, float y, float radius, int rotations) { int saved = canvas.save(); canvas.translate(x, y); path.reset(); path.arcTo(-radius, -radius, radius, radius, rotations * PERPENDICULAR_ANGLE, PERPENDICULAR_ANGLE, false); path.lineTo(0, 0); path.close(); canvas.drawPath(path, paint); canvas.restoreToCount(saved); } /** * Differs from {@link RectF#isEmpty()} as this first converts the rect to int and then checks. *
* This is required because {@link BaseCanvas_Delegate#native_drawRect(long, float, float, * float, * float, long)} casts the co-ordinates to int and we want to ensure that it doesn't end up * drawing empty rectangles, which results in IllegalArgumentException. */ private static boolean isRectEmpty(RectF rect) { return (int) rect.left >= (int) rect.right || (int) rect.top >= (int) rect.bottom; } }