// 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.content.Context;
import android.util.AttributeSet;
import android.widget.ProgressBar;
import org.chromium.base.ObserverList;
import org.chromium.base.ObserverList.RewindableIterator;
/**
* A progress bar that smoothly animates incremental updates.
* <p>
* Consumers of this class need to be aware that calls to {@link #getProgress()} will return
* the currently visible progress value and not the one set in the last call to
* {@link #setProgress(int)}.
*/
public class SmoothProgressBar extends ProgressBar {
/**
* Allows observation of visual changes to the progress bar.
*/
public interface ProgressChangeListener {
/**
* Triggered when the visible progress has changed.
* @param progress The current progress value.
*/
void onProgressChanged(int progress);
/**
* Triggered when the visibility of the progress bar has changed.
* @param visibility The visibility of the progress bar.
*/
void onProgressVisibilityChanged(int visibility);
}
private static final int MAX = 100;
// The amount of time between subsequent progress updates. 16ms is chosen to make 60fps.
private static final long PROGRESS_UPDATE_DELAY_MS = 16;
private final ObserverList<ProgressChangeListener> mObservers;
private final RewindableIterator<ProgressChangeListener> mObserversIterator;
private boolean mIsAnimated = false;
private int mTargetProgress;
// Since the progress bar is being animated, the internal progress bar resolution should be
// at least fine as the width, not MAX. This multiplier will be applied to input progress
// to convert to a finer scale.
private int mResolutionMutiplier = 1;
private Runnable mUpdateProgressRunnable = new Runnable() {
@Override
public void run() {
if (getProgress() == mTargetProgress) return;
if (!mIsAnimated) {
setProgressInternal(mTargetProgress);
return;
}
// Every time, the progress bar get's at least 20% closer to mTargetProcess.
// Add 3 to guarantee progressing even if they only differ by 1.
setProgressInternal(getProgress() + (mTargetProgress - getProgress() + 3) / 4);
postOnAnimationDelayed(this, PROGRESS_UPDATE_DELAY_MS);
}
};
/**
* Create a new progress bar with range 0...100 and initial progress of 0.
* @param context the application environment.
* @param attrs the xml attributes that should be used to initialize this view.
*/
public SmoothProgressBar(Context context, AttributeSet attrs) {
super(context, attrs);
setMax(MAX * mResolutionMutiplier);
mObservers = new ObserverList<ProgressChangeListener>();
mObserversIterator = mObservers.rewindableIterator();
}
/**
* Adds an observer to be notified of progress changes.
* @param observer The observer to be added.
*/
public void addProgressChangeListener(ProgressChangeListener observer) {
mObservers.addObserver(observer);
}
/**
* Removes an observer to be notified of progress changes.
* @param observer The observer to be removed.
*/
public void removeProgressChangeListener(ProgressChangeListener observer) {
mObservers.removeObserver(observer);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
int normalizedProgress = getProgress() / mResolutionMutiplier;
// Choose an integer resolution multiplier that makes the scale at least fine as the width.
mResolutionMutiplier = Math.max(1, (w + MAX - 1) / MAX);
setMax(mResolutionMutiplier * MAX);
setProgressInternal(normalizedProgress * mResolutionMutiplier);
}
@Override
public void setProgress(int progress) {
final int targetProgress = progress * mResolutionMutiplier;
if (mTargetProgress == targetProgress) return;
mTargetProgress = targetProgress;
removeCallbacks(mUpdateProgressRunnable);
postOnAnimation(mUpdateProgressRunnable);
}
/**
* Sets whether to animate incremental updates or not.
* @param isAnimated True if it is needed to animate incremental updates.
*/
public void setAnimated(boolean isAnimated) {
mIsAnimated = isAnimated;
}
/**
* Called to update the progress visuals.
* @param progress The progress value to set the visuals to.
*/
protected void setProgressInternal(int progress) {
super.setProgress(progress);
if (mObserversIterator != null) {
for (mObserversIterator.rewind(); mObserversIterator.hasNext();) {
mObserversIterator.next().onProgressChanged(progress);
}
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (mObserversIterator != null) {
for (mObserversIterator.rewind(); mObserversIterator.hasNext();) {
mObserversIterator.next().onProgressVisibilityChanged(visibility);
}
}
}
}