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