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.icons; 18 19 import static com.android.launcher3.icons.BaseIconFactory.getBadgeSizeForIconSize; 20 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound; 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.Rect; 32 import android.graphics.drawable.Drawable; 33 import android.util.FloatProperty; 34 import android.view.animation.AccelerateInterpolator; 35 import android.view.animation.DecelerateInterpolator; 36 import android.view.animation.Interpolator; 37 38 import androidx.annotation.Nullable; 39 import androidx.core.graphics.ColorUtils; 40 41 public class FastBitmapDrawable extends Drawable implements Drawable.Callback { 42 43 private static final Interpolator ACCEL = new AccelerateInterpolator(); 44 private static final Interpolator DEACCEL = new DecelerateInterpolator(); 45 46 private static final float PRESSED_SCALE = 1.1f; 47 public static final int WHITE_SCRIM_ALPHA = 138; 48 49 private static final float DISABLED_DESATURATION = 1f; 50 private static final float DISABLED_BRIGHTNESS = 0.5f; 51 protected static final int FULLY_OPAQUE = 255; 52 53 public static final int CLICK_FEEDBACK_DURATION = 200; 54 55 protected final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); 56 protected final Bitmap mBitmap; 57 protected final int mIconColor; 58 59 @Nullable private ColorFilter mColorFilter; 60 61 private boolean mIsPressed; 62 protected boolean mIsDisabled; 63 float mDisabledAlpha = 1f; 64 65 // Animator and properties for the fast bitmap drawable's scale 66 private static final FloatProperty<FastBitmapDrawable> SCALE 67 = new FloatProperty<FastBitmapDrawable>("scale") { 68 @Override 69 public Float get(FastBitmapDrawable fastBitmapDrawable) { 70 return fastBitmapDrawable.mScale; 71 } 72 73 @Override 74 public void setValue(FastBitmapDrawable fastBitmapDrawable, float value) { 75 fastBitmapDrawable.mScale = value; 76 fastBitmapDrawable.invalidateSelf(); 77 } 78 }; 79 private ObjectAnimator mScaleAnimation; 80 private float mScale = 1; 81 private int mAlpha = 255; 82 83 private Drawable mBadge; 84 FastBitmapDrawable(Bitmap b)85 public FastBitmapDrawable(Bitmap b) { 86 this(b, Color.TRANSPARENT); 87 } 88 FastBitmapDrawable(BitmapInfo info)89 public FastBitmapDrawable(BitmapInfo info) { 90 this(info.icon, info.color); 91 } 92 FastBitmapDrawable(Bitmap b, int iconColor)93 protected FastBitmapDrawable(Bitmap b, int iconColor) { 94 mBitmap = b; 95 mIconColor = iconColor; 96 setFilterBitmap(true); 97 } 98 99 @Override onBoundsChange(Rect bounds)100 protected void onBoundsChange(Rect bounds) { 101 super.onBoundsChange(bounds); 102 updateBadgeBounds(bounds); 103 } 104 updateBadgeBounds(Rect bounds)105 private void updateBadgeBounds(Rect bounds) { 106 if (mBadge != null) { 107 setBadgeBounds(mBadge, bounds); 108 } 109 } 110 111 @Override draw(Canvas canvas)112 public final void draw(Canvas canvas) { 113 if (mScale != 1f) { 114 int count = canvas.save(); 115 Rect bounds = getBounds(); 116 canvas.scale(mScale, mScale, bounds.exactCenterX(), bounds.exactCenterY()); 117 drawInternal(canvas, bounds); 118 if (mBadge != null) { 119 mBadge.draw(canvas); 120 } 121 canvas.restoreToCount(count); 122 } else { 123 drawInternal(canvas, getBounds()); 124 if (mBadge != null) { 125 mBadge.draw(canvas); 126 } 127 } 128 } 129 drawInternal(Canvas canvas, Rect bounds)130 protected void drawInternal(Canvas canvas, Rect bounds) { 131 canvas.drawBitmap(mBitmap, null, bounds, mPaint); 132 } 133 134 /** 135 * Returns the primary icon color, slightly tinted white 136 */ getIconColor()137 public int getIconColor() { 138 int whiteScrim = setColorAlphaBound(Color.WHITE, WHITE_SCRIM_ALPHA); 139 return ColorUtils.compositeColors(whiteScrim, mIconColor); 140 } 141 142 /** 143 * Returns if this represents a themed icon 144 */ isThemed()145 public boolean isThemed() { 146 return false; 147 } 148 149 @Override setColorFilter(ColorFilter cf)150 public void setColorFilter(ColorFilter cf) { 151 mColorFilter = cf; 152 updateFilter(); 153 } 154 155 @Override getOpacity()156 public int getOpacity() { 157 return PixelFormat.TRANSLUCENT; 158 } 159 160 @Override setAlpha(int alpha)161 public void setAlpha(int alpha) { 162 if (mAlpha != alpha) { 163 mAlpha = alpha; 164 mPaint.setAlpha(alpha); 165 invalidateSelf(); 166 } 167 } 168 169 @Override setFilterBitmap(boolean filterBitmap)170 public void setFilterBitmap(boolean filterBitmap) { 171 mPaint.setFilterBitmap(filterBitmap); 172 mPaint.setAntiAlias(filterBitmap); 173 } 174 175 @Override getAlpha()176 public int getAlpha() { 177 return mAlpha; 178 } 179 resetScale()180 public void resetScale() { 181 if (mScaleAnimation != null) { 182 mScaleAnimation.cancel(); 183 mScaleAnimation = null; 184 } 185 mScale = 1; 186 invalidateSelf(); 187 } 188 getAnimatedScale()189 public float getAnimatedScale() { 190 return mScaleAnimation == null ? 1 : mScale; 191 } 192 193 @Override getIntrinsicWidth()194 public int getIntrinsicWidth() { 195 return mBitmap.getWidth(); 196 } 197 198 @Override getIntrinsicHeight()199 public int getIntrinsicHeight() { 200 return mBitmap.getHeight(); 201 } 202 203 @Override getMinimumWidth()204 public int getMinimumWidth() { 205 return getBounds().width(); 206 } 207 208 @Override getMinimumHeight()209 public int getMinimumHeight() { 210 return getBounds().height(); 211 } 212 213 @Override isStateful()214 public boolean isStateful() { 215 return true; 216 } 217 218 @Override getColorFilter()219 public ColorFilter getColorFilter() { 220 return mPaint.getColorFilter(); 221 } 222 223 @Override onStateChange(int[] state)224 protected boolean onStateChange(int[] state) { 225 boolean isPressed = false; 226 for (int s : state) { 227 if (s == android.R.attr.state_pressed) { 228 isPressed = true; 229 break; 230 } 231 } 232 if (mIsPressed != isPressed) { 233 mIsPressed = isPressed; 234 235 if (mScaleAnimation != null) { 236 mScaleAnimation.cancel(); 237 mScaleAnimation = null; 238 } 239 240 if (mIsPressed) { 241 // Animate when going to pressed state 242 mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, PRESSED_SCALE); 243 mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION); 244 mScaleAnimation.setInterpolator(ACCEL); 245 mScaleAnimation.start(); 246 } else { 247 if (isVisible()) { 248 mScaleAnimation = ObjectAnimator.ofFloat(this, SCALE, 1f); 249 mScaleAnimation.setDuration(CLICK_FEEDBACK_DURATION); 250 mScaleAnimation.setInterpolator(DEACCEL); 251 mScaleAnimation.start(); 252 } else { 253 mScale = 1f; 254 invalidateSelf(); 255 } 256 } 257 return true; 258 } 259 return false; 260 } 261 setIsDisabled(boolean isDisabled)262 public void setIsDisabled(boolean isDisabled) { 263 if (mIsDisabled != isDisabled) { 264 mIsDisabled = isDisabled; 265 updateFilter(); 266 } 267 } 268 isDisabled()269 protected boolean isDisabled() { 270 return mIsDisabled; 271 } 272 setBadge(Drawable badge)273 public void setBadge(Drawable badge) { 274 if (mBadge != null) { 275 mBadge.setCallback(null); 276 } 277 mBadge = badge; 278 if (mBadge != null) { 279 mBadge.setCallback(this); 280 } 281 updateBadgeBounds(getBounds()); 282 updateFilter(); 283 } 284 285 /** 286 * Updates the paint to reflect the current brightness and saturation. 287 */ updateFilter()288 protected void updateFilter() { 289 mPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter(mDisabledAlpha) : mColorFilter); 290 if (mBadge != null) { 291 mBadge.setColorFilter(getColorFilter()); 292 } 293 invalidateSelf(); 294 } 295 newConstantState()296 protected FastBitmapConstantState newConstantState() { 297 return new FastBitmapConstantState(mBitmap, mIconColor); 298 } 299 300 @Override getConstantState()301 public final ConstantState getConstantState() { 302 FastBitmapConstantState cs = newConstantState(); 303 cs.mIsDisabled = mIsDisabled; 304 if (mBadge != null) { 305 cs.mBadgeConstantState = mBadge.getConstantState(); 306 } 307 return cs; 308 } 309 getDisabledColorFilter()310 public static ColorFilter getDisabledColorFilter() { 311 return getDisabledColorFilter(1); 312 } 313 getDisabledColorFilter(float disabledAlpha)314 private static ColorFilter getDisabledColorFilter(float disabledAlpha) { 315 ColorMatrix tempBrightnessMatrix = new ColorMatrix(); 316 ColorMatrix tempFilterMatrix = new ColorMatrix(); 317 318 tempFilterMatrix.setSaturation(1f - DISABLED_DESATURATION); 319 float scale = 1 - DISABLED_BRIGHTNESS; 320 int brightnessI = (int) (255 * DISABLED_BRIGHTNESS); 321 float[] mat = tempBrightnessMatrix.getArray(); 322 mat[0] = scale; 323 mat[6] = scale; 324 mat[12] = scale; 325 mat[4] = brightnessI; 326 mat[9] = brightnessI; 327 mat[14] = brightnessI; 328 mat[18] = disabledAlpha; 329 tempFilterMatrix.preConcat(tempBrightnessMatrix); 330 return new ColorMatrixColorFilter(tempFilterMatrix); 331 } 332 getDisabledColor(int color)333 protected static final int getDisabledColor(int color) { 334 int component = (Color.red(color) + Color.green(color) + Color.blue(color)) / 3; 335 float scale = 1 - DISABLED_BRIGHTNESS; 336 int brightnessI = (int) (255 * DISABLED_BRIGHTNESS); 337 component = Math.min(Math.round(scale * component + brightnessI), FULLY_OPAQUE); 338 return Color.rgb(component, component, component); 339 } 340 341 /** 342 * Sets the bounds for the badge drawable based on the main icon bounds 343 */ setBadgeBounds(Drawable badge, Rect iconBounds)344 public static void setBadgeBounds(Drawable badge, Rect iconBounds) { 345 int size = getBadgeSizeForIconSize(iconBounds.width()); 346 badge.setBounds(iconBounds.right - size, iconBounds.bottom - size, 347 iconBounds.right, iconBounds.bottom); 348 } 349 350 @Override invalidateDrawable(Drawable who)351 public void invalidateDrawable(Drawable who) { 352 if (who == mBadge) { 353 invalidateSelf(); 354 } 355 } 356 357 @Override scheduleDrawable(Drawable who, Runnable what, long when)358 public void scheduleDrawable(Drawable who, Runnable what, long when) { 359 if (who == mBadge) { 360 scheduleSelf(what, when); 361 } 362 } 363 364 @Override unscheduleDrawable(Drawable who, Runnable what)365 public void unscheduleDrawable(Drawable who, Runnable what) { 366 unscheduleSelf(what); 367 } 368 369 protected static class FastBitmapConstantState extends ConstantState { 370 protected final Bitmap mBitmap; 371 protected final int mIconColor; 372 373 // These are initialized later so that subclasses don't need to 374 // pass everything in constructor 375 protected boolean mIsDisabled; 376 private ConstantState mBadgeConstantState; 377 FastBitmapConstantState(Bitmap bitmap, int color)378 public FastBitmapConstantState(Bitmap bitmap, int color) { 379 mBitmap = bitmap; 380 mIconColor = color; 381 } 382 createDrawable()383 protected FastBitmapDrawable createDrawable() { 384 return new FastBitmapDrawable(mBitmap, mIconColor); 385 } 386 387 @Override newDrawable()388 public final FastBitmapDrawable newDrawable() { 389 FastBitmapDrawable drawable = createDrawable(); 390 drawable.setIsDisabled(mIsDisabled); 391 if (mBadgeConstantState != null) { 392 drawable.setBadge(mBadgeConstantState.newDrawable()); 393 } 394 return drawable; 395 } 396 397 @Override getChangingConfigurations()398 public int getChangingConfigurations() { 399 return 0; 400 } 401 } 402 } 403