1 /* 2 * Copyright (C) 2006 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 android.widget; 18 19 import static android.view.flags.Flags.viewVelocityApi; 20 21 import android.annotation.ColorInt; 22 import android.annotation.NonNull; 23 import android.compat.annotation.UnsupportedAppUsage; 24 import android.content.Context; 25 import android.content.res.Configuration; 26 import android.content.res.TypedArray; 27 import android.graphics.Canvas; 28 import android.graphics.Rect; 29 import android.os.Build; 30 import android.os.Build.VERSION_CODES; 31 import android.os.Bundle; 32 import android.os.Parcel; 33 import android.os.Parcelable; 34 import android.os.StrictMode; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.view.FocusFinder; 38 import android.view.HapticScrollFeedbackProvider; 39 import android.view.InputDevice; 40 import android.view.KeyEvent; 41 import android.view.MotionEvent; 42 import android.view.VelocityTracker; 43 import android.view.View; 44 import android.view.ViewConfiguration; 45 import android.view.ViewDebug; 46 import android.view.ViewGroup; 47 import android.view.ViewHierarchyEncoder; 48 import android.view.ViewParent; 49 import android.view.accessibility.AccessibilityEvent; 50 import android.view.accessibility.AccessibilityNodeInfo; 51 import android.view.animation.AnimationUtils; 52 import android.view.flags.Flags; 53 import android.view.inspector.InspectableProperty; 54 55 import com.android.internal.R; 56 import com.android.internal.annotations.VisibleForTesting; 57 58 import java.util.List; 59 60 /** 61 * A view group that allows the view hierarchy placed within it to be scrolled. 62 * Scroll view may have only one direct child placed within it. 63 * To add multiple views within the scroll view, make 64 * the direct child you add a view group, for example {@link LinearLayout}, and 65 * place additional views within that LinearLayout. 66 * 67 * <p>Scroll view supports vertical scrolling only. For horizontal scrolling, 68 * use {@link HorizontalScrollView} instead.</p> 69 * 70 * <p>Never add a {@link androidx.recyclerview.widget.RecyclerView} or {@link ListView} to 71 * a scroll view. Doing so results in poor user interface performance and a poor user 72 * experience.</p> 73 * 74 * <p class="note"> 75 * For vertical scrolling, consider {@link androidx.core.widget.NestedScrollView} 76 * instead of scroll view which offers greater user interface flexibility and 77 * support for the material design scrolling patterns.</p> 78 * 79 * <p>Material Design offers guidelines on how the appearance of 80 * <a href="https://material.io/components/">several UI components</a>, including app bars and 81 * banners, should respond to gestures.</p> 82 * 83 * @attr ref android.R.styleable#ScrollView_fillViewport 84 */ 85 public class ScrollView extends FrameLayout { 86 static final int ANIMATED_SCROLL_GAP = 250; 87 88 static final float MAX_SCROLL_FACTOR = 0.5f; 89 90 private static final String TAG = "ScrollView"; 91 92 /** 93 * When flinging the stretch towards scrolling content, it should destretch quicker than the 94 * fling would normally do. The visual effect of flinging the stretch looks strange as little 95 * appears to happen at first and then when the stretch disappears, the content starts 96 * scrolling quickly. 97 */ 98 private static final float FLING_DESTRETCH_FACTOR = 4f; 99 100 @UnsupportedAppUsage 101 private long mLastScroll; 102 103 private final Rect mTempRect = new Rect(); 104 @UnsupportedAppUsage 105 private OverScroller mScroller; 106 /** 107 * Tracks the state of the top edge glow. 108 * 109 * Even though this field is practically final, we cannot make it final because there are apps 110 * setting it via reflection and they need to keep working until they target Q. 111 * @hide 112 */ 113 @NonNull 114 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768600) 115 @VisibleForTesting 116 public EdgeEffect mEdgeGlowTop; 117 118 /** 119 * Tracks the state of the bottom edge glow. 120 * 121 * Even though this field is practically final, we cannot make it final because there are apps 122 * setting it via reflection and they need to keep working until they target Q. 123 * @hide 124 */ 125 @NonNull 126 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769386) 127 @VisibleForTesting 128 public EdgeEffect mEdgeGlowBottom; 129 130 /** 131 * Position of the last motion event. 132 */ 133 @UnsupportedAppUsage 134 private int mLastMotionY; 135 136 /** 137 * True when the layout has changed but the traversal has not come through yet. 138 * Ideally the view hierarchy would keep track of this for us. 139 */ 140 private boolean mIsLayoutDirty = true; 141 142 /** 143 * The child to give focus to in the event that a child has requested focus while the 144 * layout is dirty. This prevents the scroll from being wrong if the child has not been 145 * laid out before requesting focus. 146 */ 147 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769715) 148 private View mChildToScrollTo = null; 149 150 /** 151 * True if the user is currently dragging this ScrollView around. This is 152 * not the same as 'is being flinged', which can be checked by 153 * mScroller.isFinished() (flinging begins when the user lifts their finger). 154 */ 155 @UnsupportedAppUsage 156 private boolean mIsBeingDragged = false; 157 158 /** 159 * Determines speed during touch scrolling 160 */ 161 @UnsupportedAppUsage 162 private VelocityTracker mVelocityTracker; 163 164 /** 165 * When set to true, the scroll view measure its child to make it fill the currently 166 * visible area. 167 */ 168 @ViewDebug.ExportedProperty(category = "layout") 169 private boolean mFillViewport; 170 171 /** 172 * Whether arrow scrolling is animated. 173 */ 174 private boolean mSmoothScrollingEnabled = true; 175 176 private int mTouchSlop; 177 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124051125) 178 private int mMinimumVelocity; 179 private int mMaximumVelocity; 180 181 @UnsupportedAppUsage(maxTargetSdk = VERSION_CODES.P, trackingBug = 124050903) 182 private int mOverscrollDistance; 183 @UnsupportedAppUsage(maxTargetSdk = VERSION_CODES.P, trackingBug = 124050903) 184 private int mOverflingDistance; 185 186 private float mVerticalScrollFactor; 187 188 /** 189 * ID of the active pointer. This is used to retain consistency during 190 * drags/flings if multiple pointers are used. 191 */ 192 private int mActivePointerId = INVALID_POINTER; 193 194 /** 195 * Used during scrolling to retrieve the new offset within the window. 196 */ 197 private final int[] mScrollOffset = new int[2]; 198 private final int[] mScrollConsumed = new int[2]; 199 private int mNestedYOffset; 200 201 /** 202 * The StrictMode "critical time span" objects to catch animation 203 * stutters. Non-null when a time-sensitive animation is 204 * in-flight. Must call finish() on them when done animating. 205 * These are no-ops on user builds. 206 */ 207 private StrictMode.Span mScrollStrictSpan = null; // aka "drag" 208 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 209 private StrictMode.Span mFlingStrictSpan = null; 210 211 private DifferentialMotionFlingHelper mDifferentialMotionFlingHelper; 212 213 private HapticScrollFeedbackProvider mHapticScrollFeedbackProvider; 214 215 /** 216 * Sentinel value for no current active pointer. 217 * Used by {@link #mActivePointerId}. 218 */ 219 private static final int INVALID_POINTER = -1; 220 221 private SavedState mSavedState; 222 ScrollView(Context context)223 public ScrollView(Context context) { 224 this(context, null); 225 } 226 ScrollView(Context context, AttributeSet attrs)227 public ScrollView(Context context, AttributeSet attrs) { 228 this(context, attrs, com.android.internal.R.attr.scrollViewStyle); 229 } 230 ScrollView(Context context, AttributeSet attrs, int defStyleAttr)231 public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) { 232 this(context, attrs, defStyleAttr, 0); 233 } 234 ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)235 public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 236 super(context, attrs, defStyleAttr, defStyleRes); 237 mEdgeGlowTop = new EdgeEffect(context, attrs); 238 mEdgeGlowBottom = new EdgeEffect(context, attrs); 239 initScrollView(); 240 241 final TypedArray a = context.obtainStyledAttributes( 242 attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes); 243 saveAttributeDataForStyleable(context, com.android.internal.R.styleable.ScrollView, 244 attrs, a, defStyleAttr, defStyleRes); 245 246 setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false)); 247 248 a.recycle(); 249 250 if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) { 251 setRevealOnFocusHint(false); 252 } 253 } 254 255 @Override shouldDelayChildPressedState()256 public boolean shouldDelayChildPressedState() { 257 return true; 258 } 259 260 @Override getTopFadingEdgeStrength()261 protected float getTopFadingEdgeStrength() { 262 if (getChildCount() == 0) { 263 return 0.0f; 264 } 265 266 final int length = getVerticalFadingEdgeLength(); 267 if (mScrollY < length) { 268 return mScrollY / (float) length; 269 } 270 271 return 1.0f; 272 } 273 274 @Override getBottomFadingEdgeStrength()275 protected float getBottomFadingEdgeStrength() { 276 if (getChildCount() == 0) { 277 return 0.0f; 278 } 279 280 final int length = getVerticalFadingEdgeLength(); 281 final int bottomEdge = getHeight() - mPaddingBottom; 282 final int span = getChildAt(0).getBottom() - mScrollY - bottomEdge; 283 if (span < length) { 284 return span / (float) length; 285 } 286 287 return 1.0f; 288 } 289 290 /** 291 * Sets the edge effect color for both top and bottom edge effects. 292 * 293 * @param color The color for the edge effects. 294 * @see #setTopEdgeEffectColor(int) 295 * @see #setBottomEdgeEffectColor(int) 296 * @see #getTopEdgeEffectColor() 297 * @see #getBottomEdgeEffectColor() 298 */ setEdgeEffectColor(@olorInt int color)299 public void setEdgeEffectColor(@ColorInt int color) { 300 setTopEdgeEffectColor(color); 301 setBottomEdgeEffectColor(color); 302 } 303 304 /** 305 * Sets the bottom edge effect color. 306 * 307 * @param color The color for the bottom edge effect. 308 * @see #setTopEdgeEffectColor(int) 309 * @see #setEdgeEffectColor(int) 310 * @see #getTopEdgeEffectColor() 311 * @see #getBottomEdgeEffectColor() 312 */ setBottomEdgeEffectColor(@olorInt int color)313 public void setBottomEdgeEffectColor(@ColorInt int color) { 314 mEdgeGlowBottom.setColor(color); 315 } 316 317 /** 318 * Sets the top edge effect color. 319 * 320 * @param color The color for the top edge effect. 321 * @see #setBottomEdgeEffectColor(int) 322 * @see #setEdgeEffectColor(int) 323 * @see #getTopEdgeEffectColor() 324 * @see #getBottomEdgeEffectColor() 325 */ setTopEdgeEffectColor(@olorInt int color)326 public void setTopEdgeEffectColor(@ColorInt int color) { 327 mEdgeGlowTop.setColor(color); 328 } 329 330 /** 331 * Returns the top edge effect color. 332 * 333 * @return The top edge effect color. 334 * @see #setEdgeEffectColor(int) 335 * @see #setTopEdgeEffectColor(int) 336 * @see #setBottomEdgeEffectColor(int) 337 * @see #getBottomEdgeEffectColor() 338 */ 339 @ColorInt getTopEdgeEffectColor()340 public int getTopEdgeEffectColor() { 341 return mEdgeGlowTop.getColor(); 342 } 343 344 /** 345 * Returns the bottom edge effect color. 346 * 347 * @return The bottom edge effect color. 348 * @see #setEdgeEffectColor(int) 349 * @see #setTopEdgeEffectColor(int) 350 * @see #setBottomEdgeEffectColor(int) 351 * @see #getTopEdgeEffectColor() 352 */ 353 @ColorInt getBottomEdgeEffectColor()354 public int getBottomEdgeEffectColor() { 355 return mEdgeGlowBottom.getColor(); 356 } 357 358 /** 359 * @return The maximum amount this scroll view will scroll in response to 360 * an arrow event. 361 */ getMaxScrollAmount()362 public int getMaxScrollAmount() { 363 return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop)); 364 } 365 initScrollView()366 private void initScrollView() { 367 mScroller = new OverScroller(getContext()); 368 setFocusable(true); 369 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 370 setWillNotDraw(false); 371 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 372 mTouchSlop = configuration.getScaledTouchSlop(); 373 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 374 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 375 mOverscrollDistance = configuration.getScaledOverscrollDistance(); 376 mOverflingDistance = configuration.getScaledOverflingDistance(); 377 mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor(); 378 } 379 380 @Override addView(View child)381 public void addView(View child) { 382 if (getChildCount() > 0) { 383 throw new IllegalStateException("ScrollView can host only one direct child"); 384 } 385 386 super.addView(child); 387 } 388 389 @Override addView(View child, int index)390 public void addView(View child, int index) { 391 if (getChildCount() > 0) { 392 throw new IllegalStateException("ScrollView can host only one direct child"); 393 } 394 395 super.addView(child, index); 396 } 397 398 @Override addView(View child, ViewGroup.LayoutParams params)399 public void addView(View child, ViewGroup.LayoutParams params) { 400 if (getChildCount() > 0) { 401 throw new IllegalStateException("ScrollView can host only one direct child"); 402 } 403 404 super.addView(child, params); 405 } 406 407 @Override addView(View child, int index, ViewGroup.LayoutParams params)408 public void addView(View child, int index, ViewGroup.LayoutParams params) { 409 if (getChildCount() > 0) { 410 throw new IllegalStateException("ScrollView can host only one direct child"); 411 } 412 413 super.addView(child, index, params); 414 } 415 416 /** 417 * @return Returns true this ScrollView can be scrolled 418 */ 419 @UnsupportedAppUsage canScroll()420 private boolean canScroll() { 421 View child = getChildAt(0); 422 if (child != null) { 423 int childHeight = child.getHeight(); 424 return getHeight() < childHeight + mPaddingTop + mPaddingBottom; 425 } 426 return false; 427 } 428 429 /** 430 * Indicates whether this ScrollView's content is stretched to fill the viewport. 431 * 432 * @return True if the content fills the viewport, false otherwise. 433 * 434 * @attr ref android.R.styleable#ScrollView_fillViewport 435 */ 436 @InspectableProperty isFillViewport()437 public boolean isFillViewport() { 438 return mFillViewport; 439 } 440 441 /** 442 * Indicates this ScrollView whether it should stretch its content height to fill 443 * the viewport or not. 444 * 445 * @param fillViewport True to stretch the content's height to the viewport's 446 * boundaries, false otherwise. 447 * 448 * @attr ref android.R.styleable#ScrollView_fillViewport 449 */ setFillViewport(boolean fillViewport)450 public void setFillViewport(boolean fillViewport) { 451 if (fillViewport != mFillViewport) { 452 mFillViewport = fillViewport; 453 requestLayout(); 454 } 455 } 456 457 /** 458 * @return Whether arrow scrolling will animate its transition. 459 */ isSmoothScrollingEnabled()460 public boolean isSmoothScrollingEnabled() { 461 return mSmoothScrollingEnabled; 462 } 463 464 /** 465 * Set whether arrow scrolling will animate its transition. 466 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition 467 */ setSmoothScrollingEnabled(boolean smoothScrollingEnabled)468 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 469 mSmoothScrollingEnabled = smoothScrollingEnabled; 470 } 471 472 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)473 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 474 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 475 476 if (!mFillViewport) { 477 return; 478 } 479 480 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 481 if (heightMode == MeasureSpec.UNSPECIFIED) { 482 return; 483 } 484 485 if (getChildCount() > 0) { 486 final View child = getChildAt(0); 487 final int widthPadding; 488 final int heightPadding; 489 final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; 490 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 491 if (targetSdkVersion >= VERSION_CODES.M) { 492 widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin; 493 heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin; 494 } else { 495 widthPadding = mPaddingLeft + mPaddingRight; 496 heightPadding = mPaddingTop + mPaddingBottom; 497 } 498 499 final int desiredHeight = getMeasuredHeight() - heightPadding; 500 if (child.getMeasuredHeight() < desiredHeight) { 501 final int childWidthMeasureSpec = getChildMeasureSpec( 502 widthMeasureSpec, widthPadding, lp.width); 503 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( 504 desiredHeight, MeasureSpec.EXACTLY); 505 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 506 } 507 } 508 } 509 510 @Override dispatchKeyEvent(KeyEvent event)511 public boolean dispatchKeyEvent(KeyEvent event) { 512 // Let the focused view and/or our descendants get the key first 513 return super.dispatchKeyEvent(event) || executeKeyEvent(event); 514 } 515 516 /** 517 * You can call this function yourself to have the scroll view perform 518 * scrolling from a key event, just as if the event had been dispatched to 519 * it by the view hierarchy. 520 * 521 * @param event The key event to execute. 522 * @return Return true if the event was handled, else false. 523 */ executeKeyEvent(KeyEvent event)524 public boolean executeKeyEvent(KeyEvent event) { 525 mTempRect.setEmpty(); 526 527 if (!canScroll()) { 528 if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK 529 && event.getKeyCode() != KeyEvent.KEYCODE_ESCAPE) { 530 View currentFocused = findFocus(); 531 if (currentFocused == this) currentFocused = null; 532 View nextFocused = FocusFinder.getInstance().findNextFocus(this, 533 currentFocused, View.FOCUS_DOWN); 534 return nextFocused != null 535 && nextFocused != this 536 && nextFocused.requestFocus(View.FOCUS_DOWN); 537 } 538 return false; 539 } 540 541 boolean handled = false; 542 if (event.getAction() == KeyEvent.ACTION_DOWN) { 543 switch (event.getKeyCode()) { 544 case KeyEvent.KEYCODE_DPAD_UP: 545 if (!event.isAltPressed()) { 546 handled = arrowScroll(View.FOCUS_UP); 547 } else { 548 handled = fullScroll(View.FOCUS_UP); 549 } 550 break; 551 case KeyEvent.KEYCODE_DPAD_DOWN: 552 if (!event.isAltPressed()) { 553 handled = arrowScroll(View.FOCUS_DOWN); 554 } else { 555 handled = fullScroll(View.FOCUS_DOWN); 556 } 557 break; 558 case KeyEvent.KEYCODE_MOVE_HOME: 559 handled = fullScroll(View.FOCUS_UP); 560 break; 561 case KeyEvent.KEYCODE_MOVE_END: 562 handled = fullScroll(View.FOCUS_DOWN); 563 break; 564 case KeyEvent.KEYCODE_PAGE_UP: 565 handled = pageScroll(View.FOCUS_UP); 566 break; 567 case KeyEvent.KEYCODE_PAGE_DOWN: 568 handled = pageScroll(View.FOCUS_DOWN); 569 break; 570 case KeyEvent.KEYCODE_SPACE: 571 handled = pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); 572 break; 573 } 574 } 575 576 return handled; 577 } 578 inChild(int x, int y)579 private boolean inChild(int x, int y) { 580 if (getChildCount() > 0) { 581 final int scrollY = mScrollY; 582 final View child = getChildAt(0); 583 return !(y < child.getTop() - scrollY 584 || y >= child.getBottom() - scrollY 585 || x < child.getLeft() 586 || x >= child.getRight()); 587 } 588 return false; 589 } 590 initOrResetVelocityTracker()591 private void initOrResetVelocityTracker() { 592 if (mVelocityTracker == null) { 593 mVelocityTracker = VelocityTracker.obtain(); 594 } else { 595 mVelocityTracker.clear(); 596 } 597 } 598 initVelocityTrackerIfNotExists()599 private void initVelocityTrackerIfNotExists() { 600 if (mVelocityTracker == null) { 601 mVelocityTracker = VelocityTracker.obtain(); 602 } 603 } 604 initDifferentialFlingHelperIfNotExists()605 private void initDifferentialFlingHelperIfNotExists() { 606 if (mDifferentialMotionFlingHelper == null) { 607 mDifferentialMotionFlingHelper = 608 new DifferentialMotionFlingHelper( 609 mContext, new DifferentialFlingTarget()); 610 } 611 } 612 initHapticScrollFeedbackProviderIfNotExists()613 private void initHapticScrollFeedbackProviderIfNotExists() { 614 if (mHapticScrollFeedbackProvider == null) { 615 mHapticScrollFeedbackProvider = new HapticScrollFeedbackProvider(this); 616 } 617 } 618 recycleVelocityTracker()619 private void recycleVelocityTracker() { 620 if (mVelocityTracker != null) { 621 mVelocityTracker.recycle(); 622 mVelocityTracker = null; 623 } 624 } 625 626 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)627 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 628 if (disallowIntercept) { 629 recycleVelocityTracker(); 630 } 631 super.requestDisallowInterceptTouchEvent(disallowIntercept); 632 } 633 634 635 @Override onInterceptTouchEvent(MotionEvent ev)636 public boolean onInterceptTouchEvent(MotionEvent ev) { 637 /* 638 * This method JUST determines whether we want to intercept the motion. 639 * If we return true, onMotionEvent will be called and we do the actual 640 * scrolling there. 641 */ 642 643 /* 644 * Shortcut the most recurring case: the user is in the dragging 645 * state and they is moving their finger. We want to intercept this 646 * motion. 647 */ 648 final int action = ev.getAction(); 649 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 650 return true; 651 } 652 653 if (super.onInterceptTouchEvent(ev)) { 654 return true; 655 } 656 657 /* 658 * Don't try to intercept touch if we can't scroll anyway. 659 */ 660 if (getScrollY() == 0 && !canScrollVertically(1)) { 661 return false; 662 } 663 664 switch (action & MotionEvent.ACTION_MASK) { 665 case MotionEvent.ACTION_MOVE: { 666 /* 667 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 668 * whether the user has moved far enough from their original down touch. 669 */ 670 671 /* 672 * Locally do absolute value. mLastMotionY is set to the y value 673 * of the down event. 674 */ 675 final int activePointerId = mActivePointerId; 676 if (activePointerId == INVALID_POINTER) { 677 // If we don't have a valid id, the touch down wasn't on content. 678 break; 679 } 680 681 final int pointerIndex = ev.findPointerIndex(activePointerId); 682 if (pointerIndex == -1) { 683 Log.e(TAG, "Invalid pointerId=" + activePointerId 684 + " in onInterceptTouchEvent"); 685 break; 686 } 687 688 final int y = (int) ev.getY(pointerIndex); 689 final int yDiff = Math.abs(y - mLastMotionY); 690 if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { 691 mIsBeingDragged = true; 692 mLastMotionY = y; 693 initVelocityTrackerIfNotExists(); 694 mVelocityTracker.addMovement(ev); 695 mNestedYOffset = 0; 696 if (mScrollStrictSpan == null) { 697 mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); 698 } 699 final ViewParent parent = getParent(); 700 if (parent != null) { 701 parent.requestDisallowInterceptTouchEvent(true); 702 } 703 } 704 break; 705 } 706 707 case MotionEvent.ACTION_DOWN: { 708 final int y = (int) ev.getY(); 709 if (!inChild((int) ev.getX(), (int) y)) { 710 mIsBeingDragged = false; 711 recycleVelocityTracker(); 712 break; 713 } 714 715 /* 716 * Remember location of down touch. 717 * ACTION_DOWN always refers to pointer index 0. 718 */ 719 mLastMotionY = y; 720 mActivePointerId = ev.getPointerId(0); 721 722 initOrResetVelocityTracker(); 723 mVelocityTracker.addMovement(ev); 724 /* 725 * If being flinged and user touches the screen, initiate drag; 726 * otherwise don't. mScroller.isFinished should be false when 727 * being flinged. We need to call computeScrollOffset() first so that 728 * isFinished() is correct. 729 */ 730 mScroller.computeScrollOffset(); 731 732 // For variable refresh rate project to track the current velocity of this View 733 if (viewVelocityApi()) { 734 setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); 735 } 736 737 mIsBeingDragged = !mScroller.isFinished() || !mEdgeGlowBottom.isFinished() 738 || !mEdgeGlowTop.isFinished(); 739 // Catch the edge effect if it is active. 740 if (!mEdgeGlowTop.isFinished()) { 741 mEdgeGlowTop.onPullDistance(0f, ev.getX() / getWidth()); 742 } 743 if (!mEdgeGlowBottom.isFinished()) { 744 mEdgeGlowBottom.onPullDistance(0f, 1f - ev.getX() / getWidth()); 745 } 746 if (mIsBeingDragged && mScrollStrictSpan == null) { 747 mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); 748 } 749 startNestedScroll(SCROLL_AXIS_VERTICAL); 750 break; 751 } 752 753 case MotionEvent.ACTION_CANCEL: 754 case MotionEvent.ACTION_UP: 755 /* Release the drag */ 756 mIsBeingDragged = false; 757 mActivePointerId = INVALID_POINTER; 758 recycleVelocityTracker(); 759 if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { 760 postInvalidateOnAnimation(); 761 } 762 stopNestedScroll(); 763 break; 764 case MotionEvent.ACTION_POINTER_UP: 765 onSecondaryPointerUp(ev); 766 break; 767 } 768 769 /* 770 * The only time we want to intercept motion events is if we are in the 771 * drag mode. 772 */ 773 return mIsBeingDragged; 774 } 775 shouldDisplayEdgeEffects()776 private boolean shouldDisplayEdgeEffects() { 777 return getOverScrollMode() != OVER_SCROLL_NEVER; 778 } 779 780 @Override onTouchEvent(MotionEvent ev)781 public boolean onTouchEvent(MotionEvent ev) { 782 initVelocityTrackerIfNotExists(); 783 784 MotionEvent vtev = MotionEvent.obtain(ev); 785 786 final int actionMasked = ev.getActionMasked(); 787 788 if (actionMasked == MotionEvent.ACTION_DOWN) { 789 mNestedYOffset = 0; 790 } 791 vtev.offsetLocation(0, mNestedYOffset); 792 793 switch (actionMasked) { 794 case MotionEvent.ACTION_DOWN: { 795 if (getChildCount() == 0) { 796 return false; 797 } 798 if (!mScroller.isFinished()) { 799 final ViewParent parent = getParent(); 800 if (parent != null) { 801 parent.requestDisallowInterceptTouchEvent(true); 802 } 803 } 804 805 /* 806 * If being flinged and user touches, stop the fling. isFinished 807 * will be false if being flinged. 808 */ 809 if (!mScroller.isFinished()) { 810 mScroller.abortAnimation(); 811 if (mFlingStrictSpan != null) { 812 mFlingStrictSpan.finish(); 813 mFlingStrictSpan = null; 814 } 815 } 816 817 // Remember where the motion event started 818 mLastMotionY = (int) ev.getY(); 819 mActivePointerId = ev.getPointerId(0); 820 startNestedScroll(SCROLL_AXIS_VERTICAL); 821 break; 822 } 823 case MotionEvent.ACTION_MOVE: 824 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); 825 if (activePointerIndex == -1) { 826 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); 827 break; 828 } 829 830 final int y = (int) ev.getY(activePointerIndex); 831 int deltaY = mLastMotionY - y; 832 if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { 833 deltaY -= mScrollConsumed[1]; 834 vtev.offsetLocation(0, mScrollOffset[1]); 835 mNestedYOffset += mScrollOffset[1]; 836 } 837 if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { 838 final ViewParent parent = getParent(); 839 if (parent != null) { 840 parent.requestDisallowInterceptTouchEvent(true); 841 } 842 mIsBeingDragged = true; 843 if (deltaY > 0) { 844 deltaY -= mTouchSlop; 845 } else { 846 deltaY += mTouchSlop; 847 } 848 } 849 if (mIsBeingDragged) { 850 // Scroll to follow the motion event 851 mLastMotionY = y - mScrollOffset[1]; 852 853 final int oldY = mScrollY; 854 final int range = getScrollRange(); 855 final int overscrollMode = getOverScrollMode(); 856 boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 857 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 858 859 final float displacement = ev.getX(activePointerIndex) / getWidth(); 860 if (canOverscroll) { 861 int consumed = 0; 862 if (deltaY < 0 && mEdgeGlowBottom.getDistance() != 0f) { 863 consumed = Math.round(getHeight() 864 * mEdgeGlowBottom.onPullDistance((float) deltaY / getHeight(), 865 1 - displacement)); 866 } else if (deltaY > 0 && mEdgeGlowTop.getDistance() != 0f) { 867 consumed = Math.round(-getHeight() 868 * mEdgeGlowTop.onPullDistance((float) -deltaY / getHeight(), 869 displacement)); 870 } 871 deltaY -= consumed; 872 } 873 874 // Calling overScrollBy will call onOverScrolled, which 875 // calls onScrollChanged if applicable. 876 overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true); 877 878 final int scrolledDeltaY = mScrollY - oldY; 879 final int unconsumedY = deltaY - scrolledDeltaY; 880 if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { 881 mLastMotionY -= mScrollOffset[1]; 882 vtev.offsetLocation(0, mScrollOffset[1]); 883 mNestedYOffset += mScrollOffset[1]; 884 } else if (canOverscroll && deltaY != 0f) { 885 final int pulledToY = oldY + deltaY; 886 if (pulledToY < 0) { 887 mEdgeGlowTop.onPullDistance((float) -deltaY / getHeight(), 888 displacement); 889 if (!mEdgeGlowBottom.isFinished()) { 890 mEdgeGlowBottom.onRelease(); 891 } 892 } else if (pulledToY > range) { 893 mEdgeGlowBottom.onPullDistance((float) deltaY / getHeight(), 894 1.f - displacement); 895 if (!mEdgeGlowTop.isFinished()) { 896 mEdgeGlowTop.onRelease(); 897 } 898 } 899 if (shouldDisplayEdgeEffects() 900 && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { 901 postInvalidateOnAnimation(); 902 } 903 } 904 } 905 break; 906 case MotionEvent.ACTION_UP: 907 if (mIsBeingDragged) { 908 final VelocityTracker velocityTracker = mVelocityTracker; 909 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 910 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 911 912 if ((Math.abs(initialVelocity) > mMinimumVelocity)) { 913 flingWithNestedDispatch(-initialVelocity); 914 } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, 915 getScrollRange())) { 916 postInvalidateOnAnimation(); 917 } 918 919 mActivePointerId = INVALID_POINTER; 920 endDrag(); 921 velocityTracker.clear(); 922 } 923 break; 924 case MotionEvent.ACTION_CANCEL: 925 if (mIsBeingDragged && getChildCount() > 0) { 926 if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { 927 postInvalidateOnAnimation(); 928 } 929 mActivePointerId = INVALID_POINTER; 930 endDrag(); 931 } 932 break; 933 case MotionEvent.ACTION_POINTER_DOWN: { 934 final int index = ev.getActionIndex(); 935 mLastMotionY = (int) ev.getY(index); 936 mActivePointerId = ev.getPointerId(index); 937 break; 938 } 939 case MotionEvent.ACTION_POINTER_UP: 940 onSecondaryPointerUp(ev); 941 mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); 942 break; 943 } 944 945 if (mVelocityTracker != null) { 946 mVelocityTracker.addMovement(vtev); 947 } 948 vtev.recycle(); 949 return true; 950 } 951 onSecondaryPointerUp(MotionEvent ev)952 private void onSecondaryPointerUp(MotionEvent ev) { 953 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 954 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 955 final int pointerId = ev.getPointerId(pointerIndex); 956 if (pointerId == mActivePointerId) { 957 // This was our active pointer going up. Choose a new 958 // active pointer and adjust accordingly. 959 // TODO: Make this decision more intelligent. 960 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 961 mLastMotionY = (int) ev.getY(newPointerIndex); 962 mActivePointerId = ev.getPointerId(newPointerIndex); 963 if (mVelocityTracker != null) { 964 mVelocityTracker.clear(); 965 } 966 } 967 } 968 969 @Override onGenericMotionEvent(MotionEvent event)970 public boolean onGenericMotionEvent(MotionEvent event) { 971 switch (event.getAction()) { 972 case MotionEvent.ACTION_SCROLL: 973 final int axis; 974 if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { 975 axis = MotionEvent.AXIS_VSCROLL; 976 } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) { 977 axis = MotionEvent.AXIS_SCROLL; 978 } else { 979 axis = -1; 980 } 981 982 final float axisValue = (axis == -1) ? 0 : event.getAxisValue(axis); 983 final int delta = Math.round(axisValue * mVerticalScrollFactor); 984 if (delta != 0) { 985 // Tracks whether or not we should attempt fling for this event. 986 // Fling should not be attempted if the view is already at the limit of scroll, 987 // since it conflicts with EdgeEffect. 988 boolean hitLimit = false; 989 final int range = getScrollRange(); 990 int oldScrollY = mScrollY; 991 int newScrollY = oldScrollY - delta; 992 993 final int overscrollMode = getOverScrollMode(); 994 boolean canOverscroll = !event.isFromSource(InputDevice.SOURCE_MOUSE) 995 && (overscrollMode == OVER_SCROLL_ALWAYS 996 || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)); 997 boolean absorbed = false; 998 999 if (newScrollY < 0) { 1000 if (canOverscroll) { 1001 mEdgeGlowTop.onPullDistance(-(float) newScrollY / getHeight(), 0.5f); 1002 mEdgeGlowTop.onRelease(); 1003 invalidate(); 1004 absorbed = true; 1005 } 1006 newScrollY = 0; 1007 hitLimit = true; 1008 } else if (newScrollY > range) { 1009 if (canOverscroll) { 1010 mEdgeGlowBottom.onPullDistance( 1011 (float) (newScrollY - range) / getHeight(), 0.5f); 1012 mEdgeGlowBottom.onRelease(); 1013 invalidate(); 1014 absorbed = true; 1015 } 1016 newScrollY = range; 1017 hitLimit = true; 1018 } 1019 if (newScrollY != oldScrollY) { 1020 super.scrollTo(mScrollX, newScrollY); 1021 if (hitLimit) { 1022 if (Flags.scrollFeedbackApi()) { 1023 initHapticScrollFeedbackProviderIfNotExists(); 1024 mHapticScrollFeedbackProvider.onScrollLimit( 1025 event.getDeviceId(), event.getSource(), axis, 1026 /* isStart= */ newScrollY == 0); 1027 } 1028 } else { 1029 if (Flags.scrollFeedbackApi()) { 1030 initHapticScrollFeedbackProviderIfNotExists(); 1031 mHapticScrollFeedbackProvider.onScrollProgress( 1032 event.getDeviceId(), event.getSource(), axis, delta); 1033 } 1034 initDifferentialFlingHelperIfNotExists(); 1035 mDifferentialMotionFlingHelper.onMotionEvent(event, axis); 1036 } 1037 return true; 1038 } 1039 if (absorbed) { 1040 return true; 1041 } 1042 } 1043 break; 1044 } 1045 1046 return super.onGenericMotionEvent(event); 1047 } 1048 1049 @Override onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)1050 protected void onOverScrolled(int scrollX, int scrollY, 1051 boolean clampedX, boolean clampedY) { 1052 // Treat animating scrolls differently; see #computeScroll() for why. 1053 if (!mScroller.isFinished()) { 1054 final int oldX = mScrollX; 1055 final int oldY = mScrollY; 1056 mScrollX = scrollX; 1057 mScrollY = scrollY; 1058 invalidateParentIfNeeded(); 1059 onScrollChanged(mScrollX, mScrollY, oldX, oldY); 1060 if (clampedY) { 1061 mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange()); 1062 } 1063 } else { 1064 super.scrollTo(scrollX, scrollY); 1065 } 1066 1067 awakenScrollBars(); 1068 } 1069 1070 /** @hide */ 1071 @Override performAccessibilityActionInternal(int action, Bundle arguments)1072 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1073 if (super.performAccessibilityActionInternal(action, arguments)) { 1074 return true; 1075 } 1076 if (!isEnabled()) { 1077 return false; 1078 } 1079 switch (action) { 1080 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 1081 case R.id.accessibilityActionScrollDown: { 1082 final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop; 1083 final int targetScrollY = Math.min(mScrollY + viewportHeight, getScrollRange()); 1084 if (targetScrollY != mScrollY) { 1085 smoothScrollTo(0, targetScrollY); 1086 return true; 1087 } 1088 } return false; 1089 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: 1090 case R.id.accessibilityActionScrollUp: { 1091 final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop; 1092 final int targetScrollY = Math.max(mScrollY - viewportHeight, 0); 1093 if (targetScrollY != mScrollY) { 1094 smoothScrollTo(0, targetScrollY); 1095 return true; 1096 } 1097 } return false; 1098 } 1099 return false; 1100 } 1101 1102 @Override getAccessibilityClassName()1103 public CharSequence getAccessibilityClassName() { 1104 return ScrollView.class.getName(); 1105 } 1106 1107 /** @hide */ 1108 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1109 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 1110 super.onInitializeAccessibilityNodeInfoInternal(info); 1111 if (isEnabled()) { 1112 final int scrollRange = getScrollRange(); 1113 if (scrollRange > 0) { 1114 info.setScrollable(true); 1115 if (mScrollY > 0) { 1116 info.addAction( 1117 AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); 1118 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP); 1119 } 1120 if (mScrollY < scrollRange) { 1121 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); 1122 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN); 1123 } 1124 } 1125 } 1126 } 1127 1128 /** @hide */ 1129 @Override onInitializeAccessibilityEventInternal(AccessibilityEvent event)1130 public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { 1131 super.onInitializeAccessibilityEventInternal(event); 1132 final boolean scrollable = getScrollRange() > 0; 1133 event.setScrollable(scrollable); 1134 event.setMaxScrollX(mScrollX); 1135 event.setMaxScrollY(getScrollRange()); 1136 } 1137 getScrollRange()1138 private int getScrollRange() { 1139 int scrollRange = 0; 1140 if (getChildCount() > 0) { 1141 View child = getChildAt(0); 1142 scrollRange = Math.max(0, 1143 child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop)); 1144 } 1145 return scrollRange; 1146 } 1147 1148 /** 1149 * <p> 1150 * Finds the next focusable component that fits in the specified bounds. 1151 * </p> 1152 * 1153 * @param topFocus look for a candidate is the one at the top of the bounds 1154 * if topFocus is true, or at the bottom of the bounds if topFocus is 1155 * false 1156 * @param top the top offset of the bounds in which a focusable must be 1157 * found 1158 * @param bottom the bottom offset of the bounds in which a focusable must 1159 * be found 1160 * @return the next focusable component in the bounds or null if none can 1161 * be found 1162 */ findFocusableViewInBounds(boolean topFocus, int top, int bottom)1163 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { 1164 1165 List<View> focusables = getFocusables(View.FOCUS_FORWARD); 1166 View focusCandidate = null; 1167 1168 /* 1169 * A fully contained focusable is one where its top is below the bound's 1170 * top, and its bottom is above the bound's bottom. A partially 1171 * contained focusable is one where some part of it is within the 1172 * bounds, but it also has some part that is not within bounds. A fully contained 1173 * focusable is preferred to a partially contained focusable. 1174 */ 1175 boolean foundFullyContainedFocusable = false; 1176 1177 int count = focusables.size(); 1178 for (int i = 0; i < count; i++) { 1179 View view = focusables.get(i); 1180 int viewTop = view.getTop(); 1181 int viewBottom = view.getBottom(); 1182 1183 if (top < viewBottom && viewTop < bottom) { 1184 /* 1185 * the focusable is in the target area, it is a candidate for 1186 * focusing 1187 */ 1188 1189 final boolean viewIsFullyContained = (top < viewTop) && 1190 (viewBottom < bottom); 1191 1192 if (focusCandidate == null) { 1193 /* No candidate, take this one */ 1194 focusCandidate = view; 1195 foundFullyContainedFocusable = viewIsFullyContained; 1196 } else { 1197 final boolean viewIsCloserToBoundary = 1198 (topFocus && viewTop < focusCandidate.getTop()) || 1199 (!topFocus && viewBottom > focusCandidate 1200 .getBottom()); 1201 1202 if (foundFullyContainedFocusable) { 1203 if (viewIsFullyContained && viewIsCloserToBoundary) { 1204 /* 1205 * We're dealing with only fully contained views, so 1206 * it has to be closer to the boundary to beat our 1207 * candidate 1208 */ 1209 focusCandidate = view; 1210 } 1211 } else { 1212 if (viewIsFullyContained) { 1213 /* Any fully contained view beats a partially contained view */ 1214 focusCandidate = view; 1215 foundFullyContainedFocusable = true; 1216 } else if (viewIsCloserToBoundary) { 1217 /* 1218 * Partially contained view beats another partially 1219 * contained view if it's closer 1220 */ 1221 focusCandidate = view; 1222 } 1223 } 1224 } 1225 } 1226 } 1227 1228 return focusCandidate; 1229 } 1230 1231 /** 1232 * <p>Handles scrolling in response to a "page up/down" shortcut press. This 1233 * method will scroll the view by one page up or down and give the focus 1234 * to the topmost/bottommost component in the new visible area. If no 1235 * component is a good candidate for focus, this scrollview reclaims the 1236 * focus.</p> 1237 * 1238 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1239 * to go one page up or 1240 * {@link android.view.View#FOCUS_DOWN} to go one page down 1241 * @return true if the key event is consumed by this method, false otherwise 1242 */ pageScroll(int direction)1243 public boolean pageScroll(int direction) { 1244 boolean down = direction == View.FOCUS_DOWN; 1245 int height = getHeight(); 1246 1247 if (down) { 1248 mTempRect.top = getScrollY() + height; 1249 int count = getChildCount(); 1250 if (count > 0) { 1251 View view = getChildAt(count - 1); 1252 if (mTempRect.top + height > view.getBottom()) { 1253 mTempRect.top = view.getBottom() - height; 1254 } 1255 } 1256 } else { 1257 mTempRect.top = getScrollY() - height; 1258 if (mTempRect.top < 0) { 1259 mTempRect.top = 0; 1260 } 1261 } 1262 mTempRect.bottom = mTempRect.top + height; 1263 1264 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1265 } 1266 1267 /** 1268 * <p>Handles scrolling in response to a "home/end" shortcut press. This 1269 * method will scroll the view to the top or bottom and give the focus 1270 * to the topmost/bottommost component in the new visible area. If no 1271 * component is a good candidate for focus, this scrollview reclaims the 1272 * focus.</p> 1273 * 1274 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1275 * to go the top of the view or 1276 * {@link android.view.View#FOCUS_DOWN} to go the bottom 1277 * @return true if the key event is consumed by this method, false otherwise 1278 */ fullScroll(int direction)1279 public boolean fullScroll(int direction) { 1280 boolean down = direction == View.FOCUS_DOWN; 1281 int height = getHeight(); 1282 1283 mTempRect.top = 0; 1284 mTempRect.bottom = height; 1285 1286 if (down) { 1287 int count = getChildCount(); 1288 if (count > 0) { 1289 View view = getChildAt(count - 1); 1290 mTempRect.bottom = view.getBottom() + mPaddingBottom; 1291 mTempRect.top = mTempRect.bottom - height; 1292 } 1293 } 1294 1295 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); 1296 } 1297 1298 /** 1299 * <p>Scrolls the view to make the area defined by <code>top</code> and 1300 * <code>bottom</code> visible. This method attempts to give the focus 1301 * to a component visible in this area. If no component can be focused in 1302 * the new visible area, the focus is reclaimed by this ScrollView.</p> 1303 * 1304 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} 1305 * to go upward, {@link android.view.View#FOCUS_DOWN} to downward 1306 * @param top the top offset of the new area to be made visible 1307 * @param bottom the bottom offset of the new area to be made visible 1308 * @return true if the key event is consumed by this method, false otherwise 1309 */ scrollAndFocus(int direction, int top, int bottom)1310 private boolean scrollAndFocus(int direction, int top, int bottom) { 1311 boolean handled = true; 1312 1313 int height = getHeight(); 1314 int containerTop = getScrollY(); 1315 int containerBottom = containerTop + height; 1316 boolean up = direction == View.FOCUS_UP; 1317 1318 View newFocused = findFocusableViewInBounds(up, top, bottom); 1319 if (newFocused == null) { 1320 newFocused = this; 1321 } 1322 1323 if (top >= containerTop && bottom <= containerBottom) { 1324 handled = false; 1325 } else { 1326 int delta = up ? (top - containerTop) : (bottom - containerBottom); 1327 doScrollY(delta); 1328 } 1329 1330 if (newFocused != findFocus()) newFocused.requestFocus(direction); 1331 1332 return handled; 1333 } 1334 1335 /** 1336 * Handle scrolling in response to an up or down arrow click. 1337 * 1338 * @param direction The direction corresponding to the arrow key that was 1339 * pressed 1340 * @return True if we consumed the event, false otherwise 1341 */ arrowScroll(int direction)1342 public boolean arrowScroll(int direction) { 1343 1344 View currentFocused = findFocus(); 1345 if (currentFocused == this) currentFocused = null; 1346 1347 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); 1348 1349 final int maxJump = getMaxScrollAmount(); 1350 1351 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { 1352 nextFocused.getDrawingRect(mTempRect); 1353 offsetDescendantRectToMyCoords(nextFocused, mTempRect); 1354 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1355 doScrollY(scrollDelta); 1356 nextFocused.requestFocus(direction); 1357 } else { 1358 // no new focus 1359 int scrollDelta = maxJump; 1360 1361 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { 1362 scrollDelta = getScrollY(); 1363 } else if (direction == View.FOCUS_DOWN) { 1364 if (getChildCount() > 0) { 1365 int daBottom = getChildAt(0).getBottom(); 1366 int screenBottom = getScrollY() + getHeight() - mPaddingBottom; 1367 if (daBottom - screenBottom < maxJump) { 1368 scrollDelta = daBottom - screenBottom; 1369 } 1370 } 1371 } 1372 if (scrollDelta == 0) { 1373 return false; 1374 } 1375 doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); 1376 } 1377 1378 if (currentFocused != null && currentFocused.isFocused() 1379 && isOffScreen(currentFocused)) { 1380 // previously focused item still has focus and is off screen, give 1381 // it up (take it back to ourselves) 1382 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are 1383 // sure to 1384 // get it) 1385 final int descendantFocusability = getDescendantFocusability(); // save 1386 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 1387 requestFocus(); 1388 setDescendantFocusability(descendantFocusability); // restore 1389 } 1390 return true; 1391 } 1392 1393 /** 1394 * @return whether the descendant of this scroll view is scrolled off 1395 * screen. 1396 */ isOffScreen(View descendant)1397 private boolean isOffScreen(View descendant) { 1398 return !isWithinDeltaOfScreen(descendant, 0, getHeight()); 1399 } 1400 1401 /** 1402 * @return whether the descendant of this scroll view is within delta 1403 * pixels of being on the screen. 1404 */ isWithinDeltaOfScreen(View descendant, int delta, int height)1405 private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { 1406 descendant.getDrawingRect(mTempRect); 1407 offsetDescendantRectToMyCoords(descendant, mTempRect); 1408 1409 return (mTempRect.bottom + delta) >= getScrollY() 1410 && (mTempRect.top - delta) <= (getScrollY() + height); 1411 } 1412 1413 /** 1414 * Smooth scroll by a Y delta 1415 * 1416 * @param delta the number of pixels to scroll by on the Y axis 1417 */ doScrollY(int delta)1418 private void doScrollY(int delta) { 1419 if (delta != 0) { 1420 if (mSmoothScrollingEnabled) { 1421 smoothScrollBy(0, delta); 1422 } else { 1423 scrollBy(0, delta); 1424 } 1425 } 1426 } 1427 1428 /** 1429 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 1430 * 1431 * @param dx the number of pixels to scroll by on the X axis 1432 * @param dy the number of pixels to scroll by on the Y axis 1433 */ smoothScrollBy(int dx, int dy)1434 public final void smoothScrollBy(int dx, int dy) { 1435 if (getChildCount() == 0) { 1436 // Nothing to do. 1437 return; 1438 } 1439 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; 1440 if (duration > ANIMATED_SCROLL_GAP) { 1441 final int height = getHeight() - mPaddingBottom - mPaddingTop; 1442 final int bottom = getChildAt(0).getHeight(); 1443 final int maxY = Math.max(0, bottom - height); 1444 final int scrollY = mScrollY; 1445 dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; 1446 1447 mScroller.startScroll(mScrollX, scrollY, 0, dy); 1448 postInvalidateOnAnimation(); 1449 } else { 1450 if (!mScroller.isFinished()) { 1451 mScroller.abortAnimation(); 1452 if (mFlingStrictSpan != null) { 1453 mFlingStrictSpan.finish(); 1454 mFlingStrictSpan = null; 1455 } 1456 } 1457 scrollBy(dx, dy); 1458 } 1459 mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 1460 } 1461 1462 /** 1463 * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 1464 * 1465 * @param x the position where to scroll on the X axis 1466 * @param y the position where to scroll on the Y axis 1467 */ smoothScrollTo(int x, int y)1468 public final void smoothScrollTo(int x, int y) { 1469 smoothScrollBy(x - mScrollX, y - mScrollY); 1470 } 1471 1472 /** 1473 * <p>The scroll range of a scroll view is the overall height of all of its 1474 * children.</p> 1475 */ 1476 @Override computeVerticalScrollRange()1477 protected int computeVerticalScrollRange() { 1478 final int count = getChildCount(); 1479 final int contentHeight = getHeight() - mPaddingBottom - mPaddingTop; 1480 if (count == 0) { 1481 return contentHeight; 1482 } 1483 1484 int scrollRange = getChildAt(0).getBottom(); 1485 final int scrollY = mScrollY; 1486 final int overscrollBottom = Math.max(0, scrollRange - contentHeight); 1487 if (scrollY < 0) { 1488 scrollRange -= scrollY; 1489 } else if (scrollY > overscrollBottom) { 1490 scrollRange += scrollY - overscrollBottom; 1491 } 1492 1493 return scrollRange; 1494 } 1495 1496 @Override computeVerticalScrollOffset()1497 protected int computeVerticalScrollOffset() { 1498 return Math.max(0, super.computeVerticalScrollOffset()); 1499 } 1500 1501 @Override measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)1502 protected void measureChild(View child, int parentWidthMeasureSpec, 1503 int parentHeightMeasureSpec) { 1504 ViewGroup.LayoutParams lp = child.getLayoutParams(); 1505 1506 int childWidthMeasureSpec; 1507 int childHeightMeasureSpec; 1508 1509 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft 1510 + mPaddingRight, lp.width); 1511 final int verticalPadding = mPaddingTop + mPaddingBottom; 1512 childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( 1513 Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding), 1514 MeasureSpec.UNSPECIFIED); 1515 1516 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1517 } 1518 1519 @Override measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)1520 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 1521 int parentHeightMeasureSpec, int heightUsed) { 1522 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 1523 1524 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, 1525 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin 1526 + widthUsed, lp.width); 1527 final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + 1528 heightUsed; 1529 final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec( 1530 Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal), 1531 MeasureSpec.UNSPECIFIED); 1532 1533 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 1534 } 1535 1536 @Override computeScroll()1537 public void computeScroll() { 1538 if (mScroller.computeScrollOffset()) { 1539 // This is called at drawing time by ViewGroup. We don't want to 1540 // re-show the scrollbars at this point, which scrollTo will do, 1541 // so we replicate most of scrollTo here. 1542 // 1543 // It's a little odd to call onScrollChanged from inside the drawing. 1544 // 1545 // It is, except when you remember that computeScroll() is used to 1546 // animate scrolling. So unless we want to defer the onScrollChanged() 1547 // until the end of the animated scrolling, we don't really have a 1548 // choice here. 1549 // 1550 // I agree. The alternative, which I think would be worse, is to post 1551 // something and tell the subclasses later. This is bad because there 1552 // will be a window where mScrollX/Y is different from what the app 1553 // thinks it is. 1554 // 1555 int oldX = mScrollX; 1556 int oldY = mScrollY; 1557 int x = mScroller.getCurrX(); 1558 int y = mScroller.getCurrY(); 1559 int deltaY = consumeFlingInStretch(y - oldY); 1560 1561 if (oldX != x || deltaY != 0) { 1562 final int range = getScrollRange(); 1563 final int overscrollMode = getOverScrollMode(); 1564 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS || 1565 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); 1566 1567 overScrollBy(x - oldX, deltaY, oldX, oldY, 0, range, 1568 0, mOverflingDistance, false); 1569 onScrollChanged(mScrollX, mScrollY, oldX, oldY); 1570 1571 if (canOverscroll && deltaY != 0) { 1572 if (y < 0 && oldY >= 0) { 1573 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); 1574 } else if (y > range && oldY <= range) { 1575 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); 1576 } 1577 } 1578 } 1579 1580 if (!awakenScrollBars()) { 1581 // Keep on drawing until the animation has finished. 1582 postInvalidateOnAnimation(); 1583 } 1584 1585 // For variable refresh rate project to track the current velocity of this View 1586 if (viewVelocityApi()) { 1587 setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); 1588 } 1589 } else { 1590 if (mFlingStrictSpan != null) { 1591 mFlingStrictSpan.finish(); 1592 mFlingStrictSpan = null; 1593 } 1594 } 1595 } 1596 1597 /** 1598 * Used by consumeFlingInHorizontalStretch() and consumeFlinInVerticalStretch() for 1599 * consuming deltas from EdgeEffects 1600 * @param unconsumed The unconsumed delta that the EdgeEffets may consume 1601 * @return The unconsumed delta after the EdgeEffects have had an opportunity to consume. 1602 */ consumeFlingInStretch(int unconsumed)1603 private int consumeFlingInStretch(int unconsumed) { 1604 int scrollY = getScrollY(); 1605 if (scrollY < 0 || scrollY > getScrollRange()) { 1606 // We've overscrolled, so don't stretch 1607 return unconsumed; 1608 } 1609 if (unconsumed > 0 && mEdgeGlowTop != null && mEdgeGlowTop.getDistance() != 0f) { 1610 int size = getHeight(); 1611 float deltaDistance = -unconsumed * FLING_DESTRETCH_FACTOR / size; 1612 int consumed = Math.round(-size / FLING_DESTRETCH_FACTOR 1613 * mEdgeGlowTop.onPullDistance(deltaDistance, 0.5f)); 1614 mEdgeGlowTop.onRelease(); 1615 if (consumed != unconsumed) { 1616 mEdgeGlowTop.finish(); 1617 } 1618 return unconsumed - consumed; 1619 } 1620 if (unconsumed < 0 && mEdgeGlowBottom != null && mEdgeGlowBottom.getDistance() != 0f) { 1621 int size = getHeight(); 1622 float deltaDistance = unconsumed * FLING_DESTRETCH_FACTOR / size; 1623 int consumed = Math.round(size / FLING_DESTRETCH_FACTOR 1624 * mEdgeGlowBottom.onPullDistance(deltaDistance, 0.5f)); 1625 mEdgeGlowBottom.onRelease(); 1626 if (consumed != unconsumed) { 1627 mEdgeGlowBottom.finish(); 1628 } 1629 return unconsumed - consumed; 1630 } 1631 return unconsumed; 1632 } 1633 1634 /** 1635 * Scrolls the view to the given child. 1636 * 1637 * @param child the View to scroll to 1638 */ scrollToDescendant(@onNull View child)1639 public void scrollToDescendant(@NonNull View child) { 1640 if (!mIsLayoutDirty) { 1641 child.getDrawingRect(mTempRect); 1642 1643 /* Offset from child's local coordinates to ScrollView coordinates */ 1644 offsetDescendantRectToMyCoords(child, mTempRect); 1645 1646 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1647 1648 if (scrollDelta != 0) { 1649 scrollBy(0, scrollDelta); 1650 } 1651 } else { 1652 mChildToScrollTo = child; 1653 } 1654 } 1655 1656 /** 1657 * If rect is off screen, scroll just enough to get it (or at least the 1658 * first screen size chunk of it) on screen. 1659 * 1660 * @param rect The rectangle. 1661 * @param immediate True to scroll immediately without animation 1662 * @return true if scrolling was performed 1663 */ scrollToChildRect(Rect rect, boolean immediate)1664 private boolean scrollToChildRect(Rect rect, boolean immediate) { 1665 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 1666 final boolean scroll = delta != 0; 1667 if (scroll) { 1668 if (immediate) { 1669 scrollBy(0, delta); 1670 } else { 1671 smoothScrollBy(0, delta); 1672 } 1673 } 1674 return scroll; 1675 } 1676 1677 /** 1678 * Compute the amount to scroll in the Y direction in order to get 1679 * a rectangle completely on the screen (or, if taller than the screen, 1680 * at least the first screen size chunk of it). 1681 * 1682 * @param rect The rect. 1683 * @return The scroll delta. 1684 */ computeScrollDeltaToGetChildRectOnScreen(Rect rect)1685 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 1686 if (getChildCount() == 0) return 0; 1687 1688 int height = getHeight(); 1689 int screenTop = getScrollY(); 1690 int screenBottom = screenTop + height; 1691 1692 int fadingEdge = getVerticalFadingEdgeLength(); 1693 1694 // leave room for top fading edge as long as rect isn't at very top 1695 if (rect.top > 0) { 1696 screenTop += fadingEdge; 1697 } 1698 1699 // leave room for bottom fading edge as long as rect isn't at very bottom 1700 if (rect.bottom < getChildAt(0).getHeight()) { 1701 screenBottom -= fadingEdge; 1702 } 1703 1704 int scrollYDelta = 0; 1705 1706 if (rect.bottom > screenBottom && rect.top > screenTop) { 1707 // need to move down to get it in view: move down just enough so 1708 // that the entire rectangle is in view (or at least the first 1709 // screen size chunk). 1710 1711 if (rect.height() > height) { 1712 // just enough to get screen size chunk on 1713 scrollYDelta += (rect.top - screenTop); 1714 } else { 1715 // get entire rect at bottom of screen 1716 scrollYDelta += (rect.bottom - screenBottom); 1717 } 1718 1719 // make sure we aren't scrolling beyond the end of our content 1720 int bottom = getChildAt(0).getBottom(); 1721 int distanceToBottom = bottom - screenBottom; 1722 scrollYDelta = Math.min(scrollYDelta, distanceToBottom); 1723 1724 } else if (rect.top < screenTop && rect.bottom < screenBottom) { 1725 // need to move up to get it in view: move up just enough so that 1726 // entire rectangle is in view (or at least the first screen 1727 // size chunk of it). 1728 1729 if (rect.height() > height) { 1730 // screen size chunk 1731 scrollYDelta -= (screenBottom - rect.bottom); 1732 } else { 1733 // entire rect at top 1734 scrollYDelta -= (screenTop - rect.top); 1735 } 1736 1737 // make sure we aren't scrolling any further than the top our content 1738 scrollYDelta = Math.max(scrollYDelta, -getScrollY()); 1739 } 1740 return scrollYDelta; 1741 } 1742 1743 @Override requestChildFocus(View child, View focused)1744 public void requestChildFocus(View child, View focused) { 1745 if (focused != null && focused.getRevealOnFocusHint()) { 1746 if (!mIsLayoutDirty) { 1747 scrollToDescendant(focused); 1748 } else { 1749 // The child may not be laid out yet, we can't compute the scroll yet 1750 mChildToScrollTo = focused; 1751 } 1752 } 1753 super.requestChildFocus(child, focused); 1754 } 1755 1756 1757 /** 1758 * When looking for focus in children of a scroll view, need to be a little 1759 * more careful not to give focus to something that is scrolled off screen. 1760 * 1761 * This is more expensive than the default {@link android.view.ViewGroup} 1762 * implementation, otherwise this behavior might have been made the default. 1763 */ 1764 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)1765 protected boolean onRequestFocusInDescendants(int direction, 1766 Rect previouslyFocusedRect) { 1767 1768 // convert from forward / backward notation to up / down / left / right 1769 // (ugh). 1770 if (direction == View.FOCUS_FORWARD) { 1771 direction = View.FOCUS_DOWN; 1772 } else if (direction == View.FOCUS_BACKWARD) { 1773 direction = View.FOCUS_UP; 1774 } 1775 1776 final View nextFocus = previouslyFocusedRect == null ? 1777 FocusFinder.getInstance().findNextFocus(this, null, direction) : 1778 FocusFinder.getInstance().findNextFocusFromRect(this, 1779 previouslyFocusedRect, direction); 1780 1781 if (nextFocus == null) { 1782 return false; 1783 } 1784 1785 if (isOffScreen(nextFocus)) { 1786 return false; 1787 } 1788 1789 return nextFocus.requestFocus(direction, previouslyFocusedRect); 1790 } 1791 1792 @Override requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)1793 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, 1794 boolean immediate) { 1795 // offset into coordinate space of this scroll view 1796 rectangle.offset(child.getLeft() - child.getScrollX(), 1797 child.getTop() - child.getScrollY()); 1798 1799 return scrollToChildRect(rectangle, immediate); 1800 } 1801 1802 @Override requestLayout()1803 public void requestLayout() { 1804 mIsLayoutDirty = true; 1805 super.requestLayout(); 1806 } 1807 1808 @Override onDetachedFromWindow()1809 protected void onDetachedFromWindow() { 1810 super.onDetachedFromWindow(); 1811 1812 if (mScrollStrictSpan != null) { 1813 mScrollStrictSpan.finish(); 1814 mScrollStrictSpan = null; 1815 } 1816 if (mFlingStrictSpan != null) { 1817 mFlingStrictSpan.finish(); 1818 mFlingStrictSpan = null; 1819 } 1820 } 1821 1822 @Override onLayout(boolean changed, int l, int t, int r, int b)1823 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1824 super.onLayout(changed, l, t, r, b); 1825 mIsLayoutDirty = false; 1826 // Give a child focus if it needs it 1827 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { 1828 scrollToDescendant(mChildToScrollTo); 1829 } 1830 mChildToScrollTo = null; 1831 1832 if (!isLaidOut()) { 1833 if (mSavedState != null) { 1834 mScrollY = mSavedState.scrollPosition; 1835 mSavedState = null; 1836 } // mScrollY default value is "0" 1837 1838 final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; 1839 final int scrollRange = Math.max(0, 1840 childHeight - (b - t - mPaddingBottom - mPaddingTop)); 1841 1842 // Don't forget to clamp 1843 if (mScrollY > scrollRange) { 1844 mScrollY = scrollRange; 1845 } else if (mScrollY < 0) { 1846 mScrollY = 0; 1847 } 1848 } 1849 1850 // Calling this with the present values causes it to re-claim them 1851 scrollTo(mScrollX, mScrollY); 1852 } 1853 1854 @Override onSizeChanged(int w, int h, int oldw, int oldh)1855 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1856 super.onSizeChanged(w, h, oldw, oldh); 1857 1858 View currentFocused = findFocus(); 1859 if (null == currentFocused || this == currentFocused) 1860 return; 1861 1862 // If the currently-focused view was visible on the screen when the 1863 // screen was at the old height, then scroll the screen to make that 1864 // view visible with the new screen height. 1865 if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { 1866 currentFocused.getDrawingRect(mTempRect); 1867 offsetDescendantRectToMyCoords(currentFocused, mTempRect); 1868 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1869 doScrollY(scrollDelta); 1870 } 1871 } 1872 1873 /** 1874 * Return true if child is a descendant of parent, (or equal to the parent). 1875 */ isViewDescendantOf(View child, View parent)1876 private static boolean isViewDescendantOf(View child, View parent) { 1877 if (child == parent) { 1878 return true; 1879 } 1880 1881 final ViewParent theParent = child.getParent(); 1882 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); 1883 } 1884 1885 /** 1886 * Fling the scroll view 1887 * 1888 * @param velocityY The initial velocity in the Y direction. Positive 1889 * numbers mean that the finger/cursor is moving down the screen, 1890 * which means we want to scroll towards the top. 1891 */ fling(int velocityY)1892 public void fling(int velocityY) { 1893 if (getChildCount() > 0) { 1894 int height = getHeight() - mPaddingBottom - mPaddingTop; 1895 int bottom = getChildAt(0).getHeight(); 1896 1897 mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0, 1898 Math.max(0, bottom - height), 0, height/2); 1899 1900 // For variable refresh rate project to track the current velocity of this View 1901 if (viewVelocityApi()) { 1902 setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity())); 1903 } 1904 if (mFlingStrictSpan == null) { 1905 mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling"); 1906 } 1907 1908 postInvalidateOnAnimation(); 1909 } 1910 } 1911 flingWithNestedDispatch(int velocityY)1912 private void flingWithNestedDispatch(int velocityY) { 1913 final boolean canFling = (mScrollY > 0 || velocityY > 0) && 1914 (mScrollY < getScrollRange() || velocityY < 0); 1915 if (!dispatchNestedPreFling(0, velocityY)) { 1916 final boolean consumed = dispatchNestedFling(0, velocityY, canFling); 1917 if (canFling) { 1918 fling(velocityY); 1919 } else if (!consumed) { 1920 if (!mEdgeGlowTop.isFinished()) { 1921 if (shouldAbsorb(mEdgeGlowTop, -velocityY)) { 1922 mEdgeGlowTop.onAbsorb(-velocityY); 1923 } else { 1924 fling(velocityY); 1925 } 1926 } else if (!mEdgeGlowBottom.isFinished()) { 1927 if (shouldAbsorb(mEdgeGlowBottom, velocityY)) { 1928 mEdgeGlowBottom.onAbsorb(velocityY); 1929 } else { 1930 fling(velocityY); 1931 } 1932 } 1933 } 1934 } 1935 } 1936 1937 /** 1938 * Returns true if edgeEffect should call onAbsorb() with veclocity or false if it should 1939 * animate with a fling. It will animate with a fling if the velocity will remove the 1940 * EdgeEffect through its normal operation. 1941 * 1942 * @param edgeEffect The EdgeEffect that might absorb the velocity. 1943 * @param velocity The velocity of the fling motion 1944 * @return true if the velocity should be absorbed or false if it should be flung. 1945 */ shouldAbsorb(EdgeEffect edgeEffect, int velocity)1946 private boolean shouldAbsorb(EdgeEffect edgeEffect, int velocity) { 1947 if (velocity > 0) { 1948 return true; 1949 } 1950 float distance = edgeEffect.getDistance() * getHeight(); 1951 1952 // This is flinging without the spring, so let's see if it will fling past the overscroll 1953 float flingDistance = (float) mScroller.getSplineFlingDistance(-velocity); 1954 1955 return flingDistance < distance; 1956 } 1957 1958 @UnsupportedAppUsage endDrag()1959 private void endDrag() { 1960 mIsBeingDragged = false; 1961 1962 recycleVelocityTracker(); 1963 1964 if (shouldDisplayEdgeEffects()) { 1965 mEdgeGlowTop.onRelease(); 1966 mEdgeGlowBottom.onRelease(); 1967 } 1968 1969 if (mScrollStrictSpan != null) { 1970 mScrollStrictSpan.finish(); 1971 mScrollStrictSpan = null; 1972 } 1973 } 1974 1975 /** 1976 * {@inheritDoc} 1977 * 1978 * <p>This version also clamps the scrolling to the bounds of our child. 1979 */ 1980 @Override scrollTo(int x, int y)1981 public void scrollTo(int x, int y) { 1982 // we rely on the fact the View.scrollBy calls scrollTo. 1983 if (getChildCount() > 0) { 1984 View child = getChildAt(0); 1985 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); 1986 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); 1987 if (x != mScrollX || y != mScrollY) { 1988 super.scrollTo(x, y); 1989 } 1990 } 1991 } 1992 1993 @Override onStartNestedScroll(View child, View target, int nestedScrollAxes)1994 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 1995 return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0; 1996 } 1997 1998 @Override onNestedScrollAccepted(View child, View target, int axes)1999 public void onNestedScrollAccepted(View child, View target, int axes) { 2000 super.onNestedScrollAccepted(child, target, axes); 2001 startNestedScroll(SCROLL_AXIS_VERTICAL); 2002 } 2003 2004 /** 2005 * @inheritDoc 2006 */ 2007 @Override onStopNestedScroll(View target)2008 public void onStopNestedScroll(View target) { 2009 super.onStopNestedScroll(target); 2010 } 2011 2012 @Override onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)2013 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 2014 int dxUnconsumed, int dyUnconsumed) { 2015 final int oldScrollY = mScrollY; 2016 scrollBy(0, dyUnconsumed); 2017 final int myConsumed = mScrollY - oldScrollY; 2018 final int myUnconsumed = dyUnconsumed - myConsumed; 2019 dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); 2020 } 2021 2022 /** 2023 * @inheritDoc 2024 */ 2025 @Override onNestedFling(View target, float velocityX, float velocityY, boolean consumed)2026 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 2027 if (!consumed) { 2028 flingWithNestedDispatch((int) velocityY); 2029 return true; 2030 } 2031 return false; 2032 } 2033 2034 @Override draw(Canvas canvas)2035 public void draw(Canvas canvas) { 2036 super.draw(canvas); 2037 if (shouldDisplayEdgeEffects()) { 2038 final int scrollY = mScrollY; 2039 final boolean clipToPadding = getClipToPadding(); 2040 if (!mEdgeGlowTop.isFinished()) { 2041 final int restoreCount = canvas.save(); 2042 final int width; 2043 final int height; 2044 final float translateX; 2045 final float translateY; 2046 if (clipToPadding) { 2047 width = getWidth() - mPaddingLeft - mPaddingRight; 2048 height = getHeight() - mPaddingTop - mPaddingBottom; 2049 translateX = mPaddingLeft; 2050 translateY = mPaddingTop; 2051 } else { 2052 width = getWidth(); 2053 height = getHeight(); 2054 translateX = 0; 2055 translateY = 0; 2056 } 2057 canvas.translate(translateX, Math.min(0, scrollY) + translateY); 2058 mEdgeGlowTop.setSize(width, height); 2059 if (mEdgeGlowTop.draw(canvas)) { 2060 postInvalidateOnAnimation(); 2061 } 2062 canvas.restoreToCount(restoreCount); 2063 } 2064 if (!mEdgeGlowBottom.isFinished()) { 2065 final int restoreCount = canvas.save(); 2066 final int width; 2067 final int height; 2068 final float translateX; 2069 final float translateY; 2070 if (clipToPadding) { 2071 width = getWidth() - mPaddingLeft - mPaddingRight; 2072 height = getHeight() - mPaddingTop - mPaddingBottom; 2073 translateX = mPaddingLeft; 2074 translateY = mPaddingTop; 2075 } else { 2076 width = getWidth(); 2077 height = getHeight(); 2078 translateX = 0; 2079 translateY = 0; 2080 } 2081 canvas.translate(-width + translateX, 2082 Math.max(getScrollRange(), scrollY) + height + translateY); 2083 canvas.rotate(180, width, 0); 2084 mEdgeGlowBottom.setSize(width, height); 2085 if (mEdgeGlowBottom.draw(canvas)) { 2086 postInvalidateOnAnimation(); 2087 } 2088 canvas.restoreToCount(restoreCount); 2089 } 2090 } 2091 } 2092 clamp(int n, int my, int child)2093 private static int clamp(int n, int my, int child) { 2094 if (my >= child || n < 0) { 2095 /* my >= child is this case: 2096 * |--------------- me ---------------| 2097 * |------ child ------| 2098 * or 2099 * |--------------- me ---------------| 2100 * |------ child ------| 2101 * or 2102 * |--------------- me ---------------| 2103 * |------ child ------| 2104 * 2105 * n < 0 is this case: 2106 * |------ me ------| 2107 * |-------- child --------| 2108 * |-- mScrollX --| 2109 */ 2110 return 0; 2111 } 2112 if ((my+n) > child) { 2113 /* this case: 2114 * |------ me ------| 2115 * |------ child ------| 2116 * |-- mScrollX --| 2117 */ 2118 return child-my; 2119 } 2120 return n; 2121 } 2122 2123 @Override onRestoreInstanceState(Parcelable state)2124 protected void onRestoreInstanceState(Parcelable state) { 2125 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) { 2126 // Some old apps reused IDs in ways they shouldn't have. 2127 // Don't break them, but they don't get scroll state restoration. 2128 super.onRestoreInstanceState(state); 2129 return; 2130 } 2131 SavedState ss = (SavedState) state; 2132 super.onRestoreInstanceState(ss.getSuperState()); 2133 mSavedState = ss; 2134 requestLayout(); 2135 } 2136 2137 @Override onSaveInstanceState()2138 protected Parcelable onSaveInstanceState() { 2139 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) { 2140 // Some old apps reused IDs in ways they shouldn't have. 2141 // Don't break them, but they don't get scroll state restoration. 2142 return super.onSaveInstanceState(); 2143 } 2144 Parcelable superState = super.onSaveInstanceState(); 2145 SavedState ss = new SavedState(superState); 2146 ss.scrollPosition = mScrollY; 2147 return ss; 2148 } 2149 2150 /** @hide */ 2151 @Override encodeProperties(@onNull ViewHierarchyEncoder encoder)2152 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { 2153 super.encodeProperties(encoder); 2154 encoder.addProperty("fillViewport", mFillViewport); 2155 } 2156 2157 static class SavedState extends BaseSavedState { 2158 public int scrollPosition; 2159 SavedState(Parcelable superState)2160 SavedState(Parcelable superState) { 2161 super(superState); 2162 } 2163 SavedState(Parcel source)2164 public SavedState(Parcel source) { 2165 super(source); 2166 scrollPosition = source.readInt(); 2167 } 2168 2169 @Override writeToParcel(Parcel dest, int flags)2170 public void writeToParcel(Parcel dest, int flags) { 2171 super.writeToParcel(dest, flags); 2172 dest.writeInt(scrollPosition); 2173 } 2174 2175 @Override toString()2176 public String toString() { 2177 return "ScrollView.SavedState{" 2178 + Integer.toHexString(System.identityHashCode(this)) 2179 + " scrollPosition=" + scrollPosition + "}"; 2180 } 2181 2182 public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR 2183 = new Parcelable.Creator<SavedState>() { 2184 public SavedState createFromParcel(Parcel in) { 2185 return new SavedState(in); 2186 } 2187 2188 public SavedState[] newArray(int size) { 2189 return new SavedState[size]; 2190 } 2191 }; 2192 } 2193 2194 private class DifferentialFlingTarget 2195 implements DifferentialMotionFlingHelper.DifferentialMotionFlingTarget { 2196 @Override startDifferentialMotionFling(float velocity)2197 public boolean startDifferentialMotionFling(float velocity) { 2198 stopDifferentialMotionFling(); 2199 fling((int) velocity); 2200 return true; 2201 } 2202 2203 @Override stopDifferentialMotionFling()2204 public void stopDifferentialMotionFling() { 2205 mScroller.abortAnimation(); 2206 } 2207 2208 @Override getScaledScrollFactor()2209 public float getScaledScrollFactor() { 2210 return -mVerticalScrollFactor; 2211 } 2212 } 2213 } 2214