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