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 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 applyTheme(theme); 58 onLevelChange(0); 59 } 60 61 @Override 62 public void applyTheme(Theme t) { 63 TypedArray ta = t.obtainStyledAttributes(R.styleable.PreloadIconDrawable); 64 mBgDrawable = ta.getDrawable(R.styleable.PreloadIconDrawable_background); 65 mBgDrawable.setFilterBitmap(true); 66 mPaint.setStrokeWidth(ta.getDimension(R.styleable.PreloadIconDrawable_indicatorSize, 0)); 67 mRingOutset = ta.getDimensionPixelSize(R.styleable.PreloadIconDrawable_ringOutset, 0); 68 ta.recycle(); 69 onBoundsChange(getBounds()); 70 invalidateSelf(); 71 } 72 73 @Override 74 protected void onBoundsChange(Rect bounds) { 75 mIcon.setBounds(bounds); 76 if (mBgDrawable != null) { 77 sTempRect.set(bounds); 78 sTempRect.inset(-mRingOutset, -mRingOutset); 79 mBgDrawable.setBounds(sTempRect); 80 } 81 mIndicatorRectDirty = true; 82 } 83 84 public int getOutset() { 85 return mRingOutset; 86 } 87 88 /** 89 * The size of the indicator is same as the content region of the {@link #mBgDrawable} minus 90 * half the stroke size to accommodate the indicator. 91 */ 92 private void initIndicatorRect() { 93 Drawable d = mBgDrawable; 94 Rect bounds = d.getBounds(); 95 96 d.getPadding(sTempRect); 97 // Amount by which padding has to be scaled 98 float paddingScaleX = ((float) bounds.width()) / d.getIntrinsicWidth(); 99 float paddingScaleY = ((float) bounds.height()) / d.getIntrinsicHeight(); 100 mIndicatorRect.set( 101 bounds.left + sTempRect.left * paddingScaleX, 102 bounds.top + sTempRect.top * paddingScaleY, 103 bounds.right - sTempRect.right * paddingScaleX, 104 bounds.bottom - sTempRect.bottom * paddingScaleY); 105 106 float inset = mPaint.getStrokeWidth() / 2; 107 mIndicatorRect.inset(inset, inset); 108 mIndicatorRectDirty = false; 109 } 110 111 @Override 112 public void draw(Canvas canvas) { 113 final Rect r = new Rect(getBounds()); 114 if (canvas.getClipBounds(sTempRect) && !Rect.intersects(sTempRect, r)) { 115 // The draw region has been clipped. 116 return; 117 } 118 if (mIndicatorRectDirty) { 119 initIndicatorRect(); 120 } 121 final float iconScale; 122 123 if ((mAnimationProgress >= ANIMATION_PROGRESS_STARTED) 124 && (mAnimationProgress < ANIMATION_PROGRESS_COMPLETED)) { 125 mPaint.setAlpha((int) ((1 - mAnimationProgress) * 255)); 126 mBgDrawable.setAlpha(mPaint.getAlpha()); 127 mBgDrawable.draw(canvas); 128 canvas.drawOval(mIndicatorRect, mPaint); 129 130 iconScale = ICON_SCALE_FACTOR + (1 - ICON_SCALE_FACTOR) * mAnimationProgress; 131 } else if (mAnimationProgress == ANIMATION_PROGRESS_STOPPED) { 132 mPaint.setAlpha(255); 133 iconScale = ICON_SCALE_FACTOR; 134 mBgDrawable.setAlpha(255); 135 mBgDrawable.draw(canvas); 136 137 if (mProgress >= 100) { 138 canvas.drawOval(mIndicatorRect, mPaint); 139 } else if (mProgress > 0) { 140 canvas.drawArc(mIndicatorRect, -90, mProgress * 3.6f, false, mPaint); 141 } 142 } else { 143 iconScale = 1; 144 } 145 146 canvas.save(); 147 canvas.scale(iconScale, iconScale, r.exactCenterX(), r.exactCenterY()); 148 mIcon.draw(canvas); 149 canvas.restore(); 150 } 151 152 @Override 153 public int getOpacity() { 154 return PixelFormat.TRANSLUCENT; 155 } 156 157 @Override 158 public void setAlpha(int alpha) { 159 mIcon.setAlpha(alpha); 160 } 161 162 @Override 163 public void setColorFilter(ColorFilter cf) { 164 mIcon.setColorFilter(cf); 165 } 166 167 @Override 168 protected boolean onLevelChange(int level) { 169 mProgress = level; 170 171 // Stop Animation 172 if (mAnimator != null) { 173 mAnimator.cancel(); 174 mAnimator = null; 175 } 176 mAnimationProgress = ANIMATION_PROGRESS_STOPPED; 177 if (level > 0) { 178 // Set the paint color only when the level changes, so that the dominant color 179 // is only calculated when needed. 180 mPaint.setColor(getIndicatorColor()); 181 } 182 if (mIcon instanceof FastBitmapDrawable) { 183 ((FastBitmapDrawable) mIcon).setGhostModeEnabled(level <= 0); 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 */ 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 206 public void setAnimationProgress(float progress) { 207 if (progress != mAnimationProgress) { 208 mAnimationProgress = progress; 209 invalidateSelf(); 210 } 211 } 212 213 public float getAnimationProgress() { 214 return mAnimationProgress; 215 } 216 217 @Override 218 public int getIntrinsicHeight() { 219 return mIcon.getIntrinsicHeight(); 220 } 221 222 @Override 223 public int getIntrinsicWidth() { 224 return mIcon.getIntrinsicWidth(); 225 } 226 227 private int getIndicatorColor() { 228 if (mIndicatorColor != 0) { 229 return mIndicatorColor; 230 } 231 if (!(mIcon instanceof FastBitmapDrawable)) { 232 mIndicatorColor = DEFAULT_COLOR; 233 return mIndicatorColor; 234 } 235 mIndicatorColor = Utilities.findDominantColorByHue( 236 ((FastBitmapDrawable) mIcon).getBitmap(), 20); 237 238 // Make sure that the dominant color has enough saturation to be visible properly. 239 float[] hsv = new float[3]; 240 Color.colorToHSV(mIndicatorColor, hsv); 241 if (hsv[1] < MIN_SATUNATION) { 242 mIndicatorColor = DEFAULT_COLOR; 243 return mIndicatorColor; 244 } 245 hsv[2] = Math.max(MIN_LIGHTNESS, hsv[2]); 246 mIndicatorColor = Color.HSVToColor(hsv); 247 return mIndicatorColor; 248 } 249 } 250