// Copyright 2015 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.TimeAnimator; import android.animation.TimeAnimator.TimeListener; import android.content.Context; import android.graphics.Color; import android.os.Build; import android.support.v4.view.ViewCompat; import android.text.TextUtils; import android.util.AttributeSet; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout.LayoutParams; import android.widget.ProgressBar; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.CommandLine; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.chrome.R; import org.chromium.chrome.browser.ChromeSwitches; import org.chromium.chrome.browser.util.ColorUtils; import org.chromium.components.variations.VariationsAssociatedData; import org.chromium.ui.UiUtils; import org.chromium.ui.interpolators.BakedBezierInterpolator; /** * Progress bar for use in the Toolbar view. */ public class ToolbarProgressBar extends ClipDrawableProgressBar { private static final String ANIMATION_FIELD_TRIAL_NAME = "ProgressBarAnimationAndroid"; private static final String PROGRESS_BAR_UPDATE_COUNT_HISTOGRAM = "Omnibox.ProgressBarUpdateCount"; private static final String PROGRESS_BAR_BREAK_POINT_UPDATE_COUNT_HISTOGRAM = "Omnibox.ProgressBarBreakPointUpdateCount"; /** * Interface for progress bar animation interpolation logics. */ interface AnimationLogic { /** * Resets internal data. It must be called on every loading start. * @param startProgress The progress for the animation to start at. This is used when the * animation logic switches. */ void reset(float startProgress); /** * Returns interpolated progress for animation. * * @param targetProgress Actual page loading progress. * @param frameTimeSec Duration since the last call. * @param resolution Resolution of the displayed progress bar. Mainly for rounding. */ float updateProgress(float targetProgress, float frameTimeSec, int resolution); } // The amount of time in ms that the progress bar has to be stopped before the indeterminate // animation starts. private static final long ANIMATION_START_THRESHOLD = 5000; private static final float THEMED_BACKGROUND_WHITE_FRACTION = 0.2f; private static final float THEMED_FOREGROUND_BLACK_FRACTION = 0.64f; private static final float ANIMATION_WHITE_FRACTION = 0.4f; private static final long PROGRESS_FRAME_TIME_CAP_MS = 50; private long mAlphaAnimationDurationMs = 140; private long mHidingDelayMs = 100; private boolean mIsStarted; private float mTargetProgress; private int mTargetProgressUpdateCount; private AnimationLogic mAnimationLogic; private boolean mAnimationInitialized; private int mMarginTop; private ViewGroup mControlContainer; private int mProgressStartCount; private int mThemeColor; /** Whether the smooth-indeterminate animation is running. */ private boolean mIsRunningSmoothIndeterminate; /** If the animation logic being used for the progress bar is smooth-indeterminate. */ private boolean mIsUsingSmoothIndeterminate; private ToolbarProgressBarAnimatingView mAnimatingView; private final Runnable mHideRunnable = new Runnable() { @Override public void run() { animateAlphaTo(0.0f); mIsRunningSmoothIndeterminate = false; if (mAnimatingView != null) mAnimatingView.cancelAnimation(); } }; private final Runnable mStartSmoothIndeterminate = new Runnable() { @Override public void run() { if (!mIsStarted) return; mIsRunningSmoothIndeterminate = true; mAnimationLogic.reset(getProgress()); mProgressAnimator.start(); int width = Math.abs(getDrawable().getBounds().right - getDrawable().getBounds().left); mAnimatingView.update(getProgress() * width); mAnimatingView.startAnimation(); } }; private final TimeAnimator mProgressAnimator = new TimeAnimator(); { mProgressAnimator.setTimeListener(new TimeListener() { @Override public void onTimeUpdate(TimeAnimator animation, long totalTimeMs, long deltaTimeMs) { // Cap progress bar animation frame time so that it doesn't jump too much even when // the animation is janky. float progress = mAnimationLogic.updateProgress(mTargetProgress, Math.min(deltaTimeMs, PROGRESS_FRAME_TIME_CAP_MS) * 0.001f, getWidth()); progress = Math.max(progress, 0); ToolbarProgressBar.super.setProgress(progress); if (mAnimatingView != null) { int width = Math.abs( getDrawable().getBounds().right - getDrawable().getBounds().left); mAnimatingView.update(progress * width); } if (getProgress() == mTargetProgress) { if (!mIsStarted) postOnAnimationDelayed(mHideRunnable, mHidingDelayMs); mProgressAnimator.end(); return; } } }); } /** * Creates a toolbar progress bar. * * @param context the application environment. * @param attrs the xml attributes that should be used to initialize this view. */ public ToolbarProgressBar(Context context, AttributeSet attrs) { super(context, attrs); setAlpha(0.0f); // This tells accessibility services that progress bar changes are important enough to // announce to the user even when not focused. ViewCompat.setAccessibilityLiveRegion(this, ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); } /** * Prepare the progress bar for being attached to the window. * @param toolbarHeight The height of the toolbar. */ public void prepareForAttach(int toolbarHeight) { LayoutParams curParams = new LayoutParams(getLayoutParams()); mMarginTop = toolbarHeight - curParams.height; curParams.topMargin = mMarginTop; setLayoutParams(curParams); } /** * @param container This View's container. */ public void setControlContainer(ViewGroup container) { mControlContainer = container; } @Override public void setAlpha(float alpha) { super.setAlpha(alpha); if (mAnimatingView != null) mAnimatingView.setAlpha(alpha); } @Override public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldWidth, oldHeight); // If the size changed, the animation width needs to be manually updated. if (mAnimatingView != null) mAnimatingView.update(width * getProgress()); } /** * Initializes animation based on command line configuration. This must be called when native * library is ready. */ public void initializeAnimation() { if (mAnimationInitialized) return; mAnimationInitialized = true; assert mAnimationLogic == null; String animation = CommandLine.getInstance().getSwitchValue( ChromeSwitches.PROGRESS_BAR_ANIMATION); if (TextUtils.isEmpty(animation)) { animation = VariationsAssociatedData.getVariationParamValue( ANIMATION_FIELD_TRIAL_NAME, ChromeSwitches.PROGRESS_BAR_ANIMATION); } if (TextUtils.equals(animation, "smooth")) { mAnimationLogic = new ProgressAnimationSmooth(); } else if (TextUtils.equals(animation, "smooth-indeterminate") && Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { mAnimationLogic = new ProgressAnimationSmooth(); // The smooth-indeterminate will start running only after 5 seconds has passed with no // progress update. Until then, the default behavior will be used. mIsUsingSmoothIndeterminate = true; LayoutParams animationParams = new LayoutParams(getLayoutParams()); animationParams.width = 1; animationParams.topMargin = mMarginTop; mAnimatingView = new ToolbarProgressBarAnimatingView(getContext(), animationParams); // The primary theme color may not have been set. if (mThemeColor != 0) { setThemeColor(mThemeColor, false); } else { setForegroundColor(getForegroundColor()); } UiUtils.insertAfter(mControlContainer, mAnimatingView, this); } else if (TextUtils.equals(animation, "fast-start")) { mAnimationLogic = new ProgressAnimationFastStart(); } else if (TextUtils.equals(animation, "linear")) { mAnimationLogic = new ProgressAnimationLinear(); } else { assert TextUtils.isEmpty(animation) || TextUtils.equals(animation, "disabled"); } } /** * Start showing progress bar animation. */ public void start() { mIsStarted = true; mProgressStartCount++; if (mIsUsingSmoothIndeterminate) { removeCallbacks(mStartSmoothIndeterminate); postDelayed(mStartSmoothIndeterminate, ANIMATION_START_THRESHOLD); } mIsRunningSmoothIndeterminate = false; mTargetProgressUpdateCount = 0; resetProgressUpdateCount(); super.setProgress(0.0f); if (mAnimationLogic != null) mAnimationLogic.reset(0.0f); removeCallbacks(mHideRunnable); animateAlphaTo(1.0f); } /** * @return True if the progress bar is showing and started. */ public boolean isStarted() { return mIsStarted; } /** * Start hiding progress bar animation. * @param delayed Whether a delayed fading out animation should be posted. */ public void finish(boolean delayed) { mIsStarted = false; if (delayed) { updateVisibleProgress(); RecordHistogram.recordCount1000Histogram(PROGRESS_BAR_UPDATE_COUNT_HISTOGRAM, getProgressUpdateCount()); RecordHistogram.recordCount100Histogram( PROGRESS_BAR_BREAK_POINT_UPDATE_COUNT_HISTOGRAM, mTargetProgressUpdateCount); } else { removeCallbacks(mHideRunnable); animate().cancel(); if (mAnimatingView != null) { removeCallbacks(mStartSmoothIndeterminate); mAnimatingView.cancelAnimation(); mTargetProgress = 0; } mIsRunningSmoothIndeterminate = false; setAlpha(0.0f); } } /** * Set alpha show&hide animation duration. This is for faster testing. * @param alphaAnimationDurationMs Alpha animation duration in milliseconds. */ @VisibleForTesting public void setAlphaAnimationDuration(long alphaAnimationDurationMs) { mAlphaAnimationDurationMs = alphaAnimationDurationMs; } /** * Set hiding delay duration. This is for faster testing. * @param hidngDelayMs Hiding delay duration in milliseconds. */ @VisibleForTesting public void setHidingDelay(long hidngDelayMs) { mHidingDelayMs = hidngDelayMs; } /** * @return The number of times the progress bar has been triggered. */ @VisibleForTesting public int getStartCountForTesting() { return mProgressStartCount; } /** * Reset the number of times the progress bar has been triggered. */ @VisibleForTesting public void resetStartCountForTesting() { mProgressStartCount = 0; } private void animateAlphaTo(float targetAlpha) { float alphaDiff = targetAlpha - getAlpha(); if (alphaDiff == 0.0f) return; long duration = (long) Math.abs(alphaDiff * mAlphaAnimationDurationMs); BakedBezierInterpolator interpolator = BakedBezierInterpolator.FADE_IN_CURVE; if (alphaDiff < 0) interpolator = BakedBezierInterpolator.FADE_OUT_CURVE; animate().alpha(targetAlpha) .setDuration(duration) .setInterpolator(interpolator); if (mAnimatingView != null) { mAnimatingView.animate().alpha(targetAlpha) .setDuration(duration) .setInterpolator(interpolator); } } private void updateVisibleProgress() { if (mAnimationLogic == null || (mIsUsingSmoothIndeterminate && !mIsRunningSmoothIndeterminate)) { super.setProgress(mTargetProgress); if (!mIsStarted) postOnAnimationDelayed(mHideRunnable, mHidingDelayMs); } else if (!mProgressAnimator.isStarted()) { mProgressAnimator.start(); } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } // ClipDrawableProgressBar implementation. @Override public void setProgress(float progress) { if (!mIsStarted || mTargetProgress == progress) return; if (mIsUsingSmoothIndeterminate) { // If the progress bar was updated, reset the callback that triggers the // smooth-indeterminate animation. removeCallbacks(mStartSmoothIndeterminate); if (progress == 1.0) { if (mAnimatingView != null) mAnimatingView.cancelAnimation(); } else if (mAnimatingView != null && !mAnimatingView.isRunning()) { postDelayed(mStartSmoothIndeterminate, ANIMATION_START_THRESHOLD); } } mTargetProgressUpdateCount += 1; mTargetProgress = progress; updateVisibleProgress(); } @Override public void setVisibility(int visibility) { super.setVisibility(visibility); if (mAnimatingView != null) mAnimatingView.setVisibility(visibility); } /** * Color the progress bar based on the toolbar theme color. * @param color The Android color the toolbar is using. */ public void setThemeColor(int color, boolean isIncognito) { mThemeColor = color; // The default toolbar has specific colors to use. if ((ColorUtils.isUsingDefaultToolbarColor(getResources(), color) || !ColorUtils.isValidThemeColor(color)) && !isIncognito) { setForegroundColor(ApiCompatibilityUtils.getColor(getResources(), R.color.progress_bar_foreground)); setBackgroundColor(ApiCompatibilityUtils.getColor(getResources(), R.color.progress_bar_background)); return; } setForegroundColor(ColorUtils.getThemedAssetColor(color, isIncognito)); if (mAnimatingView != null && (ColorUtils.shouldUseLightForegroundOnBackground(color) || isIncognito)) { mAnimatingView.setColor(ColorUtils.getColorWithOverlay(color, Color.WHITE, ANIMATION_WHITE_FRACTION)); } setBackgroundColor(ColorUtils.getColorWithOverlay(color, Color.WHITE, THEMED_BACKGROUND_WHITE_FRACTION)); } @Override public void setForegroundColor(int color) { super.setForegroundColor(color); if (mAnimatingView != null) { mAnimatingView.setColor(ColorUtils.getColorWithOverlay(color, Color.WHITE, ANIMATION_WHITE_FRACTION)); } } @Override public CharSequence getAccessibilityClassName() { return ProgressBar.class.getName(); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setCurrentItemIndex((int) (mTargetProgress * 100)); event.setItemCount(100); } }