1 package com.android.launcher3; 2 3 import android.animation.ObjectAnimator; 4 import android.content.res.Resources.Theme; 5 import android.content.res.TypedArray; 6 import android.graphics.Canvas; 7 import android.graphics.Color; 8 import android.graphics.ColorFilter; 9 import android.graphics.Paint; 10 import android.graphics.PixelFormat; 11 import android.graphics.Rect; 12 import android.graphics.RectF; 13 import android.graphics.drawable.Drawable; 14 15 class PreloadIconDrawable extends Drawable { 16 17 private static final float ANIMATION_PROGRESS_STOPPED = -1.0f; 18 private static final float ANIMATION_PROGRESS_STARTED = 0f; 19 private static final float ANIMATION_PROGRESS_COMPLETED = 1.0f; 20 21 private static final float MIN_SATUNATION = 0.2f; 22 private static final float MIN_LIGHTNESS = 0.6f; 23 24 private static final float ICON_SCALE_FACTOR = 0.5f; 25 private static final int DEFAULT_COLOR = 0xFF009688; 26 27 private static final Rect sTempRect = new Rect(); 28 29 private final RectF mIndicatorRect = new RectF(); 30 private boolean mIndicatorRectDirty; 31 32 private final Paint mPaint; 33 final Drawable mIcon; 34 35 private Drawable mBgDrawable; 36 private int mRingOutset; 37 38 private int mIndicatorColor = 0; 39 40 /** 41 * Indicates the progress of the preloader [0-100]. If it goes above 100, only the icon 42 * is shown with no progress bar. 43 */ 44 private int mProgress = 0; 45 46 private float mAnimationProgress = ANIMATION_PROGRESS_STOPPED; 47 private ObjectAnimator mAnimator; 48 PreloadIconDrawable(Drawable icon, Theme theme)49 public PreloadIconDrawable(Drawable icon, Theme theme) { 50 mIcon = icon; 51 52 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 53 mPaint.setStyle(Paint.Style.STROKE); 54 mPaint.setStrokeCap(Paint.Cap.ROUND); 55 56 setBounds(icon.getBounds()); 57 applyPreloaderTheme(theme); 58 onLevelChange(0); 59 } 60 applyPreloaderTheme(Theme t)61 public void applyPreloaderTheme(Theme t) { 62 TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable); 63 mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background); 64 mBgDrawable.setFilterBitmap(true); 65 mPaint.setStrokeWidth(ta.getDimension(R.styleable.PreloadIconDrawable_indicatorSize, 0)); 66 mRingOutset = ta.getDimensionPixelSize(R.styleable.PreloadIconDrawable_ringOutset, 0); 67 ta.recycle(); 68 onBoundsChange(getBounds()); 69 invalidateSelf(); 70 } 71 72 @Override onBoundsChange(Rect bounds)73 protected void onBoundsChange(Rect bounds) { 74 mIcon.setBounds(bounds); 75 if (mBgDrawable != null) { 76 sTempRect.set(bounds); 77 sTempRect.inset(-mRingOutset, -mRingOutset); 78 mBgDrawable.setBounds(sTempRect); 79 } 80 mIndicatorRectDirty = true; 81 } 82 getOutset()83 public int getOutset() { 84 return mRingOutset; 85 } 86 87 /** 88 * The size of the indicator is same as the content region of the {@link #mBgDrawable} minus 89 * half the stroke size to accommodate the indicator. 90 */ initIndicatorRect()91 private void initIndicatorRect() { 92 Drawable d = mBgDrawable; 93 Rect bounds = d.getBounds(); 94 95 d.getPadding(sTempRect); 96 // Amount by which padding has to be scaled 97 float paddingScaleX = ((float) bounds.width()) / d.getIntrinsicWidth(); 98 float paddingScaleY = ((float) bounds.height()) / d.getIntrinsicHeight(); 99 mIndicatorRect.set( 100 bounds.left + sTempRect.left * paddingScaleX, 101 bounds.top + sTempRect.top * paddingScaleY, 102 bounds.right - sTempRect.right * paddingScaleX, 103 bounds.bottom - sTempRect.bottom * paddingScaleY); 104 105 float inset = mPaint.getStrokeWidth() / 2; 106 mIndicatorRect.inset(inset, inset); 107 mIndicatorRectDirty = false; 108 } 109 110 @Override draw(Canvas canvas)111 public void draw(Canvas canvas) { 112 final Rect r = new Rect(getBounds()); 113 if (canvas.getClipBounds(sTempRect) && !Rect.intersects(sTempRect, r)) { 114 // The draw region has been clipped. 115 return; 116 } 117 if (mIndicatorRectDirty) { 118 initIndicatorRect(); 119 } 120 final float iconScale; 121 122 if ((mAnimationProgress >= ANIMATION_PROGRESS_STARTED) 123 && (mAnimationProgress < ANIMATION_PROGRESS_COMPLETED)) { 124 mPaint.setAlpha((int) ((1 - mAnimationProgress) * 255)); 125 mBgDrawable.setAlpha(mPaint.getAlpha()); 126 mBgDrawable.draw(canvas); 127 canvas.drawOval(mIndicatorRect, mPaint); 128 129 iconScale = ICON_SCALE_FACTOR + (1 - ICON_SCALE_FACTOR) * mAnimationProgress; 130 } else if (mAnimationProgress == ANIMATION_PROGRESS_STOPPED) { 131 mPaint.setAlpha(255); 132 iconScale = ICON_SCALE_FACTOR; 133 mBgDrawable.setAlpha(255); 134 mBgDrawable.draw(canvas); 135 136 if (mProgress >= 100) { 137 canvas.drawOval(mIndicatorRect, mPaint); 138 } else if (mProgress > 0) { 139 canvas.drawArc(mIndicatorRect, -90, mProgress * 3.6f, false, mPaint); 140 } 141 } else { 142 iconScale = 1; 143 } 144 145 canvas.save(); 146 canvas.scale(iconScale, iconScale, r.exactCenterX(), r.exactCenterY()); 147 mIcon.draw(canvas); 148 canvas.restore(); 149 } 150 151 @Override getOpacity()152 public int getOpacity() { 153 return PixelFormat.TRANSLUCENT; 154 } 155 156 @Override setAlpha(int alpha)157 public void setAlpha(int alpha) { 158 mIcon.setAlpha(alpha); 159 } 160 161 @Override setColorFilter(ColorFilter cf)162 public void setColorFilter(ColorFilter cf) { 163 mIcon.setColorFilter(cf); 164 } 165 166 @Override onLevelChange(int level)167 protected boolean onLevelChange(int level) { 168 mProgress = level; 169 170 // Stop Animation 171 if (mAnimator != null) { 172 mAnimator.cancel(); 173 mAnimator = null; 174 } 175 mAnimationProgress = ANIMATION_PROGRESS_STOPPED; 176 if (level > 0) { 177 // Set the paint color only when the level changes, so that the dominant color 178 // is only calculated when needed. 179 mPaint.setColor(getIndicatorColor()); 180 } 181 if (mIcon instanceof FastBitmapDrawable) { 182 ((FastBitmapDrawable) mIcon).setState(level <= 0 ? 183 FastBitmapDrawable.State.DISABLED : FastBitmapDrawable.State.NORMAL); 184 } 185 186 invalidateSelf(); 187 return true; 188 } 189 190 /** 191 * Runs the finish animation if it is has not been run after last level change. 192 */ maybePerformFinishedAnimation()193 public void maybePerformFinishedAnimation() { 194 if (mAnimationProgress > ANIMATION_PROGRESS_STOPPED) { 195 return; 196 } 197 if (mAnimator != null) { 198 mAnimator.cancel(); 199 } 200 setAnimationProgress(ANIMATION_PROGRESS_STARTED); 201 mAnimator = ObjectAnimator.ofFloat(this, "animationProgress", 202 ANIMATION_PROGRESS_STARTED, ANIMATION_PROGRESS_COMPLETED); 203 mAnimator.start(); 204 } 205 setAnimationProgress(float progress)206 public void setAnimationProgress(float progress) { 207 if (progress != mAnimationProgress) { 208 mAnimationProgress = progress; 209 invalidateSelf(); 210 } 211 } 212 getAnimationProgress()213 public float getAnimationProgress() { 214 return mAnimationProgress; 215 } 216 hasNotCompleted()217 public boolean hasNotCompleted() { 218 return mAnimationProgress < ANIMATION_PROGRESS_COMPLETED; 219 } 220 221 @Override getIntrinsicHeight()222 public int getIntrinsicHeight() { 223 return mIcon.getIntrinsicHeight(); 224 } 225 226 @Override getIntrinsicWidth()227 public int getIntrinsicWidth() { 228 return mIcon.getIntrinsicWidth(); 229 } 230 getIndicatorColor()231 private int getIndicatorColor() { 232 if (mIndicatorColor != 0) { 233 return mIndicatorColor; 234 } 235 if (!(mIcon instanceof FastBitmapDrawable)) { 236 mIndicatorColor = DEFAULT_COLOR; 237 return mIndicatorColor; 238 } 239 mIndicatorColor = Utilities.findDominantColorByHue( 240 ((FastBitmapDrawable) mIcon).getBitmap(), 20); 241 242 // Make sure that the dominant color has enough saturation to be visible properly. 243 float[] hsv = new float[3]; 244 Color.colorToHSV(mIndicatorColor, hsv); 245 if (hsv[1] < MIN_SATUNATION) { 246 mIndicatorColor = DEFAULT_COLOR; 247 return mIndicatorColor; 248 } 249 hsv[2] = Math.max(MIN_LIGHTNESS, hsv[2]); 250 mIndicatorColor = Color.HSVToColor(hsv); 251 return mIndicatorColor; 252 } 253 } 254