1 /* 2 * Copyright (C) 2017 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 com.android.launcher3.icons; 18 19 import static android.graphics.Paint.ANTI_ALIAS_FLAG; 20 import static android.graphics.Paint.FILTER_BITMAP_FLAG; 21 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.Paint; 26 import android.graphics.Path; 27 import android.graphics.PathMeasure; 28 import android.graphics.Rect; 29 import android.graphics.RectF; 30 import android.util.Log; 31 import android.view.ViewDebug; 32 33 /** 34 * Used to draw a notification dot on top of an icon. 35 */ 36 public class DotRenderer { 37 38 private static final String TAG = "DotRenderer"; 39 40 // The dot size is defined as a percentage of the app icon size. 41 private static final float SIZE_PERCENTAGE = 0.228f; 42 43 private final float mCircleRadius; 44 private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); 45 46 private final Bitmap mBackgroundWithShadow; 47 private final float mBitmapOffset; 48 49 // Stores the center x and y position as a percentage (0 to 1) of the icon size 50 private final float[] mRightDotPosition; 51 private final float[] mLeftDotPosition; 52 53 private static final int MIN_DOT_SIZE = 1; DotRenderer(int iconSizePx, Path iconShapePath, int pathSize)54 public DotRenderer(int iconSizePx, Path iconShapePath, int pathSize) { 55 int size = Math.round(SIZE_PERCENTAGE * iconSizePx); 56 if (size <= 0) { 57 size = MIN_DOT_SIZE; 58 } 59 ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.TRANSPARENT); 60 builder.ambientShadowAlpha = 88; 61 mBackgroundWithShadow = builder.setupBlurForSize(size).createPill(size, size); 62 mCircleRadius = builder.radius; 63 64 mBitmapOffset = -mBackgroundWithShadow.getHeight() * 0.5f; // Same as width. 65 66 // Find the points on the path that are closest to the top left and right corners. 67 mLeftDotPosition = getPathPoint(iconShapePath, pathSize, -1); 68 mRightDotPosition = getPathPoint(iconShapePath, pathSize, 1); 69 } 70 getPathPoint(Path path, float size, float direction)71 private static float[] getPathPoint(Path path, float size, float direction) { 72 float halfSize = size / 2; 73 // Small delta so that we don't get a zero size triangle 74 float delta = 1; 75 76 float x = halfSize + direction * halfSize; 77 Path trianglePath = new Path(); 78 trianglePath.moveTo(halfSize, halfSize); 79 trianglePath.lineTo(x + delta * direction, 0); 80 trianglePath.lineTo(x, -delta); 81 trianglePath.close(); 82 83 trianglePath.op(path, Path.Op.INTERSECT); 84 float[] pos = new float[2]; 85 new PathMeasure(trianglePath, false).getPosTan(0, pos, null); 86 87 pos[0] = pos[0] / size; 88 pos[1] = pos[1] / size; 89 return pos; 90 } 91 getLeftDotPosition()92 public float[] getLeftDotPosition() { 93 return mLeftDotPosition; 94 } 95 getRightDotPosition()96 public float[] getRightDotPosition() { 97 return mRightDotPosition; 98 } 99 100 /** 101 * Draw a circle on top of the canvas according to the given params. 102 */ draw(Canvas canvas, DrawParams params)103 public void draw(Canvas canvas, DrawParams params) { 104 if (params == null) { 105 Log.e(TAG, "Invalid null argument(s) passed in call to draw."); 106 return; 107 } 108 canvas.save(); 109 110 Rect iconBounds = params.iconBounds; 111 float[] dotPosition = params.leftAlign ? mLeftDotPosition : mRightDotPosition; 112 float dotCenterX = iconBounds.left + iconBounds.width() * dotPosition[0]; 113 float dotCenterY = iconBounds.top + iconBounds.height() * dotPosition[1]; 114 115 // Ensure dot fits entirely in canvas clip bounds. 116 Rect canvasBounds = canvas.getClipBounds(); 117 float offsetX = params.leftAlign 118 ? Math.max(0, canvasBounds.left - (dotCenterX + mBitmapOffset)) 119 : Math.min(0, canvasBounds.right - (dotCenterX - mBitmapOffset)); 120 float offsetY = Math.max(0, canvasBounds.top - (dotCenterY + mBitmapOffset)); 121 122 // We draw the dot relative to its center. 123 canvas.translate(dotCenterX + offsetX, dotCenterY + offsetY); 124 canvas.scale(params.scale, params.scale); 125 126 mCirclePaint.setColor(Color.BLACK); 127 canvas.drawBitmap(mBackgroundWithShadow, mBitmapOffset, mBitmapOffset, mCirclePaint); 128 mCirclePaint.setColor(params.dotColor); 129 canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint); 130 canvas.restore(); 131 } 132 133 public static class DrawParams { 134 /** The color (possibly based on the icon) to use for the dot. */ 135 @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true) 136 public int dotColor; 137 /** The color (possibly based on the icon) to use for a predicted app. */ 138 @ViewDebug.ExportedProperty(category = "notification dot", formatToHexString = true) 139 public int appColor; 140 /** The bounds of the icon that the dot is drawn on top of. */ 141 @ViewDebug.ExportedProperty(category = "notification dot") 142 public Rect iconBounds = new Rect(); 143 /** The progress of the animation, from 0 to 1. */ 144 @ViewDebug.ExportedProperty(category = "notification dot") 145 public float scale; 146 /** Whether the dot should align to the top left of the icon rather than the top right. */ 147 @ViewDebug.ExportedProperty(category = "notification dot") 148 public boolean leftAlign; 149 } 150 } 151