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