/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.graphics; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.Rect; import android.util.Property; import android.util.SparseArray; import android.view.animation.LinearInterpolator; import com.android.launcher3.FastBitmapDrawable; import java.lang.ref.WeakReference; /** * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon. */ public class PreloadIconDrawable extends FastBitmapDrawable { private static final Property INTERNAL_STATE = new Property(Float.TYPE, "internalStateProgress") { @Override public Float get(PreloadIconDrawable object) { return object.mInternalStateProgress; } @Override public void set(PreloadIconDrawable object, Float value) { object.setInternalProgress(value); } }; public static final int PATH_SIZE = 100; private static final float PROGRESS_WIDTH = 7; private static final float PROGRESS_GAP = 2; private static final int MAX_PAINT_ALPHA = 255; private static final long DURATION_SCALE = 500; // The smaller the number, the faster the animation would be. // Duration = COMPLETE_ANIM_FRACTION * DURATION_SCALE private static final float COMPLETE_ANIM_FRACTION = 0.3f; private static final int COLOR_TRACK = 0x77EEEEEE; private static final int COLOR_SHADOW = 0x55000000; private static final float SMALL_SCALE = 0.75f; private static final SparseArray> sShadowCache = new SparseArray<>(); private final Matrix mTmpMatrix = new Matrix(); private final PathMeasure mPathMeasure = new PathMeasure(); private final Context mContext; // Path in [0, 100] bounds. private final Path mProgressPath; private final Path mScaledTrackPath; private final Path mScaledProgressPath; private final Paint mProgressPaint; private Bitmap mShadowBitmap; private int mIndicatorColor = 0; private int mTrackAlpha; private float mTrackLength; private float mIconScale; private boolean mRanFinishAnimation; // Progress of the internal state. [0, 1] indicates the fraction of completed progress, // [1, (1 + COMPLETE_ANIM_FRACTION)] indicates the progress of zoom animation. private float mInternalStateProgress; private ObjectAnimator mCurrentAnim; /** * @param progressPath fixed path in the bounds [0, 0, 100, 100] representing a progress bar. */ public PreloadIconDrawable(Bitmap b, Path progressPath, Context context) { super(b); mContext = context; mProgressPath = progressPath; mScaledTrackPath = new Path(); mScaledProgressPath = new Path(); mProgressPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); mProgressPaint.setStyle(Paint.Style.STROKE); mProgressPaint.setStrokeCap(Paint.Cap.ROUND); setInternalProgress(0); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); mTmpMatrix.setScale( (bounds.width() - PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE, (bounds.height() - PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE); mTmpMatrix.postTranslate( bounds.left + PROGRESS_WIDTH / 2 + PROGRESS_GAP, bounds.top + PROGRESS_WIDTH / 2 + PROGRESS_GAP); mProgressPath.transform(mTmpMatrix, mScaledTrackPath); float scale = bounds.width() / PATH_SIZE; mProgressPaint.setStrokeWidth(PROGRESS_WIDTH * scale); mShadowBitmap = getShadowBitmap(bounds.width(), bounds.height(), (PROGRESS_GAP ) * scale); mPathMeasure.setPath(mScaledTrackPath, true); mTrackLength = mPathMeasure.getLength(); setInternalProgress(mInternalStateProgress); } private Bitmap getShadowBitmap(int width, int height, float shadowRadius) { int key = (width << 16) | height; WeakReference shadowRef = sShadowCache.get(key); Bitmap shadow = shadowRef != null ? shadowRef.get() : null; if (shadow != null) { return shadow; } shadow = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(shadow); mProgressPaint.setShadowLayer(shadowRadius, 0, 0, COLOR_SHADOW); mProgressPaint.setColor(COLOR_TRACK); mProgressPaint.setAlpha(MAX_PAINT_ALPHA); c.drawPath(mScaledTrackPath, mProgressPaint); mProgressPaint.clearShadowLayer(); c.setBitmap(null); sShadowCache.put(key, new WeakReference<>(shadow)); return shadow; } @Override public void draw(Canvas canvas) { if (mRanFinishAnimation) { super.draw(canvas); return; } // Draw track. mProgressPaint.setColor(mIndicatorColor); mProgressPaint.setAlpha(mTrackAlpha); if (mShadowBitmap != null) { canvas.drawBitmap(mShadowBitmap, getBounds().left, getBounds().top, mProgressPaint); } canvas.drawPath(mScaledProgressPath, mProgressPaint); int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); Rect bounds = getBounds(); canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY()); drawInternal(canvas); canvas.restoreToCount(saveCount); } /** * Updates the install progress based on the level */ @Override protected boolean onLevelChange(int level) { // Run the animation if we have already been bound. updateInternalState(level * 0.01f, getBounds().width() > 0, false); return true; } /** * Runs the finish animation if it is has not been run after last call to * {@link #onLevelChange} */ public void maybePerformFinishedAnimation() { // If the drawable was recently initialized, skip the progress animation. if (mInternalStateProgress == 0) { mInternalStateProgress = 1; } updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true); } public boolean hasNotCompleted() { return !mRanFinishAnimation; } private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) { if (mCurrentAnim != null) { mCurrentAnim.cancel(); mCurrentAnim = null; } if (Float.compare(finalProgress, mInternalStateProgress) == 0) { return; } if (finalProgress < mInternalStateProgress) { shouldAnimate = false; } if (!shouldAnimate || mRanFinishAnimation) { setInternalProgress(finalProgress); } else { mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress); mCurrentAnim.setDuration( (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE)); mCurrentAnim.setInterpolator(new LinearInterpolator()); if (isFinish) { mCurrentAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mRanFinishAnimation = true; } }); } mCurrentAnim.start(); } } /** * Sets the internal progress and updates the UI accordingly * for progress <= 0: * - icon in the small scale and disabled state * - progress track is visible * - progress bar is not visible * for 0 < progress < 1 * - icon in the small scale and disabled state * - progress track is visible * - progress bar is visible with dominant color. Progress bar is drawn as a fraction of * {@link #mScaledTrackPath}. * @see PathMeasure#getSegment(float, float, Path, boolean) * for 1 <= progress < (1 + COMPLETE_ANIM_FRACTION) * - we calculate fraction of progress in the above range * - progress track is drawn with alpha based on fraction * - progress bar is drawn at 100% with alpha based on fraction * - icon is scaled up based on fraction and is drawn in enabled state * for progress >= (1 + COMPLETE_ANIM_FRACTION) * - only icon is drawn in normal state */ private void setInternalProgress(float progress) { mInternalStateProgress = progress; if (progress <= 0) { mIconScale = SMALL_SCALE; mScaledTrackPath.reset(); mTrackAlpha = MAX_PAINT_ALPHA; setIsDisabled(true); } else if (mIndicatorColor == 0) { // Update the indicator color mIndicatorColor = getIconPalette().getPreloadProgressColor(mContext); } if (progress < 1 && progress > 0) { mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true); mIconScale = SMALL_SCALE; mTrackAlpha = MAX_PAINT_ALPHA; setIsDisabled(true); } else if (progress >= 1) { setIsDisabled(false); mScaledTrackPath.set(mScaledProgressPath); float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION; if (fraction >= 1) { // Animation has completed mIconScale = 1; mTrackAlpha = 0; } else { mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA); mIconScale = SMALL_SCALE + (1 - SMALL_SCALE) * fraction; } } invalidateSelf(); } }