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