1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.recyclerview.widget;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ValueAnimator;
22 import android.animation.ValueAnimator.AnimatorUpdateListener;
23 import android.graphics.Canvas;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.StateListDrawable;
26 import android.view.MotionEvent;
27 import android.view.View;
28 
29 import androidx.annotation.IntDef;
30 import androidx.annotation.VisibleForTesting;
31 
32 import org.jspecify.annotations.NonNull;
33 import org.jspecify.annotations.Nullable;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 
38 /**
39  * Class responsible to animate and provide a fast scroller.
40  */
41 class FastScroller extends RecyclerView.ItemDecoration implements RecyclerView.OnItemTouchListener {
42     @IntDef({STATE_HIDDEN, STATE_VISIBLE, STATE_DRAGGING})
43     @Retention(RetentionPolicy.SOURCE)
44     private @interface State { }
45     // Scroll thumb not showing
46     private static final int STATE_HIDDEN = 0;
47     // Scroll thumb visible and moving along with the scrollbar
48     private static final int STATE_VISIBLE = 1;
49     // Scroll thumb being dragged by user
50     private static final int STATE_DRAGGING = 2;
51 
52     @IntDef({DRAG_X, DRAG_Y, DRAG_NONE})
53     @Retention(RetentionPolicy.SOURCE)
54     private @interface DragState{ }
55     private static final int DRAG_NONE = 0;
56     private static final int DRAG_X = 1;
57     private static final int DRAG_Y = 2;
58 
59     @IntDef({ANIMATION_STATE_OUT, ANIMATION_STATE_FADING_IN, ANIMATION_STATE_IN,
60         ANIMATION_STATE_FADING_OUT})
61     @Retention(RetentionPolicy.SOURCE)
62     private @interface AnimationState { }
63     private static final int ANIMATION_STATE_OUT = 0;
64     private static final int ANIMATION_STATE_FADING_IN = 1;
65     private static final int ANIMATION_STATE_IN = 2;
66     private static final int ANIMATION_STATE_FADING_OUT = 3;
67 
68     private static final int SHOW_DURATION_MS = 500;
69     private static final int HIDE_DELAY_AFTER_VISIBLE_MS = 1500;
70     private static final int HIDE_DELAY_AFTER_DRAGGING_MS = 1200;
71     private static final int HIDE_DURATION_MS = 500;
72     private static final int SCROLLBAR_FULL_OPAQUE = 255;
73 
74     private static final int[] PRESSED_STATE_SET = new int[]{android.R.attr.state_pressed};
75     private static final int[] EMPTY_STATE_SET = new int[]{};
76 
77     private final int mScrollbarMinimumRange;
78     private final int mMargin;
79 
80     // Final values for the vertical scroll bar
81     @SuppressWarnings("WeakerAccess") /* synthetic access */
82     final StateListDrawable mVerticalThumbDrawable;
83     @SuppressWarnings("WeakerAccess") /* synthetic access */
84     final Drawable mVerticalTrackDrawable;
85     private final int mVerticalThumbWidth;
86     private final int mVerticalTrackWidth;
87 
88     // Final values for the horizontal scroll bar
89     private final StateListDrawable mHorizontalThumbDrawable;
90     private final Drawable mHorizontalTrackDrawable;
91     private final int mHorizontalThumbHeight;
92     private final int mHorizontalTrackHeight;
93 
94     // Dynamic values for the vertical scroll bar
95     @VisibleForTesting int mVerticalThumbHeight;
96     @VisibleForTesting int mVerticalThumbCenterY;
97     @VisibleForTesting float mVerticalDragY;
98 
99     // Dynamic values for the horizontal scroll bar
100     @VisibleForTesting int mHorizontalThumbWidth;
101     @VisibleForTesting int mHorizontalThumbCenterX;
102     @VisibleForTesting float mHorizontalDragX;
103 
104     private int mRecyclerViewWidth = 0;
105     private int mRecyclerViewHeight = 0;
106 
107     private RecyclerView mRecyclerView;
108     /**
109      * Whether the document is long/wide enough to require scrolling. If not, we don't show the
110      * relevant scroller.
111      */
112     private boolean mNeedVerticalScrollbar = false;
113     private boolean mNeedHorizontalScrollbar = false;
114     @State private int mState = STATE_HIDDEN;
115     @DragState private int mDragState = DRAG_NONE;
116 
117     private final int[] mVerticalRange = new int[2];
118     private final int[] mHorizontalRange = new int[2];
119     @SuppressWarnings("WeakerAccess") /* synthetic access */
120     final ValueAnimator mShowHideAnimator = ValueAnimator.ofFloat(0, 1);
121     @SuppressWarnings("WeakerAccess") /* synthetic access */
122     @AnimationState int mAnimationState = ANIMATION_STATE_OUT;
123     private final Runnable mHideRunnable = new Runnable() {
124         @Override
125         public void run() {
126             hide(HIDE_DURATION_MS);
127         }
128     };
129     private final RecyclerView.OnScrollListener
130             mOnScrollListener = new RecyclerView.OnScrollListener() {
131         @Override
132         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
133             updateScrollPosition(recyclerView.computeHorizontalScrollOffset(),
134                     recyclerView.computeVerticalScrollOffset());
135         }
136     };
137 
FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable, Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable, Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange, int margin)138     FastScroller(RecyclerView recyclerView, StateListDrawable verticalThumbDrawable,
139             Drawable verticalTrackDrawable, StateListDrawable horizontalThumbDrawable,
140             Drawable horizontalTrackDrawable, int defaultWidth, int scrollbarMinimumRange,
141             int margin) {
142         mVerticalThumbDrawable = verticalThumbDrawable;
143         mVerticalTrackDrawable = verticalTrackDrawable;
144         mHorizontalThumbDrawable = horizontalThumbDrawable;
145         mHorizontalTrackDrawable = horizontalTrackDrawable;
146         mVerticalThumbWidth = Math.max(defaultWidth, verticalThumbDrawable.getIntrinsicWidth());
147         mVerticalTrackWidth = Math.max(defaultWidth, verticalTrackDrawable.getIntrinsicWidth());
148         mHorizontalThumbHeight = Math
149             .max(defaultWidth, horizontalThumbDrawable.getIntrinsicWidth());
150         mHorizontalTrackHeight = Math
151             .max(defaultWidth, horizontalTrackDrawable.getIntrinsicWidth());
152         mScrollbarMinimumRange = scrollbarMinimumRange;
153         mMargin = margin;
154         mVerticalThumbDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE);
155         mVerticalTrackDrawable.setAlpha(SCROLLBAR_FULL_OPAQUE);
156 
157         mShowHideAnimator.addListener(new AnimatorListener());
158         mShowHideAnimator.addUpdateListener(new AnimatorUpdater());
159 
160         attachToRecyclerView(recyclerView);
161     }
162 
attachToRecyclerView(@ullable RecyclerView recyclerView)163     public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
164         if (mRecyclerView == recyclerView) {
165             return; // nothing to do
166         }
167         if (mRecyclerView != null) {
168             destroyCallbacks();
169         }
170         mRecyclerView = recyclerView;
171         if (mRecyclerView != null) {
172             setupCallbacks();
173         }
174     }
175 
setupCallbacks()176     private void setupCallbacks() {
177         mRecyclerView.addItemDecoration(this);
178         mRecyclerView.addOnItemTouchListener(this);
179         mRecyclerView.addOnScrollListener(mOnScrollListener);
180     }
181 
destroyCallbacks()182     private void destroyCallbacks() {
183         mRecyclerView.removeItemDecoration(this);
184         mRecyclerView.removeOnItemTouchListener(this);
185         mRecyclerView.removeOnScrollListener(mOnScrollListener);
186         cancelHide();
187     }
188 
189     @SuppressWarnings("WeakerAccess") /* synthetic access */
requestRedraw()190     void requestRedraw() {
191         mRecyclerView.invalidate();
192     }
193 
setState(@tate int state)194     void setState(@State int state) {
195         if (state == STATE_DRAGGING && mState != STATE_DRAGGING) {
196             mVerticalThumbDrawable.setState(PRESSED_STATE_SET);
197             cancelHide();
198         }
199 
200         if (state == STATE_HIDDEN) {
201             requestRedraw();
202         } else {
203             show();
204         }
205 
206         if (mState == STATE_DRAGGING && state != STATE_DRAGGING) {
207             mVerticalThumbDrawable.setState(EMPTY_STATE_SET);
208             resetHideDelay(HIDE_DELAY_AFTER_DRAGGING_MS);
209         } else if (state == STATE_VISIBLE) {
210             resetHideDelay(HIDE_DELAY_AFTER_VISIBLE_MS);
211         }
212         mState = state;
213     }
214 
isLayoutRTL()215     private boolean isLayoutRTL() {
216         return mRecyclerView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
217     }
218 
isDragging()219     public boolean isDragging() {
220         return mState == STATE_DRAGGING;
221     }
222 
isVisible()223     @VisibleForTesting boolean isVisible() {
224         return mState == STATE_VISIBLE;
225     }
226 
show()227     public void show() {
228         switch (mAnimationState) {
229             case ANIMATION_STATE_FADING_OUT:
230                 mShowHideAnimator.cancel();
231                 // fall through
232             case ANIMATION_STATE_OUT:
233                 mAnimationState = ANIMATION_STATE_FADING_IN;
234                 mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 1);
235                 mShowHideAnimator.setDuration(SHOW_DURATION_MS);
236                 mShowHideAnimator.setStartDelay(0);
237                 mShowHideAnimator.start();
238                 break;
239         }
240     }
241 
242     @VisibleForTesting
hide(int duration)243     void hide(int duration) {
244         switch (mAnimationState) {
245             case ANIMATION_STATE_FADING_IN:
246                 mShowHideAnimator.cancel();
247                 // fall through
248             case ANIMATION_STATE_IN:
249                 mAnimationState = ANIMATION_STATE_FADING_OUT;
250                 mShowHideAnimator.setFloatValues((float) mShowHideAnimator.getAnimatedValue(), 0);
251                 mShowHideAnimator.setDuration(duration);
252                 mShowHideAnimator.start();
253                 break;
254         }
255     }
256 
cancelHide()257     private void cancelHide() {
258         mRecyclerView.removeCallbacks(mHideRunnable);
259     }
260 
resetHideDelay(int delay)261     private void resetHideDelay(int delay) {
262         cancelHide();
263         mRecyclerView.postDelayed(mHideRunnable, delay);
264     }
265 
266     @Override
onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state)267     public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
268         if (mRecyclerViewWidth != mRecyclerView.getWidth()
269                 || mRecyclerViewHeight != mRecyclerView.getHeight()) {
270             mRecyclerViewWidth = mRecyclerView.getWidth();
271             mRecyclerViewHeight = mRecyclerView.getHeight();
272             // This is due to the different events ordering when keyboard is opened or
273             // retracted vs rotate. Hence to avoid corner cases we just disable the
274             // scroller when size changed, and wait until the scroll position is recomputed
275             // before showing it back.
276             setState(STATE_HIDDEN);
277             return;
278         }
279 
280         if (mAnimationState != ANIMATION_STATE_OUT) {
281             if (mNeedVerticalScrollbar) {
282                 drawVerticalScrollbar(canvas);
283             }
284             if (mNeedHorizontalScrollbar) {
285                 drawHorizontalScrollbar(canvas);
286             }
287         }
288     }
289 
drawVerticalScrollbar(Canvas canvas)290     private void drawVerticalScrollbar(Canvas canvas) {
291         int viewWidth = mRecyclerViewWidth;
292 
293         int left = viewWidth - mVerticalThumbWidth;
294         int top = mVerticalThumbCenterY - mVerticalThumbHeight / 2;
295         mVerticalThumbDrawable.setBounds(0, 0, mVerticalThumbWidth, mVerticalThumbHeight);
296         mVerticalTrackDrawable
297             .setBounds(0, 0, mVerticalTrackWidth, mRecyclerViewHeight);
298 
299         if (isLayoutRTL()) {
300             mVerticalTrackDrawable.draw(canvas);
301             canvas.translate(mVerticalThumbWidth, top);
302             canvas.scale(-1, 1);
303             mVerticalThumbDrawable.draw(canvas);
304             canvas.scale(-1, 1);
305             canvas.translate(-mVerticalThumbWidth, -top);
306         } else {
307             canvas.translate(left, 0);
308             mVerticalTrackDrawable.draw(canvas);
309             canvas.translate(0, top);
310             mVerticalThumbDrawable.draw(canvas);
311             canvas.translate(-left, -top);
312         }
313     }
314 
drawHorizontalScrollbar(Canvas canvas)315     private void drawHorizontalScrollbar(Canvas canvas) {
316         int viewHeight = mRecyclerViewHeight;
317 
318         int top = viewHeight - mHorizontalThumbHeight;
319         int left = mHorizontalThumbCenterX - mHorizontalThumbWidth / 2;
320         mHorizontalThumbDrawable.setBounds(0, 0, mHorizontalThumbWidth, mHorizontalThumbHeight);
321         mHorizontalTrackDrawable
322             .setBounds(0, 0, mRecyclerViewWidth, mHorizontalTrackHeight);
323 
324         canvas.translate(0, top);
325         mHorizontalTrackDrawable.draw(canvas);
326         canvas.translate(left, 0);
327         mHorizontalThumbDrawable.draw(canvas);
328         canvas.translate(-left, -top);
329     }
330 
331     /**
332      * Notify the scroller of external change of the scroll, e.g. through dragging or flinging on
333      * the view itself.
334      *
335      * @param offsetX The new scroll X offset.
336      * @param offsetY The new scroll Y offset.
337      */
updateScrollPosition(int offsetX, int offsetY)338     void updateScrollPosition(int offsetX, int offsetY) {
339         int verticalContentLength = mRecyclerView.computeVerticalScrollRange();
340         int verticalVisibleLength = mRecyclerViewHeight;
341         mNeedVerticalScrollbar = verticalContentLength - verticalVisibleLength > 0
342             && mRecyclerViewHeight >= mScrollbarMinimumRange;
343 
344         int horizontalContentLength = mRecyclerView.computeHorizontalScrollRange();
345         int horizontalVisibleLength = mRecyclerViewWidth;
346         mNeedHorizontalScrollbar = horizontalContentLength - horizontalVisibleLength > 0
347             && mRecyclerViewWidth >= mScrollbarMinimumRange;
348 
349         if (!mNeedVerticalScrollbar && !mNeedHorizontalScrollbar) {
350             if (mState != STATE_HIDDEN) {
351                 setState(STATE_HIDDEN);
352             }
353             return;
354         }
355 
356         if (mNeedVerticalScrollbar) {
357             float middleScreenPos = offsetY + verticalVisibleLength / 2.0f;
358             mVerticalThumbCenterY =
359                 (int) ((verticalVisibleLength * middleScreenPos) / verticalContentLength);
360             mVerticalThumbHeight = Math.min(verticalVisibleLength,
361                 (verticalVisibleLength * verticalVisibleLength) / verticalContentLength);
362         }
363 
364         if (mNeedHorizontalScrollbar) {
365             float middleScreenPos = offsetX + horizontalVisibleLength / 2.0f;
366             mHorizontalThumbCenterX =
367                 (int) ((horizontalVisibleLength * middleScreenPos) / horizontalContentLength);
368             mHorizontalThumbWidth = Math.min(horizontalVisibleLength,
369                 (horizontalVisibleLength * horizontalVisibleLength) / horizontalContentLength);
370         }
371 
372         if (mState == STATE_HIDDEN || mState == STATE_VISIBLE) {
373             setState(STATE_VISIBLE);
374         }
375     }
376 
377     @Override
onInterceptTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent ev)378     public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
379             @NonNull MotionEvent ev) {
380         final boolean handled;
381         if (mState == STATE_VISIBLE) {
382             boolean insideVerticalThumb = isPointInsideVerticalThumb(ev.getX(), ev.getY());
383             boolean insideHorizontalThumb = isPointInsideHorizontalThumb(ev.getX(), ev.getY());
384             if (ev.getAction() == MotionEvent.ACTION_DOWN
385                     && (insideVerticalThumb || insideHorizontalThumb)) {
386                 if (insideHorizontalThumb) {
387                     mDragState = DRAG_X;
388                     mHorizontalDragX = (int) ev.getX();
389                 } else if (insideVerticalThumb) {
390                     mDragState = DRAG_Y;
391                     mVerticalDragY = (int) ev.getY();
392                 }
393 
394                 setState(STATE_DRAGGING);
395                 handled = true;
396             } else {
397                 handled = false;
398             }
399         } else if (mState == STATE_DRAGGING) {
400             handled = true;
401         } else {
402             handled = false;
403         }
404         return handled;
405     }
406 
407     @Override
onTouchEvent(@onNull RecyclerView recyclerView, @NonNull MotionEvent me)408     public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent me) {
409         if (mState == STATE_HIDDEN) {
410             return;
411         }
412 
413         if (me.getAction() == MotionEvent.ACTION_DOWN) {
414             boolean insideVerticalThumb = isPointInsideVerticalThumb(me.getX(), me.getY());
415             boolean insideHorizontalThumb = isPointInsideHorizontalThumb(me.getX(), me.getY());
416             if (insideVerticalThumb || insideHorizontalThumb) {
417                 if (insideHorizontalThumb) {
418                     mDragState = DRAG_X;
419                     mHorizontalDragX = (int) me.getX();
420                 } else if (insideVerticalThumb) {
421                     mDragState = DRAG_Y;
422                     mVerticalDragY = (int) me.getY();
423                 }
424                 setState(STATE_DRAGGING);
425             }
426         } else if (me.getAction() == MotionEvent.ACTION_UP && mState == STATE_DRAGGING) {
427             mVerticalDragY = 0;
428             mHorizontalDragX = 0;
429             setState(STATE_VISIBLE);
430             mDragState = DRAG_NONE;
431         } else if (me.getAction() == MotionEvent.ACTION_MOVE && mState == STATE_DRAGGING) {
432             show();
433             if (mDragState == DRAG_X) {
434                 horizontalScrollTo(me.getX());
435             }
436             if (mDragState == DRAG_Y) {
437                 verticalScrollTo(me.getY());
438             }
439         }
440     }
441 
442     @Override
onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)443     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { }
444 
verticalScrollTo(float y)445     private void verticalScrollTo(float y) {
446         final int[] scrollbarRange = getVerticalRange();
447         y = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], y));
448         if (Math.abs(mVerticalThumbCenterY - y) < 2) {
449             return;
450         }
451         int scrollingBy = scrollTo(mVerticalDragY, y, scrollbarRange,
452                 mRecyclerView.computeVerticalScrollRange(),
453                 mRecyclerView.computeVerticalScrollOffset(), mRecyclerViewHeight);
454         if (scrollingBy != 0) {
455             mRecyclerView.scrollBy(0, scrollingBy);
456         }
457         mVerticalDragY = y;
458     }
459 
horizontalScrollTo(float x)460     private void horizontalScrollTo(float x) {
461         final int[] scrollbarRange = getHorizontalRange();
462         x = Math.max(scrollbarRange[0], Math.min(scrollbarRange[1], x));
463         if (Math.abs(mHorizontalThumbCenterX - x) < 2) {
464             return;
465         }
466 
467         int scrollingBy = scrollTo(mHorizontalDragX, x, scrollbarRange,
468                 mRecyclerView.computeHorizontalScrollRange(),
469                 mRecyclerView.computeHorizontalScrollOffset(), mRecyclerViewWidth);
470         if (scrollingBy != 0) {
471             mRecyclerView.scrollBy(scrollingBy, 0);
472         }
473 
474         mHorizontalDragX = x;
475     }
476 
scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange, int scrollOffset, int viewLength)477     private int scrollTo(float oldDragPos, float newDragPos, int[] scrollbarRange, int scrollRange,
478             int scrollOffset, int viewLength) {
479         int scrollbarLength = scrollbarRange[1] - scrollbarRange[0];
480         if (scrollbarLength == 0) {
481             return 0;
482         }
483         float percentage = ((newDragPos - oldDragPos) / (float) scrollbarLength);
484         int totalPossibleOffset = scrollRange - viewLength;
485         int scrollingBy = (int) (percentage * totalPossibleOffset);
486         int absoluteOffset = scrollOffset + scrollingBy;
487         if (absoluteOffset < totalPossibleOffset && absoluteOffset >= 0) {
488             return scrollingBy;
489         } else {
490             return 0;
491         }
492     }
493 
494     @VisibleForTesting
isPointInsideVerticalThumb(float x, float y)495     boolean isPointInsideVerticalThumb(float x, float y) {
496         return (isLayoutRTL() ? x <= mVerticalThumbWidth
497             : x >= mRecyclerViewWidth - mVerticalThumbWidth)
498             && y >= mVerticalThumbCenterY - mVerticalThumbHeight / 2
499             && y <= mVerticalThumbCenterY + mVerticalThumbHeight / 2;
500     }
501 
502     @VisibleForTesting
isPointInsideHorizontalThumb(float x, float y)503     boolean isPointInsideHorizontalThumb(float x, float y) {
504         return (y >= mRecyclerViewHeight - mHorizontalThumbHeight)
505             && x >= mHorizontalThumbCenterX - mHorizontalThumbWidth / 2
506             && x <= mHorizontalThumbCenterX + mHorizontalThumbWidth / 2;
507     }
508 
509     @VisibleForTesting
getHorizontalTrackDrawable()510     Drawable getHorizontalTrackDrawable() {
511         return mHorizontalTrackDrawable;
512     }
513 
514     @VisibleForTesting
getHorizontalThumbDrawable()515     Drawable getHorizontalThumbDrawable() {
516         return mHorizontalThumbDrawable;
517     }
518 
519     @VisibleForTesting
getVerticalTrackDrawable()520     Drawable getVerticalTrackDrawable() {
521         return mVerticalTrackDrawable;
522     }
523 
524     @VisibleForTesting
getVerticalThumbDrawable()525     Drawable getVerticalThumbDrawable() {
526         return mVerticalThumbDrawable;
527     }
528 
529     /**
530      * Gets the (min, max) vertical positions of the vertical scroll bar.
531      */
getVerticalRange()532     private int[] getVerticalRange() {
533         mVerticalRange[0] = mMargin;
534         mVerticalRange[1] = mRecyclerViewHeight - mMargin;
535         return mVerticalRange;
536     }
537 
538     /**
539      * Gets the (min, max) horizontal positions of the horizontal scroll bar.
540      */
getHorizontalRange()541     private int[] getHorizontalRange() {
542         mHorizontalRange[0] = mMargin;
543         mHorizontalRange[1] = mRecyclerViewWidth - mMargin;
544         return mHorizontalRange;
545     }
546 
547     private class AnimatorListener extends AnimatorListenerAdapter {
548 
549         private boolean mCanceled = false;
550 
AnimatorListener()551         AnimatorListener() {
552         }
553 
554         @Override
onAnimationEnd(Animator animation)555         public void onAnimationEnd(Animator animation) {
556             // Cancel is always followed by a new directive, so don't update state.
557             if (mCanceled) {
558                 mCanceled = false;
559                 return;
560             }
561             if ((float) mShowHideAnimator.getAnimatedValue() == 0) {
562                 mAnimationState = ANIMATION_STATE_OUT;
563                 setState(STATE_HIDDEN);
564             } else {
565                 mAnimationState = ANIMATION_STATE_IN;
566                 requestRedraw();
567             }
568         }
569 
570         @Override
onAnimationCancel(Animator animation)571         public void onAnimationCancel(Animator animation) {
572             mCanceled = true;
573         }
574     }
575 
576     private class AnimatorUpdater implements AnimatorUpdateListener {
AnimatorUpdater()577         AnimatorUpdater() {
578         }
579 
580         @Override
onAnimationUpdate(ValueAnimator valueAnimator)581         public void onAnimationUpdate(ValueAnimator valueAnimator) {
582             int alpha = (int) (SCROLLBAR_FULL_OPAQUE * ((float) valueAnimator.getAnimatedValue()));
583             mVerticalThumbDrawable.setAlpha(alpha);
584             mVerticalTrackDrawable.setAlpha(alpha);
585             requestRedraw();
586         }
587     }
588 }
589