blob: 42ba191c23d297d2ffcd05681d0e2cb83ec6c20f [file] [log] [blame]
/*
* 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 com.android.launcher3.FastBitmapDrawable;
import com.android.launcher3.ItemInfoWithIcon;
import com.android.launcher3.anim.Interpolators;
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<PreloadIconDrawable, Float> INTERNAL_STATE =
new Property<PreloadIconDrawable, Float>(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.6f;
private static final SparseArray<WeakReference<Bitmap>> 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 final int mIndicatorColor;
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(ItemInfoWithIcon info, Path progressPath, Context context) {
super(info);
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);
mIndicatorColor = IconPalette.getPreloadProgressColor(context, mIconColor);
setInternalProgress(0);
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mTmpMatrix.setScale(
(bounds.width() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE,
(bounds.height() - 2 * PROGRESS_WIDTH - 2 * PROGRESS_GAP) / PATH_SIZE);
mTmpMatrix.postTranslate(
bounds.left + PROGRESS_WIDTH + PROGRESS_GAP,
bounds.top + PROGRESS_WIDTH + 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<Bitmap> 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 drawInternal(Canvas canvas, Rect bounds) {
if (mRanFinishAnimation) {
super.drawInternal(canvas, bounds);
return;
}
// Draw track.
mProgressPaint.setColor(mIndicatorColor);
mProgressPaint.setAlpha(mTrackAlpha);
if (mShadowBitmap != null) {
canvas.drawBitmap(mShadowBitmap, bounds.left, bounds.top, mProgressPaint);
}
canvas.drawPath(mScaledProgressPath, mProgressPaint);
int saveCount = canvas.save();
canvas.scale(mIconScale, mIconScale, bounds.exactCenterX(), bounds.exactCenterY());
super.drawInternal(canvas, bounds);
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(Interpolators.LINEAR);
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);
}
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();
}
}