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