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