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.badge; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.graphics.Point; 26 import android.graphics.Rect; 27 import android.graphics.Shader; 28 import android.support.annotation.Nullable; 29 import android.util.SparseArray; 30 31 import com.android.launcher3.R; 32 import com.android.launcher3.graphics.IconPalette; 33 import com.android.launcher3.graphics.ShadowGenerator; 34 35 /** 36 * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge). 37 * @see BadgeInfo for the data to draw 38 */ 39 public class BadgeRenderer { 40 41 private static final boolean DOTS_ONLY = true; 42 43 // The badge sizes are defined as percentages of the app icon size. 44 private static final float SIZE_PERCENTAGE = 0.38f; 45 // Used to expand the width of the badge for each additional digit. 46 private static final float CHAR_SIZE_PERCENTAGE = 0.12f; 47 private static final float TEXT_SIZE_PERCENTAGE = 0.26f; 48 private static final float OFFSET_PERCENTAGE = 0.02f; 49 private static final float STACK_OFFSET_PERCENTAGE_X = 0.05f; 50 private static final float STACK_OFFSET_PERCENTAGE_Y = 0.06f; 51 private static final float DOT_SCALE = 0.6f; 52 53 private final Context mContext; 54 private final int mSize; 55 private final int mCharSize; 56 private final int mTextHeight; 57 private final int mOffset; 58 private final int mStackOffsetX; 59 private final int mStackOffsetY; 60 private final IconDrawer mLargeIconDrawer; 61 private final IconDrawer mSmallIconDrawer; 62 private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 63 private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG 64 | Paint.FILTER_BITMAP_FLAG); 65 private final SparseArray<Bitmap> mBackgroundsWithShadow; 66 67 public BadgeRenderer(Context context, int iconSizePx) { 68 mContext = context; 69 Resources res = context.getResources(); 70 mSize = (int) (SIZE_PERCENTAGE * iconSizePx); 71 mCharSize = (int) (CHAR_SIZE_PERCENTAGE * iconSizePx); 72 mOffset = (int) (OFFSET_PERCENTAGE * iconSizePx); 73 mStackOffsetX = (int) (STACK_OFFSET_PERCENTAGE_X * iconSizePx); 74 mStackOffsetY = (int) (STACK_OFFSET_PERCENTAGE_Y * iconSizePx); 75 mTextPaint.setTextSize(iconSizePx * TEXT_SIZE_PERCENTAGE); 76 mTextPaint.setTextAlign(Paint.Align.CENTER); 77 mLargeIconDrawer = new IconDrawer(res.getDimensionPixelSize(R.dimen.badge_small_padding)); 78 mSmallIconDrawer = new IconDrawer(res.getDimensionPixelSize(R.dimen.badge_large_padding)); 79 // Measure the text height. 80 Rect tempTextHeight = new Rect(); 81 mTextPaint.getTextBounds("0", 0, 1, tempTextHeight); 82 mTextHeight = tempTextHeight.height(); 83 84 mBackgroundsWithShadow = new SparseArray<>(3); 85 } 86 87 /** 88 * Draw a circle in the top right corner of the given bounds, and draw 89 * {@link BadgeInfo#getNotificationCount()} on top of the circle. 90 * @param palette The colors (based on the icon) to use for the badge. 91 * @param badgeInfo Contains data to draw on the badge. Could be null if we are animating out. 92 * @param iconBounds The bounds of the icon being badged. 93 * @param badgeScale The progress of the animation, from 0 to 1. 94 * @param spaceForOffset How much space is available to offset the badge up and to the right. 95 */ 96 public void draw(Canvas canvas, IconPalette palette, @Nullable BadgeInfo badgeInfo, 97 Rect iconBounds, float badgeScale, Point spaceForOffset) { 98 mTextPaint.setColor(palette.textColor); 99 IconDrawer iconDrawer = badgeInfo != null && badgeInfo.isIconLarge() 100 ? mLargeIconDrawer : mSmallIconDrawer; 101 Shader icon = badgeInfo == null ? null : badgeInfo.getNotificationIconForBadge( 102 mContext, palette.backgroundColor, mSize, iconDrawer.mPadding); 103 String notificationCount = badgeInfo == null ? "0" 104 : String.valueOf(badgeInfo.getNotificationCount()); 105 int numChars = notificationCount.length(); 106 int width = DOTS_ONLY ? mSize : mSize + mCharSize * (numChars - 1); 107 // Lazily load the background with shadow. 108 Bitmap backgroundWithShadow = mBackgroundsWithShadow.get(numChars); 109 if (backgroundWithShadow == null) { 110 backgroundWithShadow = ShadowGenerator.createPillWithShadow(Color.WHITE, width, mSize); 111 mBackgroundsWithShadow.put(numChars, backgroundWithShadow); 112 } 113 canvas.save(Canvas.MATRIX_SAVE_FLAG); 114 // We draw the badge relative to its center. 115 int badgeCenterX = iconBounds.right - width / 2; 116 int badgeCenterY = iconBounds.top + mSize / 2; 117 boolean isText = !DOTS_ONLY && badgeInfo != null && badgeInfo.getNotificationCount() != 0; 118 boolean isIcon = !DOTS_ONLY && icon != null; 119 boolean isDot = !(isText || isIcon); 120 if (isDot) { 121 badgeScale *= DOT_SCALE; 122 } 123 int offsetX = Math.min(mOffset, spaceForOffset.x); 124 int offsetY = Math.min(mOffset, spaceForOffset.y); 125 canvas.translate(badgeCenterX + offsetX, badgeCenterY - offsetY); 126 canvas.scale(badgeScale, badgeScale); 127 // Prepare the background and shadow and possible stacking effect. 128 mBackgroundPaint.setColorFilter(palette.backgroundColorMatrixFilter); 129 int backgroundWithShadowSize = backgroundWithShadow.getHeight(); // Same as width. 130 boolean shouldStack = !isDot && badgeInfo != null 131 && badgeInfo.getNotificationKeys().size() > 1; 132 if (shouldStack) { 133 int offsetDiffX = mStackOffsetX - mOffset; 134 int offsetDiffY = mStackOffsetY - mOffset; 135 canvas.translate(offsetDiffX, offsetDiffY); 136 canvas.drawBitmap(backgroundWithShadow, -backgroundWithShadowSize / 2, 137 -backgroundWithShadowSize / 2, mBackgroundPaint); 138 canvas.translate(-offsetDiffX, -offsetDiffY); 139 } 140 141 if (isText) { 142 canvas.drawBitmap(backgroundWithShadow, -backgroundWithShadowSize / 2, 143 -backgroundWithShadowSize / 2, mBackgroundPaint); 144 canvas.drawText(notificationCount, 0, mTextHeight / 2, mTextPaint); 145 } else if (isIcon) { 146 canvas.drawBitmap(backgroundWithShadow, -backgroundWithShadowSize / 2, 147 -backgroundWithShadowSize / 2, mBackgroundPaint); 148 iconDrawer.drawIcon(icon, canvas); 149 } else if (isDot) { 150 mBackgroundPaint.setColorFilter(palette.saturatedBackgroundColorMatrixFilter); 151 canvas.drawBitmap(backgroundWithShadow, -backgroundWithShadowSize / 2, 152 -backgroundWithShadowSize / 2, mBackgroundPaint); 153 } 154 canvas.restore(); 155 } 156 157 /** Draws the notification icon with padding of a given size. */ 158 private class IconDrawer { 159 160 private final int mPadding; 161 private final Bitmap mCircleClipBitmap; 162 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | 163 Paint.FILTER_BITMAP_FLAG); 164 165 public IconDrawer(int padding) { 166 mPadding = padding; 167 mCircleClipBitmap = Bitmap.createBitmap(mSize, mSize, Bitmap.Config.ALPHA_8); 168 Canvas canvas = new Canvas(); 169 canvas.setBitmap(mCircleClipBitmap); 170 canvas.drawCircle(mSize / 2, mSize / 2, mSize / 2 - padding, mPaint); 171 } 172 173 public void drawIcon(Shader icon, Canvas canvas) { 174 mPaint.setShader(icon); 175 canvas.drawBitmap(mCircleClipBitmap, -mSize / 2, -mSize / 2, mPaint); 176 mPaint.setShader(null); 177 } 178 } 179 } 180