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