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