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