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.graphics.IconShape.DEFAULT_PATH_SIZE; 21 import static com.android.launcher3.graphics.IconShape.getShapePath; 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.Pair; 35 import android.util.Property; 36 import android.util.SparseArray; 37 38 import com.android.launcher3.FastBitmapDrawable; 39 import com.android.launcher3.anim.Interpolators; 40 import com.android.launcher3.model.data.ItemInfoWithIcon; 41 42 import java.lang.ref.WeakReference; 43 44 /** 45 * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon. 46 */ 47 public class PreloadIconDrawable extends FastBitmapDrawable { 48 49 private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE = 50 new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") { 51 @Override 52 public Float get(PreloadIconDrawable object) { 53 return object.mInternalStateProgress; 54 } 55 56 @Override 57 public void set(PreloadIconDrawable object, Float value) { 58 object.setInternalProgress(value); 59 } 60 }; 61 62 private static final float PROGRESS_WIDTH = 7; 63 private static final float PROGRESS_GAP = 2; 64 private static final int MAX_PAINT_ALPHA = 255; 65 66 private static final long DURATION_SCALE = 500; 67 68 // The smaller the number, the faster the animation would be. 69 // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE 70 private static final float COMPLETE_ANIM_FRACTION = 0.3f; 71 72 private static final int COLOR_TRACK = 0x77EEEEEE; 73 private static final int COLOR_SHADOW = 0x55000000; 74 75 private static final float SMALL_SCALE = 0.6f; 76 77 private static final SparseArray<WeakReference<Pair<Path, Bitmap>>> sShadowCache = 78 new SparseArray<>(); 79 80 private final Matrix mTmpMatrix = new Matrix(); 81 private final PathMeasure mPathMeasure = new PathMeasure(); 82 83 private final ItemInfoWithIcon mItem; 84 85 // Path in [0, 100] bounds. 86 private final Path mShapePath; 87 88 private final Path mScaledTrackPath; 89 private final Path mScaledProgressPath; 90 private final Paint mProgressPaint; 91 92 private Bitmap mShadowBitmap; 93 private final int mIndicatorColor; 94 95 private int mTrackAlpha; 96 private float mTrackLength; 97 private float mIconScale; 98 99 private boolean mRanFinishAnimation; 100 101 // Progress of the internal state. [0, 1] indicates the fraction of completed progress, 102 // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation. 103 private float mInternalStateProgress; 104 105 private ObjectAnimator mCurrentAnim; 106 PreloadIconDrawable(ItemInfoWithIcon info, Context context)107 public PreloadIconDrawable(ItemInfoWithIcon info, Context context) { 108 super(info.bitmap); 109 mItem = info; 110 mShapePath = getShapePath(); 111 mScaledTrackPath = new Path(); 112 mScaledProgressPath = new Path(); 113 114 mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 115 mProgressPaint.setStyle(Paint.Style.STROKE); 116 mProgressPaint.setStrokeCap(Paint.Cap.ROUND); 117 mIndicatorColor = IconPalette.getPreloadProgressColor(context, mIconColor); 118 119 setInternalProgress(0); 120 } 121 122 @Override onBoundsChange(Rect bounds)123 protected void onBoundsChange(Rect bounds) { 124 super.onBoundsChange(bounds); 125 mTmpMatrix.setScale( 126 (bounds.width() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE, 127 (bounds.height() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / DEFAULT_PATH_SIZE); 128 mTmpMatrix.postTranslate( 129 bounds.left + PROGRESS_WIDTH + PROGRESS_GAP, 130 bounds.top + PROGRESS_WIDTH + PROGRESS_GAP); 131 132 mShapePath.transform(mTmpMatrix, mScaledTrackPath); 133 float scale = bounds.width() / DEFAULT_PATH_SIZE; 134 mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale); 135 136 mShadowBitmap = getShadowBitmap(bounds.width(), bounds.height(), 137 (PROGRESS_GAP ) * scale); 138 mPathMeasure.setPath(mScaledTrackPath, true); 139 mTrackLength = mPathMeasure.getLength(); 140 141 setInternalProgress(mInternalStateProgress); 142 } 143 getShadowBitmap(int width, int height, float shadowRadius)144 private Bitmap getShadowBitmap(int width, int height, float shadowRadius) { 145 int key = (width << 16) | height; 146 WeakReference<Pair<Path, Bitmap>> shadowRef = sShadowCache.get(key); 147 Pair<Path, Bitmap> cache = shadowRef != null ? shadowRef.get() : null; 148 Bitmap shadow = cache != null && cache.first.equals(mShapePath) ? cache.second : null; 149 if (shadow != null) { 150 return shadow; 151 } 152 shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 153 Canvas c = new Canvas(shadow); 154 mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW); 155 mProgressPaint.setColor(COLOR_TRACK); 156 mProgressPaint.setAlpha(MAX_PAINT_ALPHA); 157 c.drawPath(mScaledTrackPath, mProgressPaint); 158 mProgressPaint.clearShadowLayer(); 159 c.setBitmap(null); 160 161 sShadowCache.put(key, new WeakReference<>(Pair.create(mShapePath, shadow))); 162 return shadow; 163 } 164 165 @Override drawInternal(Canvas canvas, Rect bounds)166 public void drawInternal(Canvas canvas, Rect bounds) { 167 if (mRanFinishAnimation) { 168 super.drawInternal(canvas, bounds); 169 return; 170 } 171 172 // Draw track. 173 mProgressPaint.setColor(mIndicatorColor); 174 mProgressPaint.setAlpha(mTrackAlpha); 175 if (mShadowBitmap != null) { 176 canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint); 177 } 178 canvas.drawPath(mScaledProgressPath, mProgressPaint); 179 180 int saveCount = canvas.save(); 181 canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY()); 182 super.drawInternal(canvas, bounds); 183 canvas.restoreToCount(saveCount); 184 } 185 186 /** 187 * Updates the install progress based on the level 188 */ 189 @Override onLevelChange(int level)190 protected boolean onLevelChange(int level) { 191 // Run the animation if we have already been bound. 192 updateInternalState(level * 0.01f, getBounds().width() > 0, false); 193 return true; 194 } 195 196 /** 197 * Runs the finish animation if it is has not been run after last call to 198 * {@link #onLevelChange} 199 */ maybePerformFinishedAnimation()200 public void maybePerformFinishedAnimation() { 201 // If the drawable was recently initialized, skip the progress animation. 202 if (mInternalStateProgress == 0) { 203 mInternalStateProgress = 1; 204 } 205 updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true); 206 } 207 hasNotCompleted()208 public boolean hasNotCompleted() { 209 return !mRanFinishAnimation; 210 } 211 updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish)212 private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) { 213 if (mCurrentAnim != null) { 214 mCurrentAnim.cancel(); 215 mCurrentAnim = null; 216 } 217 218 if (Float.compare(finalProgress, mInternalStateProgress) == 0) { 219 return; 220 } 221 if (finalProgress < mInternalStateProgress) { 222 shouldAnimate = false; 223 } 224 if (!shouldAnimate || mRanFinishAnimation) { 225 setInternalProgress(finalProgress); 226 } else { 227 mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress); 228 mCurrentAnim.setDuration( 229 (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE)); 230 mCurrentAnim.setInterpolator(Interpolators.LINEAR); 231 if (isFinish) { 232 mCurrentAnim.addListener(new AnimatorListenerAdapter() { 233 @Override 234 public void onAnimationEnd(Animator animation) { 235 mRanFinishAnimation = true; 236 } 237 }); 238 } 239 mCurrentAnim.start(); 240 } 241 } 242 243 /** 244 * Sets the internal progress and updates the UI accordingly 245 * for progress <= 0: 246 * - icon in the small scale and disabled state 247 * - progress track is visible 248 * - progress bar is not visible 249 * for 0 < progress < 1 250 * - icon in the small scale and disabled state 251 * - progress track is visible 252 * - progress bar is visible with dominant color. Progress bar is drawn as a fraction of 253 * {@link #mScaledTrackPath}. 254 * @see PathMeasure#getSegment(float, float, Path, boolean) 255 * for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION) 256 * - we calculate fraction of progress in the above range 257 * - progress track is drawn with alpha based on fraction 258 * - progress bar is drawn at 100% with alpha based on fraction 259 * - icon is scaled up based on fraction and is drawn in enabled state 260 * for progress >= (1 + COMPLETE_ANIM_FRACTION) 261 * - only icon is drawn in normal state 262 */ setInternalProgress(float progress)263 private void setInternalProgress(float progress) { 264 mInternalStateProgress = progress; 265 if (progress <= 0) { 266 mIconScale = SMALL_SCALE; 267 mScaledTrackPath.reset(); 268 mTrackAlpha = MAX_PAINT_ALPHA; 269 setIsDisabled(true); 270 } 271 272 if (progress < 1 && progress > 0) { 273 mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true); 274 mIconScale = SMALL_SCALE; 275 mTrackAlpha = MAX_PAINT_ALPHA; 276 setIsDisabled(true); 277 } else if (progress >= 1) { 278 setIsDisabled(mItem.isDisabled()); 279 mScaledTrackPath.set(mScaledProgressPath); 280 float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION; 281 282 if (fraction >= 1) { 283 // Animation has completed 284 mIconScale = 1; 285 mTrackAlpha = 0; 286 } else { 287 mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA); 288 mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction; 289 } 290 } 291 invalidateSelf(); 292 } 293 294 /** 295 * Returns a FastBitmapDrawable with the icon. 296 */ newPendingIcon(Context context, ItemInfoWithIcon info)297 public static PreloadIconDrawable newPendingIcon(Context context, ItemInfoWithIcon info) { 298 return new PreloadIconDrawable(info, context); 299 } 300 } 301