1 /* 2 * Copyright (C) 2016 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 com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 20 21 import android.graphics.Bitmap; 22 import android.graphics.BlurMaskFilter; 23 import android.graphics.BlurMaskFilter.Blur; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.Paint; 27 import android.graphics.Path; 28 import android.graphics.PorterDuff; 29 import android.graphics.PorterDuffXfermode; 30 import android.graphics.RectF; 31 32 /** 33 * Utility class to add shadows to bitmaps. 34 */ 35 public class ShadowGenerator { 36 37 public static final boolean ENABLE_SHADOWS = true; 38 39 public static final float BLUR_FACTOR = 1.68f/48; 40 41 // Percent of actual icon size 42 public static final float KEY_SHADOW_DISTANCE = 1f/48; 43 private static final int KEY_SHADOW_ALPHA = 7; 44 // Percent of actual icon size 45 private static final float HALF_DISTANCE = 0.5f; 46 private static final int AMBIENT_SHADOW_ALPHA = 25; 47 48 private final int mIconSize; 49 50 private final Paint mBlurPaint; 51 private final Paint mDrawPaint; 52 private final BlurMaskFilter mDefaultBlurMaskFilter; 53 ShadowGenerator(int iconSize)54 public ShadowGenerator(int iconSize) { 55 mIconSize = iconSize; 56 mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 57 mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 58 mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL); 59 } 60 drawShadow(Bitmap icon, Canvas out)61 public synchronized void drawShadow(Bitmap icon, Canvas out) { 62 if (ENABLE_SHADOWS) { 63 int[] offset = new int[2]; 64 mBlurPaint.setMaskFilter(mDefaultBlurMaskFilter); 65 Bitmap shadow = icon.extractAlpha(mBlurPaint, offset); 66 67 // Draw ambient shadow 68 mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA); 69 out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint); 70 71 // Draw key shadow 72 mDrawPaint.setAlpha(KEY_SHADOW_ALPHA); 73 out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize, 74 mDrawPaint); 75 } 76 } 77 78 /** package private **/ addPathShadow(Path path, Canvas out)79 void addPathShadow(Path path, Canvas out) { 80 if (ENABLE_SHADOWS) { 81 mDrawPaint.setMaskFilter(mDefaultBlurMaskFilter); 82 83 // Draw ambient shadow 84 mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA); 85 out.drawPath(path, mDrawPaint); 86 87 // Draw key shadow 88 int save = out.save(); 89 mDrawPaint.setAlpha(KEY_SHADOW_ALPHA); 90 out.translate(0, KEY_SHADOW_DISTANCE * mIconSize); 91 out.drawPath(path, mDrawPaint); 92 out.restoreToCount(save); 93 94 mDrawPaint.setMaskFilter(null); 95 } 96 } 97 98 /** 99 * Returns the minimum amount by which an icon with {@param bounds} should be scaled 100 * so that the shadows do not get clipped. 101 */ getScaleForBounds(RectF bounds)102 public static float getScaleForBounds(RectF bounds) { 103 float scale = 1; 104 105 if (ENABLE_SHADOWS) { 106 // For top, left & right, we need same space. 107 float minSide = Math.min(Math.min(bounds.left, bounds.right), bounds.top); 108 if (minSide < BLUR_FACTOR) { 109 scale = (HALF_DISTANCE - BLUR_FACTOR) / (HALF_DISTANCE - minSide); 110 } 111 112 // We are ignoring KEY_SHADOW_DISTANCE because regular icons ignore this at the moment b/298203449 113 float bottomSpace = BLUR_FACTOR; 114 if (bounds.bottom < bottomSpace) { 115 scale = Math.min(scale, 116 (HALF_DISTANCE - bottomSpace) / (HALF_DISTANCE - bounds.bottom)); 117 } 118 } 119 return scale; 120 } 121 122 public static class Builder { 123 124 public final RectF bounds = new RectF(); 125 public final int color; 126 127 public int ambientShadowAlpha = AMBIENT_SHADOW_ALPHA; 128 129 public float shadowBlur; 130 131 public float keyShadowDistance; 132 public int keyShadowAlpha = KEY_SHADOW_ALPHA; 133 public float radius; 134 Builder(int color)135 public Builder(int color) { 136 this.color = color; 137 } 138 setupBlurForSize(int height)139 public Builder setupBlurForSize(int height) { 140 if (ENABLE_SHADOWS) { 141 shadowBlur = height * 1f / 24; 142 keyShadowDistance = height * 1f / 16; 143 } else { 144 shadowBlur = 0; 145 keyShadowDistance = 0; 146 } 147 return this; 148 } 149 createPill(int width, int height)150 public Bitmap createPill(int width, int height) { 151 return createPill(width, height, height / 2f); 152 } 153 createPill(int width, int height, float r)154 public Bitmap createPill(int width, int height, float r) { 155 radius = r; 156 157 int centerX = Math.round(width / 2f + shadowBlur); 158 int centerY = Math.round(radius + shadowBlur + keyShadowDistance); 159 int center = Math.max(centerX, centerY); 160 bounds.set(0, 0, width, height); 161 bounds.offsetTo(center - width / 2f, center - height / 2f); 162 163 int size = center * 2; 164 return BitmapRenderer.createHardwareBitmap(size, size, this::drawShadow); 165 } 166 drawShadow(Canvas c)167 public void drawShadow(Canvas c) { 168 Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 169 p.setColor(color); 170 171 if (ENABLE_SHADOWS) { 172 // Key shadow 173 p.setShadowLayer(shadowBlur, 0, keyShadowDistance, 174 setColorAlphaBound(Color.BLACK, keyShadowAlpha)); 175 c.drawRoundRect(bounds, radius, radius, p); 176 177 // Ambient shadow 178 p.setShadowLayer(shadowBlur, 0, 0, 179 setColorAlphaBound(Color.BLACK, ambientShadowAlpha)); 180 c.drawRoundRect(bounds, radius, radius, p); 181 } 182 183 if (Color.alpha(color) < 255) { 184 // Clear any content inside the pill-rect for translucent fill. 185 p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 186 p.clearShadowLayer(); 187 p.setColor(Color.BLACK); 188 c.drawRoundRect(bounds, radius, radius, p); 189 190 p.setXfermode(null); 191 p.setColor(color); 192 c.drawRoundRect(bounds, radius, radius, p); 193 } 194 } 195 } 196 } 197