Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 196569bb authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add finish icon scale animation for downloading apps" into tm-qpr-dev

parents 1eb59d6c 4eb502ae
Loading
Loading
Loading
Loading
+18 −15
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

package com.android.launcher3;

import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING;
import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
import static com.android.launcher3.icons.BitmapInfo.FLAG_NO_BADGE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED;
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -291,7 +294,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,

    @UiThread
    public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) {
        applyFromWorkspaceItem(info, false);
        applyFromWorkspaceItem(info, null);
    }

    /**
@@ -320,10 +323,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
    }

    @UiThread
    public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean promiseStateChanged) {
    public void applyFromWorkspaceItem(WorkspaceItemInfo info, PreloadIconDrawable icon) {
        applyIconAndLabel(info);
        setItemInfo(info);
        applyLoadingState(promiseStateChanged);
        applyLoadingState(icon);
        applyDotState(info, false /* animate */);
        setDownloadStateContentDescription(info, info.getProgressLevel());
    }
@@ -710,23 +713,23 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
     * If this app is installed and downloading incrementally, the progress bar will be updated
     * with the total download progress.
     */
    public void applyLoadingState(boolean promiseStateChanged) {
    public void applyLoadingState(PreloadIconDrawable icon) {
        if (getTag() instanceof ItemInfoWithIcon) {
            WorkspaceItemInfo info = (WorkspaceItemInfo) getTag();
            if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE)
                    != 0) {
                updateProgressBarUi(info.getProgressLevel() == 100);
            } else if (info.hasPromiseIconUi() || (info.runtimeStatusFlags
                        & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
                updateProgressBarUi(promiseStateChanged);
            if ((info.runtimeStatusFlags & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0
                    || info.hasPromiseIconUi()
                    || (info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0
                    || (ENABLE_DOWNLOAD_APP_UX_V2.get() && icon != null)) {
                updateProgressBarUi(icon);
            }
        }
    }

    private void updateProgressBarUi(boolean maybePerformFinishedAnimation) {
    private void updateProgressBarUi(PreloadIconDrawable oldIcon) {
        FastBitmapDrawable originalIcon = mIcon;
        PreloadIconDrawable preloadDrawable = applyProgressLevel();
        if (preloadDrawable != null && maybePerformFinishedAnimation) {
            preloadDrawable.maybePerformFinishedAnimation();
        if (preloadDrawable != null && oldIcon != null) {
            preloadDrawable.maybePerformFinishedAnimation(oldIcon, () -> setIcon(originalIcon));
        }
    }

@@ -824,12 +827,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
                != 0) {
            String percentageString = NumberFormat.getPercentInstance()
                    .format(progressLevel * 0.01);
            if ((info.runtimeStatusFlags & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
            if ((info.runtimeStatusFlags & FLAG_INSTALL_SESSION_ACTIVE) != 0) {
                setContentDescription(getContext()
                        .getString(
                            R.string.app_installing_title, info.title, percentageString));
            } else if ((info.runtimeStatusFlags
                    & ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
                    & FLAG_INCREMENTAL_DOWNLOAD_ACTIVE) != 0) {
                setContentDescription(getContext()
                        .getString(
                            R.string.app_downloading_title, info.title, percentageString));
+96 −122
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
@@ -41,6 +42,7 @@ import android.util.Property;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.model.data.ItemInfoWithIcon;
@@ -53,7 +55,7 @@ import java.util.function.Function;
/**
 * Extension of {@link FastBitmapDrawable} which shows a progress bar around the icon.
 */
public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable {
public class PreloadIconDrawable extends FastBitmapDrawable {

    private static final Property<PreloadIconDrawable, Float> INTERNAL_STATE =
            new Property<PreloadIconDrawable, Float>(Float.TYPE, "internalStateProgress") {
@@ -78,16 +80,19 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable

    // 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 float COMPLETE_ANIM_FRACTION = 1f;

    private static final float SMALL_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get() ? 0.867f : 0.7f;
    private static final float PROGRESS_STROKE_SCALE = 0.075f;
    private static final float PROGRESS_STROKE_SCALE = ENABLE_DOWNLOAD_APP_UX_V2.get()
            ? 0.0655f
            : 0.075f;
    private static final float PROGRESS_BOUNDS_SCALE = 0.075f;

    private static final int PRELOAD_ACCENT_COLOR_INDEX = 0;
    private static final int PRELOAD_BACKGROUND_COLOR_INDEX = 1;

    private static final int ALPHA_DURATION_MILLIS = 3000;
    private static final float OVERLAY_ALPHA_RANGE = 127.5f;
    private static final int OVERLAY_ALPHA_RANGE = 127;
    private static final long WAVE_MOTION_DELAY_FACTOR_MILLIS = 100;
    private static final WeakHashMap<Integer, PorterDuffColorFilter> COLOR_FILTER_MAP =
            new WeakHashMap<>();
@@ -111,19 +116,17 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
    private final int mSystemBackgroundColor;
    private final boolean mIsDarkMode;

    private int mTrackAlpha;
    private float mTrackLength;

    private boolean mRanFinishAnimation;

    private final int mRefreshRateMillis;
    private final AnimatedFloat mIconScale = new AnimatedFloat(this::invalidateSelf);
    private final AnimatedFloat mOverlayAlpha = new AnimatedFloat(this::updateOverlayAlpha);
    private boolean mShouldAnimateScaleAndAlpha;

    // 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;
    // This multiplier is used to animate scale when going from 0 to non-zero and expanding
    private final Runnable mInvalidateRunnable = this::invalidateSelf;
    private final AnimatedFloat mIconScaleMultiplier = new AnimatedFloat(mInvalidateRunnable);

    private ObjectAnimator mCurrentAnim;

@@ -160,10 +163,7 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
        mRefreshRateMillis = refreshRateMillis;

        // If it's a pending app we will animate scale and alpha when it's no longer pending.
        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && info.getProgressLevel() == 0) {
            mShouldAnimateScaleAndAlpha = true;
            mOverlayAlpha.updateValue(127);
        }
        mIconScaleMultiplier.updateValue(info.getProgressLevel() == 0 ? 0 : 1);

        setLevel(info.getProgressLevel());
        setIsStartable(info.isAppStartable());
@@ -173,14 +173,17 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
    protected void onBoundsChange(Rect bounds) {
        super.onBoundsChange(bounds);

        float progressWidth = PROGRESS_STROKE_SCALE * bounds.width();

        float progressWidth = bounds.width() * (ENABLE_DOWNLOAD_APP_UX_V2.get()
                ? PROGRESS_BOUNDS_SCALE
                : PROGRESS_STROKE_SCALE);
        mTmpMatrix.setScale(
                (bounds.width() - 2 * progressWidth) / DEFAULT_PATH_SIZE,
                (bounds.height() - 2 * progressWidth) / DEFAULT_PATH_SIZE);
        mTmpMatrix.postTranslate(bounds.left + progressWidth, bounds.top + progressWidth);

        mShapePath.transform(mTmpMatrix, mScaledTrackPath);
        mProgressPaint.setStrokeWidth(progressWidth);
        mProgressPaint.setStrokeWidth(PROGRESS_STROKE_SCALE * bounds.width());

        mPathMeasure.setPath(mScaledTrackPath, true);
        mTrackLength = mPathMeasure.getLength();
@@ -195,26 +198,35 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
            return;
        }

        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() && mInternalStateProgress > 0) {
            // Draw background.
            mProgressPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            mProgressPaint.setColor(mSystemBackgroundColor);
            canvas.drawPath(mScaledTrackPath, mProgressPaint);
        }

        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
            // Draw track and progress.
            mProgressPaint.setStyle(Paint.Style.STROKE);
        mProgressPaint.setColor(mIsStartable ? mIndicatorColor : mSystemAccentColor);
            mProgressPaint.setColor(mSystemAccentColor);
            mProgressPaint.setAlpha(TRACK_ALPHA);
            canvas.drawPath(mScaledTrackPath, mProgressPaint);
        mProgressPaint.setAlpha(mTrackAlpha);
            mProgressPaint.setAlpha(MAX_PAINT_ALPHA);
            canvas.drawPath(mScaledProgressPath, mProgressPaint);
        }

        int saveCount = canvas.save();
        canvas.scale(
                mIconScale.value, mIconScale.value, bounds.exactCenterX(), bounds.exactCenterY());
        float scale = ENABLE_DOWNLOAD_APP_UX_V2.get()
                ? 1 - mIconScaleMultiplier.value * (1 - SMALL_SCALE)
                : SMALL_SCALE;
        canvas.scale(scale, scale, bounds.exactCenterX(), bounds.exactCenterY());

        ColorFilter filter = getOverlayFilter();
        mPaint.setColorFilter(filter);
        super.drawInternal(canvas, bounds);
        canvas.restoreToCount(saveCount);

        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && mInternalStateProgress == 0) {
        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && filter != null) {
            reschedule();
        }
    }
@@ -232,7 +244,7 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
    @Override
    protected boolean onLevelChange(int level) {
        // Run the animation if we have already been bound.
        updateInternalState(level * 0.01f,  getBounds().width() > 0, false);
        updateInternalState(level * 0.01f, false, null);
        return true;
    }

@@ -240,12 +252,18 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
     * Runs the finish animation if it is has not been run after last call to
     * {@link #onLevelChange}
     */
    public void maybePerformFinishedAnimation() {
    public void maybePerformFinishedAnimation(
            PreloadIconDrawable oldIcon, Runnable onFinishCallback) {

        if (oldIcon.mInternalStateProgress >= 1) {
            mInternalStateProgress = oldIcon.mInternalStateProgress;
        }

        // If the drawable was recently initialized, skip the progress animation.
        if (mInternalStateProgress == 0) {
            mInternalStateProgress = 1;
        }
        updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, true);
        updateInternalState(1 + COMPLETE_ANIM_FRACTION, true, onFinishCallback);
    }

    public boolean hasNotCompleted() {
@@ -260,26 +278,29 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
        }
    }

    private void updateInternalState(float finalProgress, boolean shouldAnimate, boolean isFinish) {
    private void updateInternalState(
            float finalProgress, boolean isFinish, Runnable onFinishCallback) {
        if (mCurrentAnim != null) {
            mCurrentAnim.cancel();
            mCurrentAnim = null;
        }

        if (Float.compare(finalProgress, mInternalStateProgress) == 0) {
            return;
        }
        if (finalProgress < mInternalStateProgress) {
            shouldAnimate = false;
        }
        if (!shouldAnimate || mRanFinishAnimation) {
        boolean animateProgress =
                finalProgress >= mInternalStateProgress && getBounds().width() > 0;
        if (!animateProgress || mRanFinishAnimation) {
            setInternalProgress(finalProgress);
            if (isFinish && onFinishCallback != null) {
                onFinishCallback.run();
            }
        } else {
            mCurrentAnim = ObjectAnimator.ofFloat(this, INTERNAL_STATE, finalProgress);
            mCurrentAnim.setDuration(
                    (long) ((finalProgress - mInternalStateProgress) * DURATION_SCALE));
            mCurrentAnim.setInterpolator(LINEAR);
            if (isFinish) {
                if (onFinishCallback != null) {
                    mCurrentAnim.addListener(AnimatorListeners.forEndCallback(onFinishCallback));
                }
                mCurrentAnim.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
@@ -297,62 +318,38 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
     *     - icon with pending motion
     *     - progress track is not visible
     *     - progress bar is not visible
     *   for progress < 1
     *   for progress < 1:
     *     - icon without pending motion
     *     - progress track is visible
     *     - progress bar is visible. 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
     *   for progress > 1:
     *     - scale the icon back to full size
     */
    private void setInternalProgress(float progress) {
        // Animate scale and alpha from pending to downloading state.
        if (ENABLE_DOWNLOAD_APP_UX_V2.get()
                && mShouldAnimateScaleAndAlpha && mInternalStateProgress == 0 && progress > 0) {
            Animator iconScaleAnimator = mIconScale.animateToValue(SMALL_SCALE);
        if (ENABLE_DOWNLOAD_APP_UX_V2.get() && progress > 0 && mInternalStateProgress == 0) {
            // Progress is changing for the first time, animate the icon scale
            Animator iconScaleAnimator = mIconScaleMultiplier.animateToValue(1);
            iconScaleAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
            iconScaleAnimator.setInterpolator(EMPHASIZED);
            iconScaleAnimator.start();

            Animator overlayAlphaAnimator = mOverlayAlpha.animateToValue(0);
            overlayAlphaAnimator.setDuration(SCALE_AND_ALPHA_ANIM_DURATION);
            overlayAlphaAnimator.setInterpolator(EMPHASIZED);
            overlayAlphaAnimator.start();
        }

        mInternalStateProgress = progress;
        if (progress <= 0) {
            mIconScale.updateValue(ENABLE_DOWNLOAD_APP_UX_V2.get() ? 1 : SMALL_SCALE);
            if (!ENABLE_DOWNLOAD_APP_UX_V2.get()) {
                mScaledTrackPath.reset();
            mTrackAlpha = MAX_PAINT_ALPHA;
        } else if (progress < 1) {
            mPathMeasure.getSegment(0, progress * mTrackLength, mScaledProgressPath, true);
            if (ENABLE_DOWNLOAD_APP_UX_V2.get()) {
                mPathMeasure.getSegment(0, mTrackLength, mScaledTrackPath, true);
            }

            if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || !mShouldAnimateScaleAndAlpha) {
                mIconScale.updateValue(SMALL_SCALE);
            }
            mTrackAlpha = MAX_PAINT_ALPHA;
        } else {
            setIsDisabled(mItem.isDisabled());
            mScaledTrackPath.set(mScaledProgressPath);
            float fraction = (progress - 1) / COMPLETE_ANIM_FRACTION;

            if (fraction >= 1) {
                // Animation has completed
                mIconScale.updateValue(1);
                mTrackAlpha = 0;
            mIconScaleMultiplier.updateValue(0);
        } else {
                mTrackAlpha = Math.round((1 - fraction) * MAX_PAINT_ALPHA);
                mIconScale.updateValue(SMALL_SCALE + (1 - SMALL_SCALE) * fraction);
            mPathMeasure.getSegment(
                    0, Math.min(progress, 1) * mTrackLength, mScaledProgressPath, true);
            if (progress > 1 && ENABLE_DOWNLOAD_APP_UX_V2.get()) {
                // map the scale back to original value
                mIconScaleMultiplier.updateValue(Utilities.mapBoundToRange(
                        progress - 1, 0, COMPLETE_ANIM_FRACTION, 1, 0, EMPHASIZED));
            }
        }
        invalidateSelf();
@@ -392,72 +389,49 @@ public class PreloadIconDrawable extends FastBitmapDrawable implements Runnable
                mRefreshRateMillis);
    }

    @Override
    public void run() {
        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
            return;
        }
        if (!applyPendingIconOverlay()) {
            reschedule();
        }
    }

    @Override
    public boolean setVisible(boolean visible, boolean restart) {
        boolean result = super.setVisible(visible, restart);
        if (visible) {
            reschedule();
        } else {
            unscheduleSelf(this);
        if (!visible) {
            unscheduleSelf(mInvalidateRunnable);
        }
        return result;
        return super.setVisible(visible, restart);
    }

    private void reschedule() {
        unscheduleSelf(this);

        unscheduleSelf(mInvalidateRunnable);
        if (!isVisible()) {
            return;
        }

        final long upTime = SystemClock.uptimeMillis();
        scheduleSelf(this, upTime - ((upTime % mRefreshRateMillis)) + mRefreshRateMillis);
        scheduleSelf(mInvalidateRunnable,
                upTime - ((upTime % mRefreshRateMillis)) + mRefreshRateMillis);
    }


    /**
     * Apply an overlay on the pending icon with cascading motion based on its position.
     * Returns {@code true} if the icon alpha is updated, so that we re-draw.
     * Returns a color filter to be used as an overlay on the pending icon with cascading motion
     * based on its position.
     */
    private boolean applyPendingIconOverlay() {
    private ColorFilter getOverlayFilter() {
        if (!ENABLE_DOWNLOAD_APP_UX_V2.get() || mInternalStateProgress > 0) {
            // If the download has started, we do no need to animate
            return null;
        }
        long waveMotionDelay = (mItem.cellX * WAVE_MOTION_DELAY_FACTOR_MILLIS)
                + (mItem.cellY * WAVE_MOTION_DELAY_FACTOR_MILLIS);
        long time = SystemClock.uptimeMillis();
        float newAlpha = Utilities.mapBoundToRange(
                (float) (time + waveMotionDelay) % ALPHA_DURATION_MILLIS,
        int alpha = (int) Utilities.mapBoundToRange(
                (int) ((time + waveMotionDelay) % ALPHA_DURATION_MILLIS),
                0,
                ALPHA_DURATION_MILLIS,
                0,
                MAX_PAINT_ALPHA,
                OVERLAY_ALPHA_RANGE * 2,
                LINEAR);
        if (newAlpha > OVERLAY_ALPHA_RANGE) {
            newAlpha = (OVERLAY_ALPHA_RANGE - (newAlpha % OVERLAY_ALPHA_RANGE));
        if (alpha > OVERLAY_ALPHA_RANGE) {
            alpha = (OVERLAY_ALPHA_RANGE - (alpha % OVERLAY_ALPHA_RANGE));
        }

        boolean invalidate = false;
        if ((int) mOverlayAlpha.value != newAlpha) {
            mOverlayAlpha.updateValue(newAlpha);
            invalidate = true;
        }
        return invalidate;
    }

    private void updateOverlayAlpha() {
        int overlayColor = mIsDarkMode ? 0 : 255;
        int currArgb =
                Color.argb((int) mOverlayAlpha.value, overlayColor, overlayColor, overlayColor);
        mPaint.setColorFilter(COLOR_FILTER_MAP.computeIfAbsent(currArgb, FILTER_FACTORY));
        invalidateSelf();
        int currArgb = Color.argb(alpha, overlayColor, overlayColor, overlayColor);
        return COLOR_FILTER_MAP.computeIfAbsent(currArgb, FILTER_FACTORY);
    }

    protected static class PreloadIconConstantState extends FastBitmapConstantState {
+7 −2
Original line number Diff line number Diff line
@@ -50,7 +50,12 @@ public interface LauncherBindableItemsContainer {
                Drawable oldIcon = shortcut.getIcon();
                boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
                        && ((PreloadIconDrawable) oldIcon).hasNotCompleted();
                shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
                shortcut.applyFromWorkspaceItem(
                        si,
                        si.isPromise() != oldPromiseState
                                && oldIcon instanceof PreloadIconDrawable
                                ? (PreloadIconDrawable) oldIcon
                                : null);
            } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
                ((FolderIcon) v).updatePreviewItems(updates::contains);
            }
@@ -74,7 +79,7 @@ public interface LauncherBindableItemsContainer {
        ItemOperator op = (info, v) -> {
            if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
                    && updates.contains(info)) {
                ((BubbleTextView) v).applyLoadingState(false /* promiseStateChanged */);
                ((BubbleTextView) v).applyLoadingState(null);
            } else if (v instanceof PendingAppWidgetHostView
                    && info instanceof LauncherAppWidgetInfo
                    && updates.contains(info)) {