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