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