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