1 /* 2 * Copyright (C) 2017 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 18 package com.android.launcher3.graphics; 19 20 import static com.android.app.animation.Interpolators.EMPHASIZED; 21 import static com.android.app.animation.Interpolators.LINEAR; 22 import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2; 23 import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V3; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.animation.ObjectAnimator; 28 import android.content.Context; 29 import android.graphics.Bitmap; 30 import android.graphics.Canvas; 31 import android.graphics.Matrix; 32 import android.graphics.Paint; 33 import android.graphics.Path; 34 import android.graphics.PathMeasure; 35 import android.graphics.Rect; 36 import android.util.Property; 37 38 import androidx.core.graphics.ColorUtils; 39 40 import com.android.launcher3.R; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.anim.AnimatedFloat; 43 import com.android.launcher3.anim.AnimatorListeners; 44 import com.android.launcher3.icons.FastBitmapDrawable; 45 import com.android.launcher3.icons.GraphicsUtils; 46 import com.android.launcher3.model.data.ItemInfoWithIcon; 47 import com.android.launcher3.util.Themes; 48 49 /** 50 * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon. 51 */ 52 public class PreloadIconDrawable extends FastBitmapDrawable { 53 54 private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE = 55 new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") { 56 @Override 57 public Float get(PreloadIconDrawable object) { 58 return object.mInternalStateProgress; 59 } 60 61 @Override 62 public void set(PreloadIconDrawable object, Float value) { 63 object.setInternalProgress(value); 64 } 65 }; 66 67 private static final int DEFAULT_PATH_SIZE = 100; 68 private static final int MAX_PAINT_ALPHA = 255; 69 private static final int TRACK_ALPHA = (int) (0.27f * MAX_PAINT_ALPHA); 70 private static final int DISABLED_ICON_ALPHA = (int) (0.6f * MAX_PAINT_ALPHA); 71 72 private static final long DURATION_SCALE = 500; 73 private static final long SCALE_AND_ALPHA_ANIM_DURATION = 500; 74 75 // The smaller the number, the faster the animation would be. 76 // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE 77 private static final float COMPLETE_ANIM_FRACTION = 1f; 78 79 private static final float SMALL_SCALE = ENABLE_DOWNLOAD_APP_UX_V3.get() ? 0.8f : 0.7f; 80 private static final float PROGRESS_STROKE_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get() 81 ? 0.055f 82 : 0.075f; 83 private static final float PROGRESS_BOUNDS_SCALE = 0.075f; 84 private static final int PRELOAD_ACCENT_COLOR_INDEX = 0; 85 private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1; 86 87 private final Matrix mTmpMatrix = new Matrix(); 88 private final PathMeasure mPathMeasure = new PathMeasure(); 89 90 private final ItemInfoWithIcon mItem; 91 92 // Path in [0, 100] bounds. 93 private final Path mShapePath; 94 95 private final Path mScaledTrackPath; 96 private final Path mScaledProgressPath; 97 private final Paint mProgressPaint; 98 99 private final int mIndicatorColor; 100 private final int mSystemAccentColor; 101 private final int mSystemBackgroundColor; 102 103 private int mProgressColor; 104 private int mTrackColor; 105 private int mPlateColor; 106 107 private final boolean mIsDarkMode; 108 109 private float mTrackLength; 110 111 private boolean mRanFinishAnimation; 112 113 // Progress of the internal state. [0, 1] indicates the fraction of completed progress, 114 // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation. 115 private float mInternalStateProgress; 116 // This multiplier is used to animate scale when going from 0 to non-zero and expanding 117 private final Runnable mInvalidateRunnable = this::invalidateSelf; 118 private final AnimatedFloat mIconScaleMultiplier = new AnimatedFloat(mInvalidateRunnable); 119 120 private ObjectAnimator mCurrentAnim; 121 122 private boolean mIsStartable; 123 PreloadIconDrawable(ItemInfoWithIcon info, Context context)124 public PreloadIconDrawable(ItemInfoWithIcon info, Context context) { 125 this( 126 info, 127 IconPalette.getPreloadProgressColor(context, info.bitmap.color), 128 getPreloadColors(context), 129 Utilities.isDarkTheme(context), 130 GraphicsUtils.getShapePath(context, DEFAULT_PATH_SIZE)); 131 } 132 PreloadIconDrawable( ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode, Path shapePath)133 public PreloadIconDrawable( 134 ItemInfoWithIcon info, 135 int indicatorColor, 136 int[] preloadColors, 137 boolean isDarkMode, 138 Path shapePath) { 139 super(info.bitmap); 140 mItem = info; 141 mShapePath = shapePath; 142 mScaledTrackPath = new Path(); 143 mScaledProgressPath = new Path(); 144 145 mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 146 mProgressPaint.setStrokeCap(Paint.Cap.ROUND); 147 if (ENABLE_DOWNLOAD_APP_UX_V3.get()) { 148 mProgressPaint.setAlpha(MAX_PAINT_ALPHA); 149 } 150 mIndicatorColor = indicatorColor; 151 152 // This is the color 153 int primaryIconColor = mItem.bitmap.color; 154 155 // Progress color 156 float[] m3HCT = new float[3]; 157 ColorUtils.colorToM3HCT(primaryIconColor, m3HCT); 158 mProgressColor = ColorUtils.M3HCTToColor( 159 m3HCT[0], 160 m3HCT[1], 161 isDarkMode ? Math.max(m3HCT[2], 55) : Math.min(m3HCT[2], 40)); 162 163 // Track color 164 mTrackColor = ColorUtils.M3HCTToColor( 165 m3HCT[0], 166 16, 167 isDarkMode ? 30 : 90 168 ); 169 // Plate color 170 mPlateColor = ColorUtils.M3HCTToColor( 171 m3HCT[0], 172 isDarkMode ? 36 : 24, 173 isDarkMode ? (isThemed() ? 10 : 20) : 80 174 ); 175 176 mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX]; 177 mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX]; 178 mIsDarkMode = isDarkMode; 179 180 // If it's a pending app we will animate scale and alpha when it's no longer pending. 181 mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1); 182 183 setLevel(info.getProgressLevel()); 184 if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) { 185 setIsStartable(info.isAppStartable()); 186 } 187 } 188 189 @Override onBoundsChange(Rect bounds)190 protected void onBoundsChange(Rect bounds) { 191 super.onBoundsChange(bounds); 192 193 float progressWidth = bounds.width() * PROGRESS_BOUNDS_SCALE; 194 mTmpMatrix.setScale( 195 (bounds.width() - 2 * progressWidth) / DEFAULT_PATH_SIZE, 196 (bounds.height() - 2 * progressWidth) / DEFAULT_PATH_SIZE); 197 mTmpMatrix.postTranslate(bounds.left + progressWidth, bounds.top + progressWidth); 198 199 mShapePath.transform(mTmpMatrix, mScaledTrackPath); 200 mProgressPaint.setStrokeWidth(PROGRESS_STROKE_SCALE * bounds.width()); 201 202 mPathMeasure.setPath(mScaledTrackPath, true); 203 mTrackLength = mPathMeasure.getLength(); 204 205 setInternalProgress(mInternalStateProgress); 206 } 207 208 @Override drawInternal(Canvas canvas, Rect bounds)209 public void drawInternal(Canvas canvas, Rect bounds) { 210 if (mRanFinishAnimation) { 211 super.drawInternal(canvas, bounds); 212 return; 213 } 214 215 if (mInternalStateProgress > 0 216 && (ENABLE_DOWNLOAD_APP_UX_V3.get() || !ENABLE_DOWNLOAD_APP_UX_V2.get())) { 217 // Draw background. 218 mProgressPaint.setStyle(ENABLE_DOWNLOAD_APP_UX_V3.get() 219 ? Paint.Style.FILL 220 : Paint.Style.FILL_AND_STROKE); 221 mProgressPaint.setColor(ENABLE_DOWNLOAD_APP_UX_V3.get() 222 ? mPlateColor 223 : mSystemBackgroundColor); 224 canvas.drawPath(mScaledTrackPath, mProgressPaint); 225 } 226 227 if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) { 228 // Draw track and progress. 229 mProgressPaint.setStyle(Paint.Style.STROKE); 230 mProgressPaint.setColor(ENABLE_DOWNLOAD_APP_UX_V3.get() 231 ? mTrackColor 232 : mSystemAccentColor); 233 if (!ENABLE_DOWNLOAD_APP_UX_V3.get()) { 234 mProgressPaint.setAlpha(TRACK_ALPHA); 235 } 236 canvas.drawPath(mScaledTrackPath, mProgressPaint); 237 mProgressPaint.setAlpha(MAX_PAINT_ALPHA); 238 if (ENABLE_DOWNLOAD_APP_UX_V3.get()) { 239 mProgressPaint.setColor(mProgressColor); 240 } 241 canvas.drawPath(mScaledProgressPath, mProgressPaint); 242 } 243 244 int saveCount = canvas.save(); 245 float scale = ENABLE_DOWNLOAD_APP_UX_V2.get() 246 ? 1 - mIconScaleMultiplier.value * (1 - SMALL_SCALE) 247 : SMALL_SCALE; 248 canvas.scale(scale, scale, bounds.exactCenterX(), bounds.exactCenterY()); 249 250 super.drawInternal(canvas, bounds); 251 canvas.restoreToCount(saveCount); 252 } 253 254 @Override updateFilter()255 protected void updateFilter() { 256 if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) { 257 setAlpha(mIsDisabled ? DISABLED_ICON_ALPHA : MAX_PAINT_ALPHA); 258 } else { 259 super.updateFilter(); 260 } 261 } 262 263 /** 264 * Updates the install progress based on the level 265 */ 266 @Override onLevelChange(int level)267 protected boolean onLevelChange(int level) { 268 // Run the animation if we have already been bound. 269 updateInternalState(level * 0.01f, false, null); 270 return true; 271 } 272 273 /** 274 * Runs the finish animation if it is has not been run after last call to 275 * {@link #onLevelChange} 276 */ maybePerformFinishedAnimation( PreloadIconDrawable oldIcon, Runnable onFinishCallback)277 public void maybePerformFinishedAnimation( 278 PreloadIconDrawable oldIcon, Runnable onFinishCallback) { 279 280 mProgressColor = oldIcon.mProgressColor; 281 mTrackColor = oldIcon.mTrackColor; 282 mPlateColor = oldIcon.mPlateColor; 283 284 if (oldIcon.mInternalStateProgress >= 1) { 285 mInternalStateProgress = oldIcon.mInternalStateProgress; 286 } 287 288 // If the drawable was recently initialized, skip the progress animation. 289 if (mInternalStateProgress == 0) { 290 mInternalStateProgress = 1; 291 } 292 updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, onFinishCallback); 293 } 294 hasNotCompleted()295 public boolean hasNotCompleted() { 296 return !mRanFinishAnimation; 297 } 298 299 /** Sets whether this icon should display the startable app UI. */ setIsStartable(boolean isStartable)300 public void setIsStartable(boolean isStartable) { 301 if (mIsStartable != isStartable) { 302 mIsStartable = isStartable; 303 setIsDisabled(!isStartable); 304 } 305 } 306 updateInternalState( float finalProgress, boolean isFinish, Runnable onFinishCallback)307 private void updateInternalState( 308 float finalProgress, boolean isFinish, Runnable onFinishCallback) { 309 if (mCurrentAnim != null) { 310 mCurrentAnim.cancel(); 311 mCurrentAnim = null; 312 } 313 314 boolean animateProgress = 315 finalProgress >= mInternalStateProgress && getBounds().width() > 0; 316 if (!animateProgress || mRanFinishAnimation) { 317 setInternalProgress(finalProgress); 318 if (isFinish && onFinishCallback != null) { 319 onFinishCallback.run(); 320 } 321 } else { 322 mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress); 323 mCurrentAnim.setDuration( 324 (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE)); 325 mCurrentAnim.setInterpolator(LINEAR); 326 if (isFinish) { 327 if (onFinishCallback != null) { 328 mCurrentAnim.addListener(AnimatorListeners.forEndCallback(onFinishCallback)); 329 } 330 mCurrentAnim.addListener(new AnimatorListenerAdapter() { 331 @Override 332 public void onAnimationEnd(Animator animation) { 333 mRanFinishAnimation = true; 334 } 335 }); 336 } 337 mCurrentAnim.start(); 338 } 339 } 340 341 /** 342 * Sets the internal progress and updates the UI accordingly 343 * for progress <= 0: 344 * - icon is pending 345 * - progress track is not visible 346 * - progress bar is not visible 347 * for progress < 1: 348 * - icon without pending motion 349 * - progress track is visible 350 * - progress bar is visible. Progress bar is drawn as a fraction of 351 * {@link #mScaledTrackPath}. 352 * @see PathMeasure#getSegment(float, float, Path, boolean) 353 * for progress > 1: 354 * - scale the icon back to full size 355 */ setInternalProgress(float progress)356 private void setInternalProgress(float progress) { 357 // Animate scale and alpha from pending to downloading state. 358 if (ENABLE_DOWNLOAD_APP_UX_V2.get() && progress > 0 && mInternalStateProgress == 0) { 359 // Progress is changing for the first time, animate the icon scale 360 Animator iconScaleAnimator = mIconScaleMultiplier.animateToValue(1); 361 iconScaleAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION); 362 iconScaleAnimator.setInterpolator(EMPHASIZED); 363 iconScaleAnimator.start(); 364 } 365 366 mInternalStateProgress = progress; 367 if (progress <= 0) { 368 if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) { 369 mScaledTrackPath.reset(); 370 } 371 mIconScaleMultiplier.updateValue(0); 372 } else { 373 mPathMeasure.getSegment( 374 0, Math.min(progress, 1) * mTrackLength, mScaledProgressPath, true); 375 if (progress > 1 && ENABLE_DOWNLOAD_APP_UX_V2.get()) { 376 // map the scale back to original value 377 mIconScaleMultiplier.updateValue(Utilities.mapBoundToRange( 378 progress - 1, 0, COMPLETE_ANIM_FRACTION, 1, 0, EMPHASIZED)); 379 } 380 } 381 invalidateSelf(); 382 } 383 getPreloadColors(Context context)384 private static int[] getPreloadColors(Context context) { 385 int[] preloadColors = new int[2]; 386 387 preloadColors[PRELOAD_ACCENT_COLOR_INDEX] = Themes.getAttrColor(context, 388 R.attr.preloadIconAccentColor); 389 preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX] = Themes.getAttrColor(context, 390 R.attr.preloadIconBackgroundColor); 391 392 return preloadColors; 393 } 394 /** 395 * Returns a FastBitmapDrawable with the icon. 396 */ newPendingIcon(Context context, ItemInfoWithIcon info)397 public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) { 398 return new PreloadIconDrawable(info, context); 399 } 400 401 @Override newConstantState()402 public FastBitmapConstantState newConstantState() { 403 return new PreloadIconConstantState( 404 mBitmap, 405 mIconColor, 406 mItem, 407 mIndicatorColor, 408 new int[] {mSystemAccentColor, mSystemBackgroundColor}, 409 mIsDarkMode, 410 mShapePath); 411 } 412 413 protected static class PreloadIconConstantState extends FastBitmapConstantState { 414 415 protected final ItemInfoWithIcon mInfo; 416 protected final int mIndicatorColor; 417 protected final int[] mPreloadColors; 418 protected final boolean mIsDarkMode; 419 protected final int mLevel; 420 private final Path mShapePath; 421 PreloadIconConstantState( Bitmap bitmap, int iconColor, ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode, Path shapePath)422 public PreloadIconConstantState( 423 Bitmap bitmap, 424 int iconColor, 425 ItemInfoWithIcon info, 426 int indicatorColor, 427 int[] preloadColors, 428 boolean isDarkMode, 429 Path shapePath) { 430 super(bitmap, iconColor); 431 mInfo = info; 432 mIndicatorColor = indicatorColor; 433 mPreloadColors = preloadColors; 434 mIsDarkMode = isDarkMode; 435 mLevel = info.getProgressLevel(); 436 mShapePath = shapePath; 437 } 438 439 @Override createDrawable()440 public PreloadIconDrawable createDrawable() { 441 return new PreloadIconDrawable( 442 mInfo, 443 mIndicatorColor, 444 mPreloadColors, 445 mIsDarkMode, 446 mShapePath); 447 } 448 } 449 } 450