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