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