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