1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 package org.mozilla.gecko.widget; 7 8 import android.animation.Animator; 9 import android.animation.ValueAnimator; 10 import android.annotation.TargetApi; 11 import android.content.Context; 12 import android.content.res.TypedArray; 13 import android.graphics.Canvas; 14 import android.graphics.Rect; 15 import android.graphics.drawable.Drawable; 16 import android.os.Build; 17 import android.os.Handler; 18 import android.support.annotation.InterpolatorRes; 19 import android.support.annotation.NonNull; 20 import android.support.annotation.Nullable; 21 import android.support.v4.view.ViewCompat; 22 import android.util.AttributeSet; 23 import android.view.animation.AnimationUtils; 24 import android.view.animation.Interpolator; 25 import android.view.animation.LinearInterpolator; 26 27 import org.mozilla.gecko.DynamicToolbar; 28 import org.mozilla.gecko.R; 29 import org.mozilla.gecko.drawable.ShiftDrawable; 30 import org.mozilla.gecko.gfx.DynamicToolbarAnimator; 31 import org.mozilla.gecko.widget.themed.ThemedProgressBar; 32 33 /** 34 * A progressbar with some animations on changing progress. 35 * When changing progress of this bar, it does not change value directly. Instead, it use 36 * {@link Animator} to change value progressively. Moreover, change visibility to View.GONE will 37 * cause closing animation. 38 */ 39 public class AnimatedProgressBar extends ThemedProgressBar { 40 41 /** 42 * Animation duration of progress changing. 43 */ 44 private final static int PROGRESS_DURATION = 200; 45 46 /** 47 * Delay before applying closing animation when progress reach max value. 48 */ 49 private final static int CLOSING_DELAY = 300; 50 51 /** 52 * Animation duration for closing 53 */ 54 private final static int CLOSING_DURATION = 300; 55 56 private ValueAnimator mPrimaryAnimator; 57 private final ValueAnimator mClosingAnimator = ValueAnimator.ofFloat(0f, 1f); 58 59 /** 60 * For closing animation. To indicate how many visible region should be clipped. 61 */ 62 private float mClipRatio = 0f; 63 private final Rect mRect = new Rect(); 64 65 /** 66 * To store the final expected progress to reach, it does matter in animation. 67 */ 68 private int mExpectedProgress = 0; 69 70 /** 71 * setProgress() might be invoked in constructor. Add to flag to avoid null checking for animators. 72 */ 73 private boolean mInitialized = false; 74 75 private boolean mIsRtl = false; 76 77 private DynamicToolbar mDynamicToolbar; 78 private EndingRunner mEndingRunner = new EndingRunner(); 79 80 private final ValueAnimator.AnimatorUpdateListener mListener = 81 new ValueAnimator.AnimatorUpdateListener() { 82 @Override 83 public void onAnimationUpdate(ValueAnimator animation) { 84 setProgressImmediately((int) mPrimaryAnimator.getAnimatedValue()); 85 } 86 }; 87 AnimatedProgressBar(@onNull Context context)88 public AnimatedProgressBar(@NonNull Context context) { 89 super(context, null); 90 init(context, null); 91 } 92 AnimatedProgressBar(@onNull Context context, @Nullable AttributeSet attrs)93 public AnimatedProgressBar(@NonNull Context context, 94 @Nullable AttributeSet attrs) { 95 super(context, attrs); 96 init(context, attrs); 97 } 98 AnimatedProgressBar(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)99 public AnimatedProgressBar(@NonNull Context context, 100 @Nullable AttributeSet attrs, 101 int defStyleAttr) { 102 super(context, attrs, defStyleAttr); 103 init(context, attrs); 104 } 105 106 @TargetApi(Build.VERSION_CODES.LOLLIPOP) AnimatedProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)107 public AnimatedProgressBar(Context context, 108 AttributeSet attrs, 109 int defStyleAttr, 110 int defStyleRes) { 111 super(context, attrs, defStyleAttr); 112 init(context, attrs); 113 } 114 115 /** 116 * {@inheritDoc} 117 */ 118 @Override setMax(int max)119 public synchronized void setMax(int max) { 120 super.setMax(max); 121 mPrimaryAnimator = createAnimator(getMax(), mListener); 122 } 123 124 /** 125 * {@inheritDoc} 126 * <p> 127 * Instead of set progress directly, this method triggers an animator to change progress. 128 */ 129 @Override setProgress(int nextProgress)130 public void setProgress(int nextProgress) { 131 nextProgress = Math.min(nextProgress, getMax()); 132 nextProgress = Math.max(0, nextProgress); 133 mExpectedProgress = nextProgress; 134 if (!mInitialized) { 135 setProgressImmediately(mExpectedProgress); 136 return; 137 } 138 139 // if regress, jump to the expected value without any animation 140 if (mExpectedProgress < getProgress()) { 141 cancelAnimations(); 142 setProgressImmediately(mExpectedProgress); 143 return; 144 } 145 146 // Animation is not needed for reloading a completed page 147 if ((mExpectedProgress == 0) && (getProgress() == getMax())) { 148 cancelAnimations(); 149 setProgressImmediately(0); 150 return; 151 } 152 153 cancelAnimations(); 154 mPrimaryAnimator.setIntValues(getProgress(), nextProgress); 155 mPrimaryAnimator.start(); 156 } 157 158 @Override onDraw(Canvas canvas)159 public void onDraw(Canvas canvas) { 160 if (mClipRatio == 0) { 161 super.onDraw(canvas); 162 } else { 163 canvas.getClipBounds(mRect); 164 final float clipWidth = mRect.width() * mClipRatio; 165 final int saveCount = canvas.save(); 166 if (mIsRtl) { 167 canvas.clipRect(mRect.left, mRect.top, mRect.right - clipWidth, mRect.bottom); 168 } else { 169 canvas.clipRect(mRect.left + clipWidth, mRect.top, mRect.right, mRect.bottom); 170 } 171 super.onDraw(canvas); 172 canvas.restoreToCount(saveCount); 173 } 174 } 175 176 /** 177 * {@inheritDoc} 178 * <p> 179 * Instead of change visibility directly, this method also applies the closing animation if 180 * progress reaches max value. 181 */ 182 @Override setVisibility(int value)183 public void setVisibility(int value) { 184 // nothing changed 185 if (getVisibility() == value) { 186 return; 187 } 188 189 if (value == GONE) { 190 if (mExpectedProgress == getMax()) { 191 setProgressImmediately(mExpectedProgress); 192 animateClosing(); 193 } else { 194 setVisibilityImmediately(value); 195 } 196 } else { 197 final Handler handler = getHandler(); 198 // if this view is detached from window, the handler would be null 199 if (handler != null) { 200 handler.removeCallbacks(mEndingRunner); 201 } 202 203 if (mClosingAnimator != null) { 204 mClipRatio = 0; 205 mClosingAnimator.cancel(); 206 } 207 setVisibilityImmediately(value); 208 } 209 } 210 211 @Override onAttachedToWindow()212 public void onAttachedToWindow() { 213 super.onAttachedToWindow(); 214 mIsRtl = (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL); 215 } 216 setDynamicToolbar(@ullable DynamicToolbar toolbar)217 public void setDynamicToolbar(@Nullable DynamicToolbar toolbar) { 218 mDynamicToolbar = toolbar; 219 } 220 pinDynamicToolbar()221 public void pinDynamicToolbar() { 222 if (mDynamicToolbar == null) { 223 return; 224 } 225 if (mDynamicToolbar.isEnabled()) { 226 mDynamicToolbar.setPinned(true, DynamicToolbarAnimator.PinReason.PAGE_LOADING); 227 mDynamicToolbar.setVisible(true, DynamicToolbar.VisibilityTransition.ANIMATE); 228 } 229 } 230 unpinDynamicToolbar()231 public void unpinDynamicToolbar() { 232 if (mDynamicToolbar == null) { 233 return; 234 } 235 if (mDynamicToolbar.isEnabled()) { 236 mDynamicToolbar.setPinned(false, DynamicToolbarAnimator.PinReason.PAGE_LOADING); 237 } 238 } 239 cancelAnimations()240 private void cancelAnimations() { 241 if (mPrimaryAnimator != null) { 242 mPrimaryAnimator.cancel(); 243 } 244 if (mClosingAnimator != null) { 245 mClosingAnimator.cancel(); 246 } 247 248 mClipRatio = 0; 249 } 250 init(@onNull Context context, @Nullable AttributeSet attrs)251 private void init(@NonNull Context context, @Nullable AttributeSet attrs) { 252 mInitialized = true; 253 254 final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AnimatedProgressBar); 255 final int duration = a.getInteger(R.styleable.AnimatedProgressBar_shiftDuration, 1000); 256 final boolean wrap = a.getBoolean(R.styleable.AnimatedProgressBar_wrapShiftDrawable, false); 257 @InterpolatorRes final int itplId = a.getResourceId(R.styleable.AnimatedProgressBar_shiftInterpolator, 0); 258 a.recycle(); 259 260 setProgressDrawable(buildDrawable(getProgressDrawable(), wrap, duration, itplId)); 261 262 mPrimaryAnimator = createAnimator(getMax(), mListener); 263 264 mClosingAnimator.setDuration(CLOSING_DURATION); 265 mClosingAnimator.setInterpolator(new LinearInterpolator()); 266 mClosingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 267 @Override 268 public void onAnimationUpdate(ValueAnimator valueAnimator) { 269 final float ratio = (float) valueAnimator.getAnimatedValue(); 270 if (mClipRatio != ratio) { 271 mClipRatio = ratio; 272 invalidate(); 273 } 274 } 275 }); 276 mClosingAnimator.addListener(new Animator.AnimatorListener() { 277 @Override 278 public void onAnimationStart(Animator animator) { 279 mClipRatio = 0f; 280 } 281 282 @Override 283 public void onAnimationEnd(Animator animator) { 284 setVisibilityImmediately(GONE); 285 } 286 287 @Override 288 public void onAnimationCancel(Animator animator) { 289 mClipRatio = 0f; 290 } 291 292 @Override 293 public void onAnimationRepeat(Animator animator) { 294 } 295 }); 296 } 297 setVisibilityImmediately(int value)298 private void setVisibilityImmediately(int value) { 299 super.setVisibility(value); 300 } 301 animateClosing()302 private void animateClosing() { 303 mClosingAnimator.cancel(); 304 final Handler handler = getHandler(); 305 // if this view is detached from window, the handler would be null 306 if (handler != null) { 307 handler.removeCallbacks(mEndingRunner); 308 handler.postDelayed(mEndingRunner, CLOSING_DELAY); 309 } 310 } 311 setProgressImmediately(int progress)312 private void setProgressImmediately(int progress) { 313 super.setProgress(progress); 314 } 315 buildDrawable(@onNull Drawable original, boolean isWrap, int duration, @InterpolatorRes int itplId)316 private Drawable buildDrawable(@NonNull Drawable original, 317 boolean isWrap, 318 int duration, 319 @InterpolatorRes int itplId) { 320 if (isWrap) { 321 final Interpolator interpolator = (itplId > 0) 322 ? AnimationUtils.loadInterpolator(getContext(), itplId) 323 : null; 324 return new ShiftDrawable(original, duration, interpolator); 325 } else { 326 return original; 327 } 328 } 329 createAnimator(int max, ValueAnimator.AnimatorUpdateListener listener)330 private static ValueAnimator createAnimator(int max, ValueAnimator.AnimatorUpdateListener listener) { 331 ValueAnimator animator = ValueAnimator.ofInt(0, max); 332 animator.setInterpolator(new LinearInterpolator()); 333 animator.setDuration(PROGRESS_DURATION); 334 animator.addUpdateListener(listener); 335 return animator; 336 } 337 338 private class EndingRunner implements Runnable { 339 @Override run()340 public void run() { 341 mClosingAnimator.start(); 342 } 343 } 344 } 345