Home | History | Annotate | Download | only in view
      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 
     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