1 /* 2 * Copyright (C) 2009 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.util.AttributeSet; 20 import android.graphics.Rect; 21 import android.view.View; 22 import android.view.VelocityTracker; 23 import android.view.ViewConfiguration; 24 import android.view.ViewGroup; 25 import android.view.KeyEvent; 26 import android.view.FocusFinder; 27 import android.view.MotionEvent; 28 import android.view.ViewParent; 29 import android.view.animation.AnimationUtils; 30 import android.content.Context; 31 import android.content.res.TypedArray; 32 33 import java.util.List; 34 35 /** 36 * Layout container for a view hierarchy that can be scrolled by the user, 37 * allowing it to be larger than the physical display. A HorizontalScrollView 38 * is a {@link FrameLayout}, meaning you should place one child in it 39 * containing the entire contents to scroll; this child may itself be a layout 40 * manager with a complex hierarchy of objects. A child that is often used 41 * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal 42 * array of top-level items that the user can scroll through. 43 * 44 * <p>You should never use a HorizontalScrollView with a {@link ListView}, since 45 * ListView takes care of its own scrolling. Most importantly, doing this 46 * defeats all of the important optimizations in ListView for dealing with 47 * large lists, since it effectively forces the ListView to display its entire 48 * list of items to fill up the infinite container supplied by HorizontalScrollView. 49 * 50 * <p>The {@link TextView} class also 51 * takes care of its own scrolling, so does not require a ScrollView, but 52 * using the two together is possible to achieve the effect of a text view 53 * within a larger container. 54 * 55 * <p>HorizontalScrollView only supports horizontal scrolling. 56 */ 57 public class HorizontalScrollView extends FrameLayout { 58 private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP; 59 60 private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR; 61 62 63 private long mLastScroll; 64 65 private final Rect mTempRect = new Rect(); 66 private Scroller mScroller; 67 68 /** 69 * Flag to indicate that we are moving focus ourselves. This is so the 70 * code that watches for focus changes initiated outside this ScrollView 71 * knows that it does not have to do anything. 72 */ 73 private boolean mScrollViewMovedFocus; 74 75 /** 76 * Position of the last motion event. 77 */ 78 private float mLastMotionX; 79 80 /** 81 * True when the layout has changed but the traversal has not come through yet. 82 * Ideally the view hierarchy would keep track of this for us. 83 */ 84 private boolean mIsLayoutDirty = true; 85 86 /** 87 * The child to give focus to in the event that a child has requested focus while the 88 * layout is dirty. This prevents the scroll from being wrong if the child has not been 89 * laid out before requesting focus. 90 */ 91 private View mChildToScrollTo = null; 92 93 /** 94 * True if the user is currently dragging this ScrollView around. This is 95 * not the same as 'is being flinged', which can be checked by 96 * mScroller.isFinished() (flinging begins when the user lifts his finger). 97 */ 98 private boolean mIsBeingDragged = false; 99 100 /** 101 * Determines speed during touch scrolling 102 */ 103 private VelocityTracker mVelocityTracker; 104 105 /** 106 * When set to true, the scroll view measure its child to make it fill the currently 107 * visible area. 108 */ 109 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 HorizontalScrollView(Context context)120 public HorizontalScrollView(Context context) { 121 this(context, null); 122 } 123 HorizontalScrollView(Context context, AttributeSet attrs)124 public HorizontalScrollView(Context context, AttributeSet attrs) { 125 this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle); 126 } 127 HorizontalScrollView(Context context, AttributeSet attrs, int defStyle)128 public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) { 129 super(context, attrs, defStyle); 130 initScrollView(); 131 132 TypedArray a = context.obtainStyledAttributes(attrs, 133 android.R.styleable.HorizontalScrollView, defStyle, 0); 134 135 setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false)); 136 137 a.recycle(); 138 } 139 140 @Override getLeftFadingEdgeStrength()141 protected float getLeftFadingEdgeStrength() { 142 if (getChildCount() == 0) { 143 return 0.0f; 144 } 145 146 final int length = getHorizontalFadingEdgeLength(); 147 if (mScrollX < length) { 148 return mScrollX / (float) length; 149 } 150 151 return 1.0f; 152 } 153 154 @Override getRightFadingEdgeStrength()155 protected float getRightFadingEdgeStrength() { 156 if (getChildCount() == 0) { 157 return 0.0f; 158 } 159 160 final int length = getHorizontalFadingEdgeLength(); 161 final int rightEdge = getWidth() - mPaddingRight; 162 final int span = getChildAt(0).getRight() - mScrollX - rightEdge; 163 if (span < length) { 164 return span / (float) length; 165 } 166 167 return 1.0f; 168 } 169 170 /** 171 * @return The maximum amount this scroll view will scroll in response to 172 * an arrow event. 173 */ getMaxScrollAmount()174 public int getMaxScrollAmount() { 175 return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft)); 176 } 177 178 initScrollView()179 private void initScrollView() { 180 mScroller = new Scroller(getContext()); 181 setFocusable(true); 182 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 183 setWillNotDraw(false); 184 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 185 mTouchSlop = configuration.getScaledTouchSlop(); 186 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 187 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 188 } 189 190 @Override addView(View child)191 public void addView(View child) { 192 if (getChildCount() > 0) { 193 throw new IllegalStateException("HorizontalScrollView can host only one direct child"); 194 } 195 196 super.addView(child); 197 } 198 199 @Override addView(View child, int index)200 public void addView(View child, int index) { 201 if (getChildCount() > 0) { 202 throw new IllegalStateException("HorizontalScrollView can host only one direct child"); 203 } 204 205 super.addView(child, index); 206 } 207 208 @Override addView(View child, ViewGroup.LayoutParams params)209 public void addView(View child, ViewGroup.LayoutParams params) { 210 if (getChildCount() > 0) { 211 throw new IllegalStateException("HorizontalScrollView can host only one direct child"); 212 } 213 214 super.addView(child, params); 215 } 216 217 @Override addView(View child, int index, ViewGroup.LayoutParams params)218 public void addView(View child, int index, ViewGroup.LayoutParams params) { 219 if (getChildCount() > 0) { 220 throw new IllegalStateException("HorizontalScrollView can host only one direct child"); 221 } 222 223 super.addView(child, index, params); 224 } 225 226 /** 227 * @return Returns true this HorizontalScrollView can be scrolled 228 */ canScroll()229 private boolean canScroll() { 230 View child = getChildAt(0); 231 if (child != null) { 232 int childWidth = child.getWidth(); 233 return getWidth() < childWidth + mPaddingLeft + mPaddingRight ; 234 } 235 return false; 236 } 237 238 /** 239 * Indicates whether this ScrollView's content is stretched to fill the viewport. 240 * 241 * @return True if the content fills the viewport, false otherwise. 242 */ isFillViewport()243 public boolean isFillViewport() { 244 return mFillViewport; 245 } 246 247 /** 248 * Indicates this ScrollView whether it should stretch its content width to fill 249 * the viewport or not. 250 * 251 * @param fillViewport True to stretch the content's width to the viewport's 252 * boundaries, false otherwise. 253 */ setFillViewport(boolean fillViewport)254 public void setFillViewport(boolean fillViewport) { 255 if (fillViewport != mFillViewport) { 256 mFillViewport = fillViewport; 257 requestLayout(); 258 } 259 } 260 261 /** 262 * @return Whether arrow scrolling will animate its transition. 263 */ isSmoothScrollingEnabled()264 public boolean isSmoothScrollingEnabled() { 265 return mSmoothScrollingEnabled; 266 } 267 268 /** 269 * Set whether arrow scrolling will animate its transition. 270 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition 271 */ setSmoothScrollingEnabled(boolean smoothScrollingEnabled)272 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { 273 mSmoothScrollingEnabled = smoothScrollingEnabled; 274 } 275 276 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)277 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 278 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 279 280 if (!mFillViewport) { 281 return; 282 } 283 284 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 285 if (widthMode == MeasureSpec.UNSPECIFIED) { 286 return; 287 } 288 289 if (getChildCount() > 0) { 290 final View child = getChildAt(0); 291 int width = getMeasuredWidth(); 292 if (child.getMeasuredWidth() < width) { 293 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); 294 295 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop 296 + mPaddingBottom, lp.height); 297 width -= mPaddingLeft; 298 width -= mPaddingRight; 299 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); 300 301 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 302 } 303 } 304 } 305 306 @Override dispatchKeyEvent(KeyEvent event)307 public boolean dispatchKeyEvent(KeyEvent event) { 308 // Let the focused view and/or our descendants get the key first 309 boolean handled = super.dispatchKeyEvent(event); 310 if (handled) { 311 return true; 312 } 313 return executeKeyEvent(event); 314 } 315 316 /** 317 * You can call this function yourself to have the scroll view perform 318 * scrolling from a key event, just as if the event had been dispatched to 319 * it by the view hierarchy. 320 * 321 * @param event The key event to execute. 322 * @return Return true if the event was handled, else false. 323 */ executeKeyEvent(KeyEvent event)324 public boolean executeKeyEvent(KeyEvent event) { 325 mTempRect.setEmpty(); 326 327 if (!canScroll()) { 328 if (isFocused()) { 329 View currentFocused = findFocus(); 330 if (currentFocused == this) currentFocused = null; 331 View nextFocused = FocusFinder.getInstance().findNextFocus(this, 332 currentFocused, View.FOCUS_RIGHT); 333 return nextFocused != null && nextFocused != this && 334 nextFocused.requestFocus(View.FOCUS_RIGHT); 335 } 336 return false; 337 } 338 339 boolean handled = false; 340 if (event.getAction() == KeyEvent.ACTION_DOWN) { 341 switch (event.getKeyCode()) { 342 case KeyEvent.KEYCODE_DPAD_LEFT: 343 if (!event.isAltPressed()) { 344 handled = arrowScroll(View.FOCUS_LEFT); 345 } else { 346 handled = fullScroll(View.FOCUS_LEFT); 347 } 348 break; 349 case KeyEvent.KEYCODE_DPAD_RIGHT: 350 if (!event.isAltPressed()) { 351 handled = arrowScroll(View.FOCUS_RIGHT); 352 } else { 353 handled = fullScroll(View.FOCUS_RIGHT); 354 } 355 break; 356 case KeyEvent.KEYCODE_SPACE: 357 pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT); 358 break; 359 } 360 } 361 362 return handled; 363 } 364 365 @Override onInterceptTouchEvent(MotionEvent ev)366 public boolean onInterceptTouchEvent(MotionEvent ev) { 367 /* 368 * This method JUST determines whether we want to intercept the motion. 369 * If we return true, onMotionEvent will be called and we do the actual 370 * scrolling there. 371 */ 372 373 /* 374 * Shortcut the most recurring case: the user is in the dragging 375 * state and he is moving his finger. We want to intercept this 376 * motion. 377 */ 378 final int action = ev.getAction(); 379 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { 380 return true; 381 } 382 383 if (!canScroll()) { 384 mIsBeingDragged = false; 385 return false; 386 } 387 388 final float x = ev.getX(); 389 390 switch (action) { 391 case MotionEvent.ACTION_MOVE: 392 /* 393 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check 394 * whether the user has moved far enough from his original down touch. 395 */ 396 397 /* 398 * Locally do absolute value. mLastMotionX is set to the x value 399 * of the down event. 400 */ 401 final int xDiff = (int) Math.abs(x - mLastMotionX); 402 if (xDiff > mTouchSlop) { 403 mIsBeingDragged = true; 404 if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true); 405 } 406 break; 407 408 case MotionEvent.ACTION_DOWN: 409 /* Remember location of down touch */ 410 mLastMotionX = x; 411 412 /* 413 * If being flinged and user touches the screen, initiate drag; 414 * otherwise don't. mScroller.isFinished should be false when 415 * being flinged. 416 */ 417 mIsBeingDragged = !mScroller.isFinished(); 418 break; 419 420 case MotionEvent.ACTION_CANCEL: 421 case MotionEvent.ACTION_UP: 422 /* Release the drag */ 423 mIsBeingDragged = false; 424 break; 425 } 426 427 /* 428 * The only time we want to intercept motion events is if we are in the 429 * drag mode. 430 */ 431 return mIsBeingDragged; 432 } 433 434 @Override onTouchEvent(MotionEvent ev)435 public boolean onTouchEvent(MotionEvent ev) { 436 437 if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { 438 // Don't handle edge touches immediately -- they may actually belong to one of our 439 // descendants. 440 return false; 441 } 442 443 if (!canScroll()) { 444 return false; 445 } 446 447 if (mVelocityTracker == null) { 448 mVelocityTracker = VelocityTracker.obtain(); 449 } 450 mVelocityTracker.addMovement(ev); 451 452 final int action = ev.getAction(); 453 final float x = ev.getX(); 454 455 switch (action) { 456 case MotionEvent.ACTION_DOWN: 457 /* 458 * If being flinged and user touches, stop the fling. isFinished 459 * will be false if being flinged. 460 */ 461 if (!mScroller.isFinished()) { 462 mScroller.abortAnimation(); 463 } 464 465 // Remember where the motion event started 466 mLastMotionX = x; 467 break; 468 case MotionEvent.ACTION_MOVE: 469 // Scroll to follow the motion event 470 final int deltaX = (int) (mLastMotionX - x); 471 mLastMotionX = x; 472 473 if (deltaX < 0) { 474 if (mScrollX > 0) { 475 scrollBy(deltaX, 0); 476 } 477 } else if (deltaX > 0) { 478 final int rightEdge = getWidth() - mPaddingRight; 479 final int availableToScroll = getChildAt(0).getRight() - mScrollX - rightEdge; 480 if (availableToScroll > 0) { 481 scrollBy(Math.min(availableToScroll, deltaX), 0); 482 } 483 } 484 break; 485 case MotionEvent.ACTION_UP: 486 final VelocityTracker velocityTracker = mVelocityTracker; 487 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 488 int initialVelocity = (int) velocityTracker.getXVelocity(); 489 490 if ((Math.abs(initialVelocity) > mMinimumVelocity) && getChildCount() > 0) { 491 fling(-initialVelocity); 492 } 493 494 if (mVelocityTracker != null) { 495 mVelocityTracker.recycle(); 496 mVelocityTracker = null; 497 } 498 } 499 return true; 500 } 501 502 /** 503 * <p> 504 * Finds the next focusable component that fits in this View's bounds 505 * (excluding fading edges) pretending that this View's left is located at 506 * the parameter left. 507 * </p> 508 * 509 * @param leftFocus look for a candidate is the one at the left of the bounds 510 * if leftFocus is true, or at the right of the bounds if leftFocus 511 * is false 512 * @param left the left offset of the bounds in which a focusable must be 513 * found (the fading edge is assumed to start at this position) 514 * @param preferredFocusable the View that has highest priority and will be 515 * returned if it is within my bounds (null is valid) 516 * @return the next focusable component in the bounds or null if none can be found 517 */ findFocusableViewInMyBounds(final boolean leftFocus, final int left, View preferredFocusable)518 private View findFocusableViewInMyBounds(final boolean leftFocus, 519 final int left, View preferredFocusable) { 520 /* 521 * The fading edge's transparent side should be considered for focus 522 * since it's mostly visible, so we divide the actual fading edge length 523 * by 2. 524 */ 525 final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2; 526 final int leftWithoutFadingEdge = left + fadingEdgeLength; 527 final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength; 528 529 if ((preferredFocusable != null) 530 && (preferredFocusable.getLeft() < rightWithoutFadingEdge) 531 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) { 532 return preferredFocusable; 533 } 534 535 return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge, 536 rightWithoutFadingEdge); 537 } 538 539 /** 540 * <p> 541 * Finds the next focusable component that fits in the specified bounds. 542 * </p> 543 * 544 * @param leftFocus look for a candidate is the one at the left of the bounds 545 * if leftFocus is true, or at the right of the bounds if 546 * leftFocus is false 547 * @param left the left offset of the bounds in which a focusable must be 548 * found 549 * @param right the right offset of the bounds in which a focusable must 550 * be found 551 * @return the next focusable component in the bounds or null if none can 552 * be found 553 */ findFocusableViewInBounds(boolean leftFocus, int left, int right)554 private View findFocusableViewInBounds(boolean leftFocus, int left, int right) { 555 556 List<View> focusables = getFocusables(View.FOCUS_FORWARD); 557 View focusCandidate = null; 558 559 /* 560 * A fully contained focusable is one where its left is below the bound's 561 * left, and its right is above the bound's right. A partially 562 * contained focusable is one where some part of it is within the 563 * bounds, but it also has some part that is not within bounds. A fully contained 564 * focusable is preferred to a partially contained focusable. 565 */ 566 boolean foundFullyContainedFocusable = false; 567 568 int count = focusables.size(); 569 for (int i = 0; i < count; i++) { 570 View view = focusables.get(i); 571 int viewLeft = view.getLeft(); 572 int viewRight = view.getRight(); 573 574 if (left < viewRight && viewLeft < right) { 575 /* 576 * the focusable is in the target area, it is a candidate for 577 * focusing 578 */ 579 580 final boolean viewIsFullyContained = (left < viewLeft) && 581 (viewRight < right); 582 583 if (focusCandidate == null) { 584 /* No candidate, take this one */ 585 focusCandidate = view; 586 foundFullyContainedFocusable = viewIsFullyContained; 587 } else { 588 final boolean viewIsCloserToBoundary = 589 (leftFocus && viewLeft < focusCandidate.getLeft()) || 590 (!leftFocus && viewRight > focusCandidate.getRight()); 591 592 if (foundFullyContainedFocusable) { 593 if (viewIsFullyContained && viewIsCloserToBoundary) { 594 /* 595 * We're dealing with only fully contained views, so 596 * it has to be closer to the boundary to beat our 597 * candidate 598 */ 599 focusCandidate = view; 600 } 601 } else { 602 if (viewIsFullyContained) { 603 /* Any fully contained view beats a partially contained view */ 604 focusCandidate = view; 605 foundFullyContainedFocusable = true; 606 } else if (viewIsCloserToBoundary) { 607 /* 608 * Partially contained view beats another partially 609 * contained view if it's closer 610 */ 611 focusCandidate = view; 612 } 613 } 614 } 615 } 616 } 617 618 return focusCandidate; 619 } 620 621 /** 622 * <p>Handles scrolling in response to a "page up/down" shortcut press. This 623 * method will scroll the view by one page left or right and give the focus 624 * to the leftmost/rightmost component in the new visible area. If no 625 * component is a good candidate for focus, this scrollview reclaims the 626 * focus.</p> 627 * 628 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT} 629 * to go one page left or {@link android.view.View#FOCUS_RIGHT} 630 * to go one page right 631 * @return true if the key event is consumed by this method, false otherwise 632 */ pageScroll(int direction)633 public boolean pageScroll(int direction) { 634 boolean right = direction == View.FOCUS_RIGHT; 635 int width = getWidth(); 636 637 if (right) { 638 mTempRect.left = getScrollX() + width; 639 int count = getChildCount(); 640 if (count > 0) { 641 View view = getChildAt(0); 642 if (mTempRect.left + width > view.getRight()) { 643 mTempRect.left = view.getRight() - width; 644 } 645 } 646 } else { 647 mTempRect.left = getScrollX() - width; 648 if (mTempRect.left < 0) { 649 mTempRect.left = 0; 650 } 651 } 652 mTempRect.right = mTempRect.left + width; 653 654 return scrollAndFocus(direction, mTempRect.left, mTempRect.right); 655 } 656 657 /** 658 * <p>Handles scrolling in response to a "home/end" shortcut press. This 659 * method will scroll the view to the left or right and give the focus 660 * to the leftmost/rightmost component in the new visible area. If no 661 * component is a good candidate for focus, this scrollview reclaims the 662 * focus.</p> 663 * 664 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT} 665 * to go the left of the view or {@link android.view.View#FOCUS_RIGHT} 666 * to go the right 667 * @return true if the key event is consumed by this method, false otherwise 668 */ fullScroll(int direction)669 public boolean fullScroll(int direction) { 670 boolean right = direction == View.FOCUS_RIGHT; 671 int width = getWidth(); 672 673 mTempRect.left = 0; 674 mTempRect.right = width; 675 676 if (right) { 677 int count = getChildCount(); 678 if (count > 0) { 679 View view = getChildAt(0); 680 mTempRect.right = view.getRight(); 681 mTempRect.left = mTempRect.right - width; 682 } 683 } 684 685 return scrollAndFocus(direction, mTempRect.left, mTempRect.right); 686 } 687 688 /** 689 * <p>Scrolls the view to make the area defined by <code>left</code> and 690 * <code>right</code> visible. This method attempts to give the focus 691 * to a component visible in this area. If no component can be focused in 692 * the new visible area, the focus is reclaimed by this scrollview.</p> 693 * 694 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT} 695 * to go left {@link android.view.View#FOCUS_RIGHT} to right 696 * @param left the left offset of the new area to be made visible 697 * @param right the right offset of the new area to be made visible 698 * @return true if the key event is consumed by this method, false otherwise 699 */ scrollAndFocus(int direction, int left, int right)700 private boolean scrollAndFocus(int direction, int left, int right) { 701 boolean handled = true; 702 703 int width = getWidth(); 704 int containerLeft = getScrollX(); 705 int containerRight = containerLeft + width; 706 boolean goLeft = direction == View.FOCUS_LEFT; 707 708 View newFocused = findFocusableViewInBounds(goLeft, left, right); 709 if (newFocused == null) { 710 newFocused = this; 711 } 712 713 if (left >= containerLeft && right <= containerRight) { 714 handled = false; 715 } else { 716 int delta = goLeft ? (left - containerLeft) : (right - containerRight); 717 doScrollX(delta); 718 } 719 720 if (newFocused != findFocus() && newFocused.requestFocus(direction)) { 721 mScrollViewMovedFocus = true; 722 mScrollViewMovedFocus = false; 723 } 724 725 return handled; 726 } 727 728 /** 729 * Handle scrolling in response to a left or right arrow click. 730 * 731 * @param direction The direction corresponding to the arrow key that was 732 * pressed 733 * @return True if we consumed the event, false otherwise 734 */ arrowScroll(int direction)735 public boolean arrowScroll(int direction) { 736 737 View currentFocused = findFocus(); 738 if (currentFocused == this) currentFocused = null; 739 740 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); 741 742 final int maxJump = getMaxScrollAmount(); 743 744 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) { 745 nextFocused.getDrawingRect(mTempRect); 746 offsetDescendantRectToMyCoords(nextFocused, mTempRect); 747 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 748 doScrollX(scrollDelta); 749 nextFocused.requestFocus(direction); 750 } else { 751 // no new focus 752 int scrollDelta = maxJump; 753 754 if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) { 755 scrollDelta = getScrollX(); 756 } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) { 757 758 int daRight = getChildAt(0).getRight(); 759 760 int screenRight = getScrollX() + getWidth(); 761 762 if (daRight - screenRight < maxJump) { 763 scrollDelta = daRight - screenRight; 764 } 765 } 766 if (scrollDelta == 0) { 767 return false; 768 } 769 doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta); 770 } 771 772 if (currentFocused != null && currentFocused.isFocused() 773 && isOffScreen(currentFocused)) { 774 // previously focused item still has focus and is off screen, give 775 // it up (take it back to ourselves) 776 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are 777 // sure to 778 // get it) 779 final int descendantFocusability = getDescendantFocusability(); // save 780 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); 781 requestFocus(); 782 setDescendantFocusability(descendantFocusability); // restore 783 } 784 return true; 785 } 786 787 /** 788 * @return whether the descendant of this scroll view is scrolled off 789 * screen. 790 */ isOffScreen(View descendant)791 private boolean isOffScreen(View descendant) { 792 return !isWithinDeltaOfScreen(descendant, 0); 793 } 794 795 /** 796 * @return whether the descendant of this scroll view is within delta 797 * pixels of being on the screen. 798 */ isWithinDeltaOfScreen(View descendant, int delta)799 private boolean isWithinDeltaOfScreen(View descendant, int delta) { 800 descendant.getDrawingRect(mTempRect); 801 offsetDescendantRectToMyCoords(descendant, mTempRect); 802 803 return (mTempRect.right + delta) >= getScrollX() 804 && (mTempRect.left - delta) <= (getScrollX() + getWidth()); 805 } 806 807 /** 808 * Smooth scroll by a X delta 809 * 810 * @param delta the number of pixels to scroll by on the X axis 811 */ doScrollX(int delta)812 private void doScrollX(int delta) { 813 if (delta != 0) { 814 if (mSmoothScrollingEnabled) { 815 smoothScrollBy(delta, 0); 816 } else { 817 scrollBy(delta, 0); 818 } 819 } 820 } 821 822 /** 823 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. 824 * 825 * @param dx the number of pixels to scroll by on the X axis 826 * @param dy the number of pixels to scroll by on the Y axis 827 */ smoothScrollBy(int dx, int dy)828 public final void smoothScrollBy(int dx, int dy) { 829 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; 830 if (duration > ANIMATED_SCROLL_GAP) { 831 mScroller.startScroll(mScrollX, mScrollY, dx, dy); 832 awakenScrollBars(mScroller.getDuration()); 833 invalidate(); 834 } else { 835 if (!mScroller.isFinished()) { 836 mScroller.abortAnimation(); 837 } 838 scrollBy(dx, dy); 839 } 840 mLastScroll = AnimationUtils.currentAnimationTimeMillis(); 841 } 842 843 /** 844 * Like {@link #scrollTo}, but scroll smoothly instead of immediately. 845 * 846 * @param x the position where to scroll on the X axis 847 * @param y the position where to scroll on the Y axis 848 */ smoothScrollTo(int x, int y)849 public final void smoothScrollTo(int x, int y) { 850 smoothScrollBy(x - mScrollX, y - mScrollY); 851 } 852 853 /** 854 * <p>The scroll range of a scroll view is the overall width of all of its 855 * children.</p> 856 */ 857 @Override computeHorizontalScrollRange()858 protected int computeHorizontalScrollRange() { 859 int count = getChildCount(); 860 return count == 0 ? getWidth() : getChildAt(0).getRight(); 861 } 862 863 864 @Override measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)865 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { 866 ViewGroup.LayoutParams lp = child.getLayoutParams(); 867 868 int childWidthMeasureSpec; 869 int childHeightMeasureSpec; 870 871 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop 872 + mPaddingBottom, lp.height); 873 874 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 875 876 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 877 } 878 879 @Override measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)880 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 881 int parentHeightMeasureSpec, int heightUsed) { 882 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 883 884 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, 885 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin 886 + heightUsed, lp.height); 887 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( 888 lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); 889 890 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); 891 } 892 893 @Override computeScroll()894 public void computeScroll() { 895 if (mScroller.computeScrollOffset()) { 896 // This is called at drawing time by ViewGroup. We don't want to 897 // re-show the scrollbars at this point, which scrollTo will do, 898 // so we replicate most of scrollTo here. 899 // 900 // It's a little odd to call onScrollChanged from inside the drawing. 901 // 902 // It is, except when you remember that computeScroll() is used to 903 // animate scrolling. So unless we want to defer the onScrollChanged() 904 // until the end of the animated scrolling, we don't really have a 905 // choice here. 906 // 907 // I agree. The alternative, which I think would be worse, is to post 908 // something and tell the subclasses later. This is bad because there 909 // will be a window where mScrollX/Y is different from what the app 910 // thinks it is. 911 // 912 int oldX = mScrollX; 913 int oldY = mScrollY; 914 int x = mScroller.getCurrX(); 915 int y = mScroller.getCurrY(); 916 if (getChildCount() > 0) { 917 View child = getChildAt(0); 918 mScrollX = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); 919 mScrollY = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); 920 } else { 921 mScrollX = x; 922 mScrollY = y; 923 } 924 if (oldX != mScrollX || oldY != mScrollY) { 925 onScrollChanged(mScrollX, mScrollY, oldX, oldY); 926 } 927 928 // Keep on drawing until the animation has finished. 929 postInvalidate(); 930 } 931 } 932 933 /** 934 * Scrolls the view to the given child. 935 * 936 * @param child the View to scroll to 937 */ scrollToChild(View child)938 private void scrollToChild(View child) { 939 child.getDrawingRect(mTempRect); 940 941 /* Offset from child's local coordinates to ScrollView coordinates */ 942 offsetDescendantRectToMyCoords(child, mTempRect); 943 944 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 945 946 if (scrollDelta != 0) { 947 scrollBy(scrollDelta, 0); 948 } 949 } 950 951 /** 952 * If rect is off screen, scroll just enough to get it (or at least the 953 * first screen size chunk of it) on screen. 954 * 955 * @param rect The rectangle. 956 * @param immediate True to scroll immediately without animation 957 * @return true if scrolling was performed 958 */ scrollToChildRect(Rect rect, boolean immediate)959 private boolean scrollToChildRect(Rect rect, boolean immediate) { 960 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 961 final boolean scroll = delta != 0; 962 if (scroll) { 963 if (immediate) { 964 scrollBy(delta, 0); 965 } else { 966 smoothScrollBy(delta, 0); 967 } 968 } 969 return scroll; 970 } 971 972 /** 973 * Compute the amount to scroll in the X direction in order to get 974 * a rectangle completely on the screen (or, if taller than the screen, 975 * at least the first screen size chunk of it). 976 * 977 * @param rect The rect. 978 * @return The scroll delta. 979 */ computeScrollDeltaToGetChildRectOnScreen(Rect rect)980 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 981 if (getChildCount() == 0) return 0; 982 983 int width = getWidth(); 984 int screenLeft = getScrollX(); 985 int screenRight = screenLeft + width; 986 987 int fadingEdge = getHorizontalFadingEdgeLength(); 988 989 // leave room for left fading edge as long as rect isn't at very left 990 if (rect.left > 0) { 991 screenLeft += fadingEdge; 992 } 993 994 // leave room for right fading edge as long as rect isn't at very right 995 if (rect.right < getChildAt(0).getWidth()) { 996 screenRight -= fadingEdge; 997 } 998 999 int scrollXDelta = 0; 1000 1001 if (rect.right > screenRight && rect.left > screenLeft) { 1002 // need to move right to get it in view: move right just enough so 1003 // that the entire rectangle is in view (or at least the first 1004 // screen size chunk). 1005 1006 if (rect.width() > width) { 1007 // just enough to get screen size chunk on 1008 scrollXDelta += (rect.left - screenLeft); 1009 } else { 1010 // get entire rect at right of screen 1011 scrollXDelta += (rect.right - screenRight); 1012 } 1013 1014 // make sure we aren't scrolling beyond the end of our content 1015 int right = getChildAt(0).getRight(); 1016 int distanceToRight = right - screenRight; 1017 scrollXDelta = Math.min(scrollXDelta, distanceToRight); 1018 1019 } else if (rect.left < screenLeft && rect.right < screenRight) { 1020 // need to move right to get it in view: move right just enough so that 1021 // entire rectangle is in view (or at least the first screen 1022 // size chunk of it). 1023 1024 if (rect.width() > width) { 1025 // screen size chunk 1026 scrollXDelta -= (screenRight - rect.right); 1027 } else { 1028 // entire rect at left 1029 scrollXDelta -= (screenLeft - rect.left); 1030 } 1031 1032 // make sure we aren't scrolling any further than the left our content 1033 scrollXDelta = Math.max(scrollXDelta, -getScrollX()); 1034 } 1035 return scrollXDelta; 1036 } 1037 1038 @Override requestChildFocus(View child, View focused)1039 public void requestChildFocus(View child, View focused) { 1040 if (!mScrollViewMovedFocus) { 1041 if (!mIsLayoutDirty) { 1042 scrollToChild(focused); 1043 } else { 1044 // The child may not be laid out yet, we can't compute the scroll yet 1045 mChildToScrollTo = focused; 1046 } 1047 } 1048 super.requestChildFocus(child, focused); 1049 } 1050 1051 1052 /** 1053 * When looking for focus in children of a scroll view, need to be a little 1054 * more careful not to give focus to something that is scrolled off screen. 1055 * 1056 * This is more expensive than the default {@link android.view.ViewGroup} 1057 * implementation, otherwise this behavior might have been made the default. 1058 */ 1059 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)1060 protected boolean onRequestFocusInDescendants(int direction, 1061 Rect previouslyFocusedRect) { 1062 1063 // convert from forward / backward notation to up / down / left / right 1064 // (ugh). 1065 if (direction == View.FOCUS_FORWARD) { 1066 direction = View.FOCUS_RIGHT; 1067 } else if (direction == View.FOCUS_BACKWARD) { 1068 direction = View.FOCUS_LEFT; 1069 } 1070 1071 final View nextFocus = previouslyFocusedRect == null ? 1072 FocusFinder.getInstance().findNextFocus(this, null, direction) : 1073 FocusFinder.getInstance().findNextFocusFromRect(this, 1074 previouslyFocusedRect, direction); 1075 1076 if (nextFocus == null) { 1077 return false; 1078 } 1079 1080 if (isOffScreen(nextFocus)) { 1081 return false; 1082 } 1083 1084 return nextFocus.requestFocus(direction, previouslyFocusedRect); 1085 } 1086 1087 @Override requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate)1088 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, 1089 boolean immediate) { 1090 // offset into coordinate space of this scroll view 1091 rectangle.offset(child.getLeft() - child.getScrollX(), 1092 child.getTop() - child.getScrollY()); 1093 1094 return scrollToChildRect(rectangle, immediate); 1095 } 1096 1097 @Override requestLayout()1098 public void requestLayout() { 1099 mIsLayoutDirty = true; 1100 super.requestLayout(); 1101 } 1102 1103 @Override onLayout(boolean changed, int l, int t, int r, int b)1104 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1105 super.onLayout(changed, l, t, r, b); 1106 mIsLayoutDirty = false; 1107 // Give a child focus if it needs it 1108 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { 1109 scrollToChild(mChildToScrollTo); 1110 } 1111 mChildToScrollTo = null; 1112 1113 // Calling this with the present values causes it to re-clam them 1114 scrollTo(mScrollX, mScrollY); 1115 } 1116 1117 @Override onSizeChanged(int w, int h, int oldw, int oldh)1118 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1119 super.onSizeChanged(w, h, oldw, oldh); 1120 1121 View currentFocused = findFocus(); 1122 if (null == currentFocused || this == currentFocused) 1123 return; 1124 1125 final int maxJump = mRight - mLeft; 1126 1127 if (isWithinDeltaOfScreen(currentFocused, maxJump)) { 1128 currentFocused.getDrawingRect(mTempRect); 1129 offsetDescendantRectToMyCoords(currentFocused, mTempRect); 1130 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); 1131 doScrollX(scrollDelta); 1132 } 1133 } 1134 1135 /** 1136 * Return true if child is an descendant of parent, (or equal to the parent). 1137 */ isViewDescendantOf(View child, View parent)1138 private boolean isViewDescendantOf(View child, View parent) { 1139 if (child == parent) { 1140 return true; 1141 } 1142 1143 final ViewParent theParent = child.getParent(); 1144 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); 1145 } 1146 1147 /** 1148 * Fling the scroll view 1149 * 1150 * @param velocityX The initial velocity in the X direction. Positive 1151 * numbers mean that the finger/curor is moving down the screen, 1152 * which means we want to scroll towards the left. 1153 */ fling(int velocityX)1154 public void fling(int velocityX) { 1155 if (getChildCount() > 0) { 1156 int width = getWidth() - mPaddingRight - mPaddingLeft; 1157 int right = getChildAt(0).getWidth(); 1158 1159 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0, right - width, 0, 0); 1160 1161 final boolean movingRight = velocityX > 0; 1162 1163 View newFocused = findFocusableViewInMyBounds(movingRight, 1164 mScroller.getFinalX(), findFocus()); 1165 1166 if (newFocused == null) { 1167 newFocused = this; 1168 } 1169 1170 if (newFocused != findFocus() 1171 && newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT)) { 1172 mScrollViewMovedFocus = true; 1173 mScrollViewMovedFocus = false; 1174 } 1175 1176 awakenScrollBars(mScroller.getDuration()); 1177 invalidate(); 1178 } 1179 } 1180 1181 /** 1182 * {@inheritDoc} 1183 * 1184 * <p>This version also clamps the scrolling to the bounds of our child. 1185 */ scrollTo(int x, int y)1186 public void scrollTo(int x, int y) { 1187 // we rely on the fact the View.scrollBy calls scrollTo. 1188 if (getChildCount() > 0) { 1189 View child = getChildAt(0); 1190 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth()); 1191 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight()); 1192 if (x != mScrollX || y != mScrollY) { 1193 super.scrollTo(x, y); 1194 } 1195 } 1196 } 1197 clamp(int n, int my, int child)1198 private int clamp(int n, int my, int child) { 1199 if (my >= child || n < 0) { 1200 return 0; 1201 } 1202 if ((my + n) > child) { 1203 return child - my; 1204 } 1205 return n; 1206 } 1207 } 1208