// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.widget; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.widget.FrameLayout.LayoutParams; import android.widget.ImageView; import org.chromium.ui.base.LocalizationUtils; import org.chromium.ui.interpolators.BakedBezierInterpolator; /** * An animating ImageView that is drawn on top of the progress bar. This will animate over the * current length of the progress bar only if the progress bar is static for some amount of time. */ public class ToolbarProgressBarAnimatingView extends ImageView { /** The drawable inside this ImageView. */ private final ColorDrawable mAnimationDrawable; /** The fraction of the total time that the slow animation should take. */ private static final float SLOW_ANIMATION_FRACTION = 0.60f; /** The fraction of the total time that the fast animation delay should take. */ private static final float FAST_ANIMATION_DELAY = 0.02f; /** The fraction of the total time that the fast animation should take. */ private static final float FAST_ANIMATION_FRACTION = 0.38f; /** The time between animation sequences. */ private static final int ANIMATION_DELAY_MS = 1000; /** The width of the animating bar relative to the current width of the progress bar. */ private static final float ANIMATING_BAR_SCALE = 0.3f; /** * The width of the animating bar relative to the current width of the progress bar for the * first half of the slow animation. */ private static final float SMALL_ANIMATING_BAR_SCALE = 0.1f; /** The fraction of overall completion that the small animating bar should be expanded at. */ private static final float SMALL_BAR_EXPANSION_COMPLETE = 0.6f; /** The maximum size of the animating view. */ private static final float ANIMATING_VIEW_MAX_WIDTH_DP = 400; /** Interpolator for enter and exit animation. */ private final BakedBezierInterpolator mBezier = BakedBezierInterpolator.FADE_OUT_CURVE; /** The current width of the progress bar. */ private float mProgressWidth = 0; /** The set of individual animators that constitute the whole animation sequence. */ private final AnimatorSet mAnimatorSet; /** The animator controlling the fast animation. */ private final ValueAnimator mFastAnimation; /** The animator controlling the slow animation. */ private final ValueAnimator mSlowAnimation; /** Track if the animation has been canceled. */ private boolean mIsCanceled; /** If the layout is RTL. */ private boolean mIsRtl; /** The update listener for the animation. */ private ProgressBarUpdateListener mListener; /** The last fraction of the animation that was drawn. */ private float mLastAnimatedFraction; /** The last animation that received an update. */ private ValueAnimator mLastUpdatedAnimation; /** The ratio of px to dp. */ private float mDpToPx; /** * An animation update listener that moves an ImageView across the progress bar. */ private class ProgressBarUpdateListener implements AnimatorUpdateListener { @Override public void onAnimationUpdate(ValueAnimator animation) { mLastUpdatedAnimation = animation; mLastAnimatedFraction = animation.getAnimatedFraction(); updateAnimation(mLastUpdatedAnimation, mLastAnimatedFraction); } } /** * @param context The Context for this view. * @param height The LayoutParams for this view. */ public ToolbarProgressBarAnimatingView(Context context, LayoutParams layoutParams) { super(context); setLayoutParams(layoutParams); mIsRtl = LocalizationUtils.isLayoutRtl(); mDpToPx = getResources().getDisplayMetrics().density; mAnimationDrawable = new ColorDrawable(Color.WHITE); setImageDrawable(mAnimationDrawable); mListener = new ProgressBarUpdateListener(); mAnimatorSet = new AnimatorSet(); mSlowAnimation = new ValueAnimator(); mSlowAnimation.setFloatValues(0.0f, 1.0f); mSlowAnimation.addUpdateListener(mListener); mFastAnimation = new ValueAnimator(); mFastAnimation.setFloatValues(0.0f, 1.0f); mFastAnimation.addUpdateListener(mListener); updateAnimationDuration(); mAnimatorSet.playSequentially(mSlowAnimation, mFastAnimation); AnimatorListener listener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator a) { // Replay the animation if it has not been canceled. if (mIsCanceled) return; // Repeats of the animation should have a start delay. mAnimatorSet.setStartDelay(ANIMATION_DELAY_MS); updateAnimationDuration(); // Only restart the animation if the last animation is ending. if (a == mFastAnimation) mAnimatorSet.start(); } }; mSlowAnimation.addListener(listener); mFastAnimation.addListener(listener); } /** * Update the duration of the animation based on the width of the progress bar. */ private void updateAnimationDuration() { // If progress is <= 0, the duration is also 0. if (mProgressWidth <= 0) return; // Total duration: logE(progress_dp) * 200 * 1.3 long totalDuration = (long) (Math.log(mProgressWidth / mDpToPx) / Math.log(Math.E)) * 260; if (totalDuration <= 0) return; mSlowAnimation.setDuration((long) (totalDuration * SLOW_ANIMATION_FRACTION)); mFastAnimation.setStartDelay((long) (totalDuration * FAST_ANIMATION_DELAY)); mFastAnimation.setDuration((long) (totalDuration * FAST_ANIMATION_FRACTION)); } /** * Start the animation if it hasn't been already. */ public void startAnimation() { mIsCanceled = false; if (!mAnimatorSet.isStarted()) { updateAnimationDuration(); // Set the initial start delay to 0ms so it starts immediately. mAnimatorSet.setStartDelay(0); // Reset position. setScaleX(0.0f); setTranslationX(0.0f); mAnimatorSet.start(); // Fade in to look nice on sites that trigger many loads that end quickly. animate().alpha(1.0f) .setDuration(500) .setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); } } /** * Update the animating view. * @param animator The current running animator. * @param animatedFraction The current fraction of completion for the animation. */ private void updateAnimation(ValueAnimator animator, float animatedFraction) { if (mIsCanceled) return; float bezierProgress = mBezier.getInterpolation(animatedFraction); // Left and right bound change based on if the layout is RTL. float leftBound = mIsRtl ? -mProgressWidth : 0.0f; float rightBound = mIsRtl ? 0.0f : mProgressWidth; float barScale = ANIMATING_BAR_SCALE; // If the current animation is the slow animation, the bar slowly expands from 20% of the // progress bar width to 30%. if (animator == mSlowAnimation && animatedFraction <= SMALL_BAR_EXPANSION_COMPLETE) { float sizeDiff = ANIMATING_BAR_SCALE - SMALL_ANIMATING_BAR_SCALE; // Since the bar will only expand for the first 60% of the animation, multiply the // animated fraction by 1/0.6 to get the fraction completed. float completeFraction = (animatedFraction / SMALL_BAR_EXPANSION_COMPLETE); barScale = SMALL_ANIMATING_BAR_SCALE + sizeDiff * completeFraction; } // Include the width of the animating bar in this computation so it comes from // off-screen. float animatingWidth = Math.min(ANIMATING_VIEW_MAX_WIDTH_DP * mDpToPx, mProgressWidth * barScale); float animatorCenter = ((mProgressWidth + animatingWidth) * bezierProgress) - animatingWidth / 2.0f; if (mIsRtl) animatorCenter *= -1.0f; // The left and right x-coordinate of the animating view. float animatorRight = animatorCenter + (animatingWidth / 2.0f); float animatorLeft = animatorCenter - (animatingWidth / 2.0f); // "Clip" the view so it doesn't go past where the progress bar starts or ends. if (animatorRight > rightBound) { animatingWidth -= Math.abs(animatorRight - rightBound); animatorCenter -= Math.abs(animatorRight - rightBound) / 2.0f; } else if (animatorLeft < leftBound) { animatingWidth -= Math.abs(animatorLeft - leftBound); animatorCenter += Math.abs(animatorLeft - leftBound) / 2.0f; } setScaleX(animatingWidth); setTranslationX(animatorCenter); } /** * @return True if the animation is running. */ public boolean isRunning() { return mAnimatorSet.isStarted(); } /** * Cancel the animation. */ public void cancelAnimation() { mIsCanceled = true; mAnimatorSet.cancel(); // Reset position and alpha. setScaleX(0.0f); setTranslationX(0.0f); animate().cancel(); setAlpha(0.0f); mLastAnimatedFraction = 0.0f; mProgressWidth = 0; } /** * Update info about the progress bar holding this animating block. * @param progressWidth The width of the contaiing progress bar. */ public void update(float progressWidth) { // Since the progress bar can become visible before current progress is sent, the width // needs to be updated even if the progess bar isn't showing. The result of not having // this is most noticable if you rotate the device on a slow page. mProgressWidth = progressWidth; updateAnimation(mLastUpdatedAnimation, mLastAnimatedFraction); } /** * @param color The Android color that the animating bar should be. */ public void setColor(int color) { mAnimationDrawable.setColor(color); } }