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