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