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