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