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