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