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 mAlpha = alpha; 146 mPaint.setAlpha(alpha); 147 } 148 149 @Override setFilterBitmap(boolean filterBitmap)150 public void setFilterBitmap(boolean filterBitmap) { 151 mPaint.setFilterBitmap(filterBitmap); 152 mPaint.setAntiAlias(filterBitmap); 153 } 154 getAlpha()155 public int getAlpha() { 156 return mAlpha; 157 } 158 setScale(float scale)159 public void setScale(float scale) { 160 if (mScaleAnimation != null) { 161 mScaleAnimation.cancel(); 162 mScaleAnimation = null; 163 } 164 mScale = scale; 165 invalidateSelf(); 166 } 167 getAnimatedScale()168 public float getAnimatedScale() { 169 return mScaleAnimation == null ? 1 : mScale; 170 } 171 getScale()172 public float getScale() { 173 return mScale; 174 } 175 176 @Override getIntrinsicWidth()177 public int getIntrinsicWidth() { 178 return mBitmap.getWidth(); 179 } 180 181 @Override getIntrinsicHeight()182 public int getIntrinsicHeight() { 183 return mBitmap.getHeight(); 184 } 185 186 @Override getMinimumWidth()187 public int getMinimumWidth() { 188 return getBounds().width(); 189 } 190 191 @Override getMinimumHeight()192 public int getMinimumHeight() { 193 return getBounds().height(); 194 } 195 196 @Override isStateful()197 public boolean isStateful() { 198 return true; 199 } 200 201 @Override getColorFilter()202 public ColorFilter getColorFilter() { 203 return mPaint.getColorFilter(); 204 } 205 206 @Override onStateChange(int[] state)207 protected boolean onStateChange(int[] state) { 208 boolean isPressed = false; 209 for (int s : state) { 210 if (s == android.R.attr.state_pressed) { 211 isPressed = true; 212 break; 213 } 214 } 215 if (mIsPressed != isPressed) { 216 mIsPressed = isPressed; 217 218 if (mScaleAnimation != null) { 219 mScaleAnimation.cancel(); 220 mScaleAnimation = null; 221 } 222 223 if (mIsPressed) { 224 // Animate when going to pressed state 225 mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE); 226 mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION); 227 mScaleAnimation.setInterpolator(ACCEL); 228 mScaleAnimation.start(); 229 } else { 230 if (isVisible()) { 231 mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, 1f); 232 mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION); 233 mScaleAnimation.setInterpolator(DEACCEL); 234 mScaleAnimation.start(); 235 } else { 236 mScale = 1f; 237 invalidateSelf(); 238 } 239 } 240 return true; 241 } 242 return false; 243 } 244 invalidateDesaturationAndBrightness()245 private void invalidateDesaturationAndBrightness() { 246 setDesaturation(mIsDisabled ? DISABLED_DESATURATION : 0); 247 setBrightness(mIsDisabled ? DISABLED_BRIGHTNESS : 0); 248 } 249 setIsDisabled(boolean isDisabled)250 public void setIsDisabled(boolean isDisabled) { 251 if (mIsDisabled != isDisabled) { 252 mIsDisabled = isDisabled; 253 invalidateDesaturationAndBrightness(); 254 } 255 } 256 isDisabled()257 protected boolean isDisabled() { 258 return mIsDisabled; 259 } 260 261 /** 262 * Sets the saturation of this icon, 0 [full color] -> 1 [desaturated] 263 */ setDesaturation(float desaturation)264 private void setDesaturation(float desaturation) { 265 int newDesaturation = (int) Math.floor(desaturation * REDUCED_FILTER_VALUE_SPACE); 266 if (mDesaturation != newDesaturation) { 267 mDesaturation = newDesaturation; 268 updateFilter(); 269 } 270 } 271 getDesaturation()272 public float getDesaturation() { 273 return (float) mDesaturation / REDUCED_FILTER_VALUE_SPACE; 274 } 275 276 /** 277 * Sets the brightness of this icon, 0 [no add. brightness] -> 1 [2bright2furious] 278 */ setBrightness(float brightness)279 private void setBrightness(float brightness) { 280 int newBrightness = (int) Math.floor(brightness * REDUCED_FILTER_VALUE_SPACE); 281 if (mBrightness != newBrightness) { 282 mBrightness = newBrightness; 283 updateFilter(); 284 } 285 } 286 getBrightness()287 private float getBrightness() { 288 return (float) mBrightness / REDUCED_FILTER_VALUE_SPACE; 289 } 290 291 /** 292 * Updates the paint to reflect the current brightness and saturation. 293 */ updateFilter()294 protected void updateFilter() { 295 boolean usePorterDuffFilter = false; 296 int key = -1; 297 if (mDesaturation > 0) { 298 key = (mDesaturation << 16) | mBrightness; 299 } else if (mBrightness > 0) { 300 // Compose a key with a fully saturated icon if we are just animating brightness 301 key = (1 << 16) | mBrightness; 302 303 // We found that in L, ColorFilters cause drawing artifacts with shadows baked into 304 // icons, so just use a PorterDuff filter when we aren't animating saturation 305 usePorterDuffFilter = true; 306 } 307 308 // Debounce multiple updates on the same frame 309 if (key == mPrevUpdateKey) { 310 return; 311 } 312 mPrevUpdateKey = key; 313 314 if (key != -1) { 315 ColorFilter filter = sCachedFilter.get(key); 316 if (filter == null) { 317 float brightnessF = getBrightness(); 318 int brightnessI = (int) (255 * brightnessF); 319 if (usePorterDuffFilter) { 320 filter = new PorterDuffColorFilter(Color.argb(brightnessI, 255, 255, 255), 321 PorterDuff.Mode.SRC_ATOP); 322 } else { 323 float saturationF = 1f - getDesaturation(); 324 sTempFilterMatrix.setSaturation(saturationF); 325 if (mBrightness > 0) { 326 // Brightness: C-new = C-old*(1-amount) + amount 327 float scale = 1f - brightnessF; 328 float[] mat = sTempBrightnessMatrix.getArray(); 329 mat[0] = scale; 330 mat[6] = scale; 331 mat[12] = scale; 332 mat[4] = brightnessI; 333 mat[9] = brightnessI; 334 mat[14] = brightnessI; 335 sTempFilterMatrix.preConcat(sTempBrightnessMatrix); 336 } 337 filter = new ColorMatrixColorFilter(sTempFilterMatrix); 338 } 339 sCachedFilter.append(key, filter); 340 } 341 mPaint.setColorFilter(filter); 342 } else { 343 mPaint.setColorFilter(null); 344 } 345 invalidateSelf(); 346 } 347 348 @Override getConstantState()349 public ConstantState getConstantState() { 350 return new MyConstantState(mBitmap, mIconColor, mIsDisabled); 351 } 352 353 protected static class MyConstantState extends ConstantState { 354 protected final Bitmap mBitmap; 355 protected final int mIconColor; 356 protected final boolean mIsDisabled; 357 MyConstantState(Bitmap bitmap, int color, boolean isDisabled)358 public MyConstantState(Bitmap bitmap, int color, boolean isDisabled) { 359 mBitmap = bitmap; 360 mIconColor = color; 361 mIsDisabled = isDisabled; 362 } 363 364 @Override newDrawable()365 public Drawable newDrawable() { 366 return new FastBitmapDrawable(mBitmap, mIconColor, mIsDisabled); 367 } 368 369 @Override getChangingConfigurations()370 public int getChangingConfigurations() { 371 return 0; 372 } 373 } 374 } 375