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