1 /* 2 * Copyright (C) 2008 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; 18 19 import static com.android.launcher3.anim.Interpolators.ACCEL; 20 21 import android.animation.ObjectAnimator; 22 import android.graphics.Bitmap; 23 import android.graphics.Canvas; 24 import android.graphics.Color; 25 import android.graphics.ColorFilter; 26 import android.graphics.ColorMatrix; 27 import android.graphics.ColorMatrixColorFilter; 28 import android.graphics.Paint; 29 import android.graphics.PixelFormat; 30 import android.graphics.PorterDuff; 31 import android.graphics.PorterDuffColorFilter; 32 import android.graphics.Rect; 33 import android.graphics.drawable.Drawable; 34 import android.util.Property; 35 import android.util.SparseArray; 36 37 import com.android.launcher3.graphics.BitmapInfo; 38 39 public class FastBitmapDrawable extends Drawable { 40 41 private static final float PRESSED_SCALE = 1.1f; 42 43 private static final float DISABLED_DESATURATION = 1f; 44 private static final float DISABLED_BRIGHTNESS = 0.5f; 45 46 public static final int CLICK_FEEDBACK_DURATION = 200; 47 48 // Since we don't need 256^2 values for combinations of both the brightness and saturation, we 49 // reduce the value space to a smaller value V, which reduces the number of cached 50 // ColorMatrixColorFilters that we need to keep to V^2 51 private static final int REDUCED_FILTER_VALUE_SPACE = 48; 52 53 // A cache of ColorFilters for optimizing brightness and saturation animations 54 private static final SparseArray<ColorFilter> sCachedFilter = new SparseArray<>(); 55 56 // Temporary matrices used for calculation 57 private static final ColorMatrix sTempBrightnessMatrix = new ColorMatrix(); 58 private static final ColorMatrix sTempFilterMatrix = new ColorMatrix(); 59 60 protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); 61 protected Bitmap mBitmap; 62 protected final int mIconColor; 63 64 private boolean mIsPressed; 65 private boolean mIsDisabled; 66 67 // Animator and properties for the fast bitmap drawable's scale 68 private static final Property<FastBitmapDrawable, Float> SCALE 69 = new Property<FastBitmapDrawable, Float>(Float.TYPE, "scale") { 70 @Override 71 public Float get(FastBitmapDrawable fastBitmapDrawable) { 72 return fastBitmapDrawable.mScale; 73 } 74 75 @Override 76 public void set(FastBitmapDrawable fastBitmapDrawable, Float value) { 77 fastBitmapDrawable.mScale = value; 78 fastBitmapDrawable.invalidateSelf(); 79 } 80 }; 81 private ObjectAnimator mScaleAnimation; 82 private float mScale = 1; 83 84 85 // The saturation and brightness are values that are mapped to REDUCED_FILTER_VALUE_SPACE and 86 // as a result, can be used to compose the key for the cached ColorMatrixColorFilters 87 private int mDesaturation = 0; 88 private int mBrightness = 0; 89 private int mAlpha = 255; 90 private int mPrevUpdateKey = Integer.MAX_VALUE; 91 92 public FastBitmapDrawable(Bitmap b) { 93 this(b, Color.TRANSPARENT); 94 } 95 96 public FastBitmapDrawable(BitmapInfo info) { 97 this(info.icon, info.color); 98 } 99 100 public FastBitmapDrawable(ItemInfoWithIcon info) { 101 this(info.iconBitmap, info.iconColor); 102 } 103 104 protected FastBitmapDrawable(Bitmap b, int iconColor) { 105 mBitmap = b; 106 mIconColor = iconColor; 107 setFilterBitmap(true); 108 } 109 110 @Override 111 public final void draw(Canvas canvas) { 112 if (mScaleAnimation != null) { 113 int count = canvas.save(); 114 Rect bounds = getBounds(); 115 canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY()); 116 drawInternal(canvas, bounds); 117 canvas.restoreToCount(count); 118 } else { 119 drawInternal(canvas, getBounds()); 120 } 121 } 122 123 protected void drawInternal(Canvas canvas, Rect bounds) { 124 canvas.drawBitmap(mBitmap, null, bounds, mPaint); 125 } 126 127 @Override 128 public void setColorFilter(ColorFilter cf) { 129 // No op 130 } 131 132 @Override 133 public int getOpacity() { 134 return PixelFormat.TRANSLUCENT; 135 } 136 137 @Override 138 public void setAlpha(int alpha) { 139 mAlpha = alpha; 140 mPaint.setAlpha(alpha); 141 } 142 143 @Override 144 public void setFilterBitmap(boolean filterBitmap) { 145 mPaint.setFilterBitmap(filterBitmap); 146 mPaint.setAntiAlias(filterBitmap); 147 } 148 149 public int getAlpha() { 150 return mAlpha; 151 } 152 153 public float getAnimatedScale() { 154 return mScaleAnimation == null ? 1 : mScale; 155 } 156 157 @Override 158 public int getIntrinsicWidth() { 159 return mBitmap.getWidth(); 160 } 161 162 @Override 163 public int getIntrinsicHeight() { 164 return mBitmap.getHeight(); 165 } 166 167 @Override 168 public int getMinimumWidth() { 169 return getBounds().width(); 170 } 171 172 @Override 173 public int getMinimumHeight() { 174 return getBounds().height(); 175 } 176 177 @Override 178 public boolean isStateful() { 179 return true; 180 } 181 182 @Override 183 public ColorFilter getColorFilter() { 184 return mPaint.getColorFilter(); 185 } 186 187 @Override 188 protected boolean onStateChange(int[] state) { 189 boolean isPressed = false; 190 for (int s : state) { 191 if (s == android.R.attr.state_pressed) { 192 isPressed = true; 193 break; 194 } 195 } 196 if (mIsPressed != isPressed) { 197 mIsPressed = isPressed; 198 199 if (mScaleAnimation != null) { 200 mScaleAnimation.cancel(); 201 mScaleAnimation = null; 202 } 203 204 if (mIsPressed) { 205 // Animate when going to pressed state 206 mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE); 207 mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION); 208 mScaleAnimation.setInterpolator(ACCEL); 209 mScaleAnimation.start(); 210 } else { 211 mScale = 1f; 212 invalidateSelf(); 213 } 214 return true; 215 } 216 return false; 217 } 218 219 private void invalidateDesaturationAndBrightness() { 220 setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0); 221 setBrightness(mIsDisabled ? DISABLED_BRIGHTNESS : 0); 222 } 223 224 public void setIsDisabled(boolean isDisabled) { 225 if (mIsDisabled != isDisabled) { 226 mIsDisabled = isDisabled; 227 invalidateDesaturationAndBrightness(); 228 } 229 } 230 231 /** 232 * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] 233 */ 234 private void setDesaturation(float desaturation) { 235 int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE); 236 if (mDesaturation != newDesaturation) { 237 mDesaturation = newDesaturation; 238 updateFilter(); 239 } 240 } 241 242 public float getDesaturation() { 243 return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE; 244 } 245 246 /** 247 * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] 248 */ 249 private void setBrightness(float brightness) { 250 int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE); 251 if (mBrightness != newBrightness) { 252 mBrightness = newBrightness; 253 updateFilter(); 254 } 255 } 256 257 private float getBrightness() { 258 return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE; 259 } 260 261 /** 262 * Updates the paint to reflect the current brightness and saturation. 263 */ 264 private void updateFilter() { 265 boolean usePorterDuffFilter = false; 266 int key = -1; 267 if (mDesaturation > 0) { 268 key = (mDesaturation << 16) | mBrightness; 269 } else if (mBrightness > 0) { 270 // Compose a key with a fully saturated icon if we are just animating brightness 271 key = (1 << 16) | mBrightness; 272 273 // We found that in L, ColorFilters cause drawing artifacts with shadows baked into 274 // icons, so just use a PorterDuff filter when we aren't animating saturation 275 usePorterDuffFilter = true; 276 } 277 278 // Debounce multiple updates on the same frame 279 if (key == mPrevUpdateKey) { 280 return; 281 } 282 mPrevUpdateKey = key; 283 284 if (key != -1) { 285 ColorFilter filter = sCachedFilter.get(key); 286 if (filter == null) { 287 float brightnessF = getBrightness(); 288 int brightnessI = (int) (255 * brightnessF); 289 if (usePorterDuffFilter) { 290 filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255), 291 PorterDuff.Mode.SRC_ATOP); 292 } else { 293 float saturationF = 1f - getDesaturation(); 294 sTempFilterMatrix.setSaturation(saturationF); 295 if (mBrightness > 0) { 296 // Brightness: C-new = C-old*(1-amount) + amount 297 float scale = 1f - brightnessF; 298 float[] mat = sTempBrightnessMatrix.getArray(); 299 mat[0] = scale; 300 mat[6] = scale; 301 mat[12] = scale; 302 mat[4] = brightnessI; 303 mat[9] = brightnessI; 304 mat[14] = brightnessI; 305 sTempFilterMatrix.preConcat(sTempBrightnessMatrix); 306 } 307 filter = new ColorMatrixColorFilter(sTempFilterMatrix); 308 } 309 sCachedFilter.append(key, filter); 310 } 311 mPaint.setColorFilter(filter); 312 } else { 313 mPaint.setColorFilter(null); 314 } 315 invalidateSelf(); 316 } 317 318 @Override 319 public ConstantState getConstantState() { 320 return new MyConstantState(mBitmap, mIconColor); 321 } 322 323 protected static class MyConstantState extends ConstantState { 324 protected final Bitmap mBitmap; 325 protected final int mIconColor; 326 327 public MyConstantState(Bitmap bitmap, int color) { 328 mBitmap = bitmap; 329 mIconColor = color; 330 } 331 332 @Override 333 public Drawable newDrawable() { 334 return new FastBitmapDrawable(mBitmap, mIconColor); 335 } 336 337 @Override 338 public int getChangingConfigurations() { 339 return 0; 340 } 341 } 342 } 343