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 android.animation.Animator; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.ObjectAnimator; 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.Matrix; 27 import android.graphics.Paint; 28 import android.graphics.Path; 29 import android.graphics.PathMeasure; 30 import android.graphics.Rect; 31 import android.util.Pair; 32 import android.util.Property; 33 import android.util.SparseArray; 34 import android.view.ContextThemeWrapper; 35 36 import com.android.launcher3.Utilities; 37 import com.android.launcher3.anim.Interpolators; 38 import com.android.launcher3.icons.FastBitmapDrawable; 39 import com.android.launcher3.icons.GraphicsUtils; 40 import com.android.launcher3.model.data.ItemInfoWithIcon; 41 import com.android.launcher3.util.Themes; 42 43 import java.lang.ref.WeakReference; 44 45 /** 46 * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon. 47 */ 48 public class PreloadIconDrawable extends FastBitmapDrawable { 49 50 private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE = 51 new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") { 52 @Override 53 public Float get(PreloadIconDrawable object) { 54 return object.mInternalStateProgress; 55 } 56 57 @Override 58 public void set(PreloadIconDrawable object, Float value) { 59 object.setInternalProgress(value); 60 } 61 }; 62 63 private static final int DEFAULT_PATH_SIZE = 100; 64 private static final float PROGRESS_WIDTH = 7; 65 private static final float PROGRESS_GAP = 2; 66 private static final int MAX_PAINT_ALPHA = 255; 67 68 private static final long DURATION_SCALE = 500; 69 70 // The smaller the number, the faster the animation would be. 71 // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE 72 private static final float COMPLETE_ANIM_FRACTION = 0.3f; 73 74 private static final int COLOR_TRACK = 0x77EEEEEE; 75 private static final int COLOR_SHADOW = 0x55000000; 76 77 private static final float SMALL_SCALE = 0.6f; 78 79 private static final SparseArray<WeakReference<Pair<Path, Bitmap>>> sShadowCache = 80 new SparseArray<>(); 81 82 private static final int PRELOAD_ACCENT_COLOR_INDEX = 0; 83 private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1; 84 85 private final Matrix mTmpMatrix = new Matrix(); 86 private final PathMeasure mPathMeasure = new PathMeasure(); 87 88 private final ItemInfoWithIcon mItem; 89 90 // Path in [0, 100] bounds. 91 private final Path mShapePath; 92 93 private final Path mScaledTrackPath; 94 private final Path mScaledProgressPath; 95 private final Paint mProgressPaint; 96 97 private Bitmap mShadowBitmap; 98 private final int mIndicatorColor; 99 private final int mSystemAccentColor; 100 private final int mSystemBackgroundColor; 101 private final boolean mIsDarkMode; 102 103 private int mTrackAlpha; 104 private float mTrackLength; 105 private float mIconScale; 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 113 private ObjectAnimator mCurrentAnim; 114 115 private boolean mIsStartable; 116 PreloadIconDrawable(ItemInfoWithIcon info, Context context)117 public PreloadIconDrawable(ItemInfoWithIcon info, Context context) { 118 this( 119 info, 120 IconPalette.getPreloadProgressColor(context, info.bitmap.color), 121 getPreloadColors(context), 122 Utilities.isDarkTheme(context)); 123 } 124 PreloadIconDrawable( ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode)125 public PreloadIconDrawable( 126 ItemInfoWithIcon info, 127 int indicatorColor, 128 int[] preloadColors, 129 boolean isDarkMode) { 130 super(info.bitmap); 131 mItem = info; 132 mShapePath = GraphicsUtils.getShapePath(DEFAULT_PATH_SIZE); 133 mScaledTrackPath = new Path(); 134 mScaledProgressPath = new Path(); 135 136 mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 137 mProgressPaint.setStyle(Paint.Style.STROKE); 138 mProgressPaint.setStrokeCap(Paint.Cap.ROUND); 139 mIndicatorColor = indicatorColor; 140 141 mSystemAccentColor = preloadColors[PRELOAD_ACCENT_COLOR_INDEX]; 142 mSystemBackgroundColor = preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX]; 143 mIsDarkMode = isDarkMode; 144 145 setInternalProgress(info.getProgressLevel()); 146 setIsStartable(info.isAppStartable()); 147 } 148 149 @Override onBoundsChange(Rect bounds)150 protected void onBoundsChange(Rect bounds) { 151 super.onBoundsChange(bounds); 152 mTmpMatrix.setScale( 153 (bounds.width() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE, 154 (bounds.height() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE); 155 mTmpMatrix.postTranslate( 156 bounds.left + PROGRESS_WIDTH + PROGRESS_GAP, 157 bounds.top + PROGRESS_WIDTH + PROGRESS_GAP); 158 159 mShapePath.transform(mTmpMatrix, mScaledTrackPath); 160 float scale = bounds.width() / DEFAULT_PATH_SIZE; 161 mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale); 162 163 mShadowBitmap = getShadowBitmap(bounds.width(), bounds.height(), 164 (PROGRESS_GAP ) * scale); 165 mPathMeasure.setPath(mScaledTrackPath, true); 166 mTrackLength = mPathMeasure.getLength(); 167 168 setInternalProgress(mInternalStateProgress); 169 } 170 getShadowBitmap(int width, int height, float shadowRadius)171 private Bitmap getShadowBitmap(int width, int height, float shadowRadius) { 172 int key = ((width << 16) | height) * (mIsDarkMode ? -1 : 1); 173 WeakReference<Pair<Path, Bitmap>> shadowRef = sShadowCache.get(key); 174 Pair<Path, Bitmap> cache = shadowRef != null ? shadowRef.get() : null; 175 Bitmap shadow = cache != null && cache.first.equals(mShapePath) ? cache.second : null; 176 if (shadow != null) { 177 return shadow; 178 } 179 shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 180 Canvas c = new Canvas(shadow); 181 mProgressPaint.setShadowLayer(shadowRadius, 0, 0, mIsStartable 182 ? COLOR_SHADOW : mSystemAccentColor); 183 mProgressPaint.setColor(mIsStartable ? COLOR_TRACK : mSystemBackgroundColor); 184 mProgressPaint.setAlpha(MAX_PAINT_ALPHA); 185 c.drawPath(mScaledTrackPath, mProgressPaint); 186 mProgressPaint.clearShadowLayer(); 187 c.setBitmap(null); 188 189 sShadowCache.put(key, new WeakReference<>(Pair.create(mShapePath, shadow))); 190 return shadow; 191 } 192 193 @Override drawInternal(Canvas canvas, Rect bounds)194 public void drawInternal(Canvas canvas, Rect bounds) { 195 if (mRanFinishAnimation) { 196 super.drawInternal(canvas, bounds); 197 return; 198 } 199 200 // Draw track. 201 mProgressPaint.setColor(mIsStartable ? mIndicatorColor : mSystemAccentColor); 202 mProgressPaint.setAlpha(mTrackAlpha); 203 if (mShadowBitmap != null) { 204 canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint); 205 } 206 canvas.drawPath(mScaledProgressPath, mProgressPaint); 207 208 int saveCount = canvas.save(); 209 canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY()); 210 super.drawInternal(canvas, bounds); 211 canvas.restoreToCount(saveCount); 212 } 213 214 /** 215 * Updates the install progress based on the level 216 */ 217 @Override onLevelChange(int level)218 protected boolean onLevelChange(int level) { 219 // Run the animation if we have already been bound. 220 updateInternalState(level * 0.01f, getBounds().width() > 0, false); 221 return true; 222 } 223 224 /** 225 * Runs the finish animation if it is has not been run after last call to 226 * {@link #onLevelChange} 227 */ maybePerformFinishedAnimation()228 public void maybePerformFinishedAnimation() { 229 // If the drawable was recently initialized, skip the progress animation. 230 if (mInternalStateProgress == 0) { 231 mInternalStateProgress = 1; 232 } 233 updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true); 234 } 235 hasNotCompleted()236 public boolean hasNotCompleted() { 237 return !mRanFinishAnimation; 238 } 239 240 /** Sets whether this icon should display the startable app UI. */ setIsStartable(boolean isStartable)241 public void setIsStartable(boolean isStartable) { 242 if (mIsStartable != isStartable) { 243 mIsStartable = isStartable; 244 setIsDisabled(!isStartable); 245 } 246 } 247 updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish)248 private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) { 249 if (mCurrentAnim != null) { 250 mCurrentAnim.cancel(); 251 mCurrentAnim = null; 252 } 253 254 if (Float.compare(finalProgress, mInternalStateProgress) == 0) { 255 return; 256 } 257 if (finalProgress < mInternalStateProgress) { 258 shouldAnimate = false; 259 } 260 if (!shouldAnimate || mRanFinishAnimation) { 261 setInternalProgress(finalProgress); 262 } else { 263 mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress); 264 mCurrentAnim.setDuration( 265 (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE)); 266 mCurrentAnim.setInterpolator(Interpolators.LINEAR); 267 if (isFinish) { 268 mCurrentAnim.addListener(new AnimatorListenerAdapter() { 269 @Override 270 public void onAnimationEnd(Animator animation) { 271 mRanFinishAnimation = true; 272 } 273 }); 274 } 275 mCurrentAnim.start(); 276 } 277 } 278 279 /** 280 * Sets the internal progress and updates the UI accordingly 281 * for progress <= 0: 282 * - icon in the small scale and disabled state 283 * - progress track is visible 284 * - progress bar is not visible 285 * for 0 < progress < 1 286 * - icon in the small scale and disabled state 287 * - progress track is visible 288 * - progress bar is visible with dominant color. Progress bar is drawn as a fraction of 289 * {@link #mScaledTrackPath}. 290 * @see PathMeasure#getSegment(float, float, Path, boolean) 291 * for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION) 292 * - we calculate fraction of progress in the above range 293 * - progress track is drawn with alpha based on fraction 294 * - progress bar is drawn at 100% with alpha based on fraction 295 * - icon is scaled up based on fraction and is drawn in enabled state 296 * for progress >= (1 + COMPLETE_ANIM_FRACTION) 297 * - only icon is drawn in normal state 298 */ setInternalProgress(float progress)299 private void setInternalProgress(float progress) { 300 mInternalStateProgress = progress; 301 if (progress <= 0) { 302 mIconScale = SMALL_SCALE; 303 mScaledTrackPath.reset(); 304 mTrackAlpha = MAX_PAINT_ALPHA; 305 } 306 307 if (progress < 1 && progress > 0) { 308 mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true); 309 mIconScale = SMALL_SCALE; 310 mTrackAlpha = MAX_PAINT_ALPHA; 311 } else if (progress >= 1) { 312 setIsDisabled(mItem.isDisabled()); 313 mScaledTrackPath.set(mScaledProgressPath); 314 float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION; 315 316 if (fraction >= 1) { 317 // Animation has completed 318 mIconScale = 1; 319 mTrackAlpha = 0; 320 } else { 321 mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA); 322 mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction; 323 } 324 } 325 invalidateSelf(); 326 } 327 getPreloadColors(Context context)328 private static int[] getPreloadColors(Context context) { 329 Context dayNightThemeContext = new ContextThemeWrapper( 330 context, android.R.style.Theme_DeviceDefault_DayNight); 331 int[] preloadColors = new int[2]; 332 333 preloadColors[PRELOAD_ACCENT_COLOR_INDEX] = Themes.getColorAccent(dayNightThemeContext); 334 preloadColors[PRELOAD_BACKGROUND_COLOR_INDEX] = Themes.getColorBackgroundFloating( 335 dayNightThemeContext); 336 337 return preloadColors; 338 } 339 340 /** 341 * Returns a FastBitmapDrawable with the icon. 342 */ newPendingIcon(Context context, ItemInfoWithIcon info)343 public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) { 344 return new PreloadIconDrawable(info, context); 345 } 346 347 @Override getConstantState()348 public ConstantState getConstantState() { 349 return new PreloadIconConstantState( 350 mBitmap, 351 mIconColor, 352 !mItem.isAppStartable(), 353 mItem, 354 mIndicatorColor, 355 new int[] {mSystemAccentColor, mSystemBackgroundColor}, 356 mIsDarkMode); 357 } 358 359 protected static class PreloadIconConstantState extends FastBitmapConstantState { 360 361 protected final ItemInfoWithIcon mInfo; 362 protected final int mIndicatorColor; 363 protected final int[] mPreloadColors; 364 protected final boolean mIsDarkMode; 365 protected final int mLevel; 366 PreloadIconConstantState( Bitmap bitmap, int iconColor, boolean isDisabled, ItemInfoWithIcon info, int indicatorColor, int[] preloadColors, boolean isDarkMode)367 public PreloadIconConstantState( 368 Bitmap bitmap, 369 int iconColor, 370 boolean isDisabled, 371 ItemInfoWithIcon info, 372 int indicatorColor, 373 int[] preloadColors, 374 boolean isDarkMode) { 375 super(bitmap, iconColor, isDisabled); 376 mInfo = info; 377 mIndicatorColor = indicatorColor; 378 mPreloadColors = preloadColors; 379 mIsDarkMode = isDarkMode; 380 mLevel = info.getProgressLevel(); 381 } 382 383 @Override newDrawable()384 public PreloadIconDrawable newDrawable() { 385 return new PreloadIconDrawable( 386 mInfo, 387 mIndicatorColor, 388 mPreloadColors, 389 mIsDarkMode); 390 } 391 392 @Override getChangingConfigurations()393 public int getChangingConfigurations() { 394 return 0; 395 } 396 } 397 } 398