blob: 25cfdc7e4411a3d05c3dfe47f3d033dd7809e18a [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.widget;
18
Yigit Boyar08b1d992019-02-06 12:34:21 -080019import android.annotation.ColorInt;
Siva Velusamy94a6d152015-05-05 15:07:00 -070020import android.annotation.NonNull;
Mathew Inwood978c6e22018-08-21 15:58:55 +010021import android.annotation.UnsupportedAppUsage;
Adam Powell637d3372010-08-25 14:37:03 -070022import android.content.Context;
Adam Powell2fe301d2016-08-15 16:34:37 -070023import android.content.res.Configuration;
Adam Powell637d3372010-08-25 14:37:03 -070024import android.content.res.TypedArray;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080025import android.graphics.Canvas;
26import android.graphics.Rect;
Adam Powell90f339a2013-06-13 17:44:04 -070027import android.os.Build;
Svetoslav Ganova1dc7612012-05-10 04:14:53 -070028import android.os.Bundle;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -070029import android.os.Parcel;
30import android.os.Parcelable;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080031import android.util.AttributeSet;
Johan Rosengren0dc291e2011-02-21 09:49:45 +010032import android.util.Log;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080033import android.view.FocusFinder;
Jeff Brown33bbfd22011-02-24 20:55:35 -080034import android.view.InputDevice;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080035import android.view.KeyEvent;
36import android.view.MotionEvent;
37import android.view.VelocityTracker;
38import android.view.View;
39import android.view.ViewConfiguration;
40import android.view.ViewDebug;
41import android.view.ViewGroup;
Siva Velusamy94a6d152015-05-05 15:07:00 -070042import android.view.ViewHierarchyEncoder;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080043import android.view.ViewParent;
Svetoslav Ganova0156172011-06-26 17:55:44 -070044import android.view.accessibility.AccessibilityEvent;
45import android.view.accessibility.AccessibilityNodeInfo;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080046import android.view.animation.AnimationUtils;
Ashley Rose55f9f922019-01-28 19:29:36 -050047import android.view.inspector.InspectableProperty;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048
Maxim Bogatovac6ffce2015-04-27 13:45:52 -070049import com.android.internal.R;
50
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051import java.util.List;
52
53/**
54 * Layout container for a view hierarchy that can be scrolled by the user,
55 * allowing it to be larger than the physical display. A HorizontalScrollView
56 * is a {@link FrameLayout}, meaning you should place one child in it
57 * containing the entire contents to scroll; this child may itself be a layout
58 * manager with a complex hierarchy of objects. A child that is often used
59 * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
60 * array of top-level items that the user can scroll through.
61 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062 * <p>The {@link TextView} class also
Scott Main15279cf2012-07-02 21:49:47 -070063 * takes care of its own scrolling, so does not require a HorizontalScrollView, but
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064 * using the two together is possible to achieve the effect of a text view
65 * within a larger container.
66 *
Scott Main15279cf2012-07-02 21:49:47 -070067 * <p>HorizontalScrollView only supports horizontal scrolling. For vertical scrolling,
68 * use either {@link ScrollView} or {@link ListView}.
Mindy Pereira4e30d892010-11-24 15:32:39 -080069 *
70 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071 */
72public class HorizontalScrollView extends FrameLayout {
73 private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
74
75 private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
76
Johan Rosengren0dc291e2011-02-21 09:49:45 +010077 private static final String TAG = "HorizontalScrollView";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080078
79 private long mLastScroll;
80
81 private final Rect mTempRect = new Rect();
Mathew Inwood978c6e22018-08-21 15:58:55 +010082 @UnsupportedAppUsage
Adam Powell637d3372010-08-25 14:37:03 -070083 private OverScroller mScroller;
Yigit Boyar08b1d992019-02-06 12:34:21 -080084 /**
85 * Tracks the state of the left edge glow.
86 *
87 * Even though this field is practically final, we cannot make it final because there are apps
88 * setting it via reflection and they need to keep working until they target Q.
89 */
90 @NonNull
91 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124053130)
92 private EdgeEffect mEdgeGlowLeft = new EdgeEffect(getContext());
93
94 /**
95 * Tracks the state of the bottom edge glow.
96 *
97 * Even though this field is practically final, we cannot make it final because there are apps
98 * setting it via reflection and they need to keep working until they target Q.
99 */
100 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124052619)
101 private EdgeEffect mEdgeGlowRight = new EdgeEffect(getContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800102
103 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800104 * Position of the last motion event.
105 */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100106 @UnsupportedAppUsage
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700107 private int mLastMotionX;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108
109 /**
110 * True when the layout has changed but the traversal has not come through yet.
111 * Ideally the view hierarchy would keep track of this for us.
112 */
113 private boolean mIsLayoutDirty = true;
114
115 /**
116 * The child to give focus to in the event that a child has requested focus while the
117 * layout is dirty. This prevents the scroll from being wrong if the child has not been
118 * laid out before requesting focus.
119 */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100120 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800121 private View mChildToScrollTo = null;
122
123 /**
124 * True if the user is currently dragging this ScrollView around. This is
125 * not the same as 'is being flinged', which can be checked by
126 * mScroller.isFinished() (flinging begins when the user lifts his finger).
127 */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100128 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800129 private boolean mIsBeingDragged = false;
130
131 /**
132 * Determines speed during touch scrolling
133 */
Mathew Inwood978c6e22018-08-21 15:58:55 +0100134 @UnsupportedAppUsage
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800135 private VelocityTracker mVelocityTracker;
136
137 /**
138 * When set to true, the scroll view measure its child to make it fill the currently
139 * visible area.
140 */
Romain Guya174d7a2011-01-07 13:27:39 -0800141 @ViewDebug.ExportedProperty(category = "layout")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800142 private boolean mFillViewport;
143
144 /**
145 * Whether arrow scrolling is animated.
146 */
147 private boolean mSmoothScrollingEnabled = true;
148
149 private int mTouchSlop;
Romain Guy4296fc42009-07-06 11:48:52 -0700150 private int mMinimumVelocity;
151 private int mMaximumVelocity;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800152
Mathew Inwood978c6e22018-08-21 15:58:55 +0100153 @UnsupportedAppUsage
Adam Powell637d3372010-08-25 14:37:03 -0700154 private int mOverscrollDistance;
Mathew Inwood978c6e22018-08-21 15:58:55 +0100155 @UnsupportedAppUsage
Adam Powell637d3372010-08-25 14:37:03 -0700156 private int mOverflingDistance;
157
Aaron Whytef8306522017-03-22 16:30:58 -0700158 private float mHorizontalScrollFactor;
Ned Burns20ad0732016-08-18 14:22:57 -0400159
Adam Powell4cd47702010-02-25 11:21:14 -0800160 /**
161 * ID of the active pointer. This is used to retain consistency during
162 * drags/flings if multiple pointers are used.
163 */
164 private int mActivePointerId = INVALID_POINTER;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800165
Adam Powell4cd47702010-02-25 11:21:14 -0800166 /**
167 * Sentinel value for no current active pointer.
168 * Used by {@link #mActivePointerId}.
169 */
170 private static final int INVALID_POINTER = -1;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800171
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -0700172 private SavedState mSavedState;
173
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800174 public HorizontalScrollView(Context context) {
175 this(context, null);
176 }
177
178 public HorizontalScrollView(Context context, AttributeSet attrs) {
179 this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
180 }
181
Alan Viverette617feb92013-09-09 18:09:13 -0700182 public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
183 this(context, attrs, defStyleAttr, 0);
184 }
185
186 public HorizontalScrollView(
187 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
188 super(context, attrs, defStyleAttr, defStyleRes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800189 initScrollView();
190
Alan Viverette617feb92013-09-09 18:09:13 -0700191 final TypedArray a = context.obtainStyledAttributes(
192 attrs, android.R.styleable.HorizontalScrollView, defStyleAttr, defStyleRes);
Aurimas Liutikasab324cf2019-02-07 16:46:38 -0800193 saveAttributeDataForStyleable(context, android.R.styleable.HorizontalScrollView,
194 attrs, a, defStyleAttr, defStyleRes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800195
196 setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
197
198 a.recycle();
Adam Powell2fe301d2016-08-15 16:34:37 -0700199
200 if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
201 setRevealOnFocusHint(false);
202 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 }
204
205 @Override
206 protected float getLeftFadingEdgeStrength() {
207 if (getChildCount() == 0) {
208 return 0.0f;
209 }
210
211 final int length = getHorizontalFadingEdgeLength();
212 if (mScrollX < length) {
213 return mScrollX / (float) length;
214 }
215
216 return 1.0f;
217 }
218
219 @Override
220 protected float getRightFadingEdgeStrength() {
221 if (getChildCount() == 0) {
222 return 0.0f;
223 }
224
225 final int length = getHorizontalFadingEdgeLength();
226 final int rightEdge = getWidth() - mPaddingRight;
227 final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
228 if (span < length) {
229 return span / (float) length;
230 }
231
232 return 1.0f;
233 }
234
235 /**
Yigit Boyar08b1d992019-02-06 12:34:21 -0800236 * Sets the edge effect color for both left and right edge effects.
237 *
238 * @param color The color for the edge effects.
239 * @see #setLeftEdgeEffectColor(int)
240 * @see #setRightEdgeEffectColor(int)
241 * @see #getLeftEdgeEffectColor()
242 * @see #getRightEdgeEffectColor()
243 */
244 public void setEdgeEffectColor(@ColorInt int color) {
245 setLeftEdgeEffectColor(color);
246 setRightEdgeEffectColor(color);
247 }
248
249 /**
250 * Sets the right edge effect color.
251 *
252 * @param color The color for the right edge effect.
253 * @see #setLeftEdgeEffectColor(int)
254 * @see #setEdgeEffectColor(int)
255 * @see #getLeftEdgeEffectColor()
256 * @see #getRightEdgeEffectColor()
257 */
258 public void setRightEdgeEffectColor(@ColorInt int color) {
259 mEdgeGlowRight.setColor(color);
260 }
261
262 /**
263 * Sets the left edge effect color.
264 *
265 * @param color The color for the left edge effect.
266 * @see #setRightEdgeEffectColor(int)
267 * @see #setEdgeEffectColor(int)
268 * @see #getLeftEdgeEffectColor()
269 * @see #getRightEdgeEffectColor()
270 */
271 public void setLeftEdgeEffectColor(@ColorInt int color) {
272 mEdgeGlowLeft.setColor(color);
273 }
274
275 /**
276 * Returns the left edge effect color.
277 *
278 * @return The left edge effect color.
279 * @see #setEdgeEffectColor(int)
280 * @see #setLeftEdgeEffectColor(int)
281 * @see #setRightEdgeEffectColor(int)
282 * @see #getRightEdgeEffectColor()
283 */
284 @ColorInt
285 public int getLeftEdgeEffectColor() {
286 return mEdgeGlowLeft.getColor();
287 }
288
289 /**
290 * Returns the right edge effect color.
291 *
292 * @return The right edge effect color.
293 * @see #setEdgeEffectColor(int)
294 * @see #setLeftEdgeEffectColor(int)
295 * @see #setRightEdgeEffectColor(int)
296 * @see #getLeftEdgeEffectColor()
297 */
298 @ColorInt
299 public int getRightEdgeEffectColor() {
300 return mEdgeGlowRight.getColor();
301 }
302
303 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800304 * @return The maximum amount this scroll view will scroll in response to
305 * an arrow event.
306 */
307 public int getMaxScrollAmount() {
308 return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
309 }
310
311
312 private void initScrollView() {
Adam Powell637d3372010-08-25 14:37:03 -0700313 mScroller = new OverScroller(getContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800314 setFocusable(true);
315 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
316 setWillNotDraw(false);
Romain Guy4296fc42009-07-06 11:48:52 -0700317 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
318 mTouchSlop = configuration.getScaledTouchSlop();
319 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
320 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Adam Powell637d3372010-08-25 14:37:03 -0700321 mOverscrollDistance = configuration.getScaledOverscrollDistance();
322 mOverflingDistance = configuration.getScaledOverflingDistance();
Aaron Whytef8306522017-03-22 16:30:58 -0700323 mHorizontalScrollFactor = configuration.getScaledHorizontalScrollFactor();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800324 }
325
326 @Override
327 public void addView(View child) {
328 if (getChildCount() > 0) {
329 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
330 }
331
332 super.addView(child);
333 }
334
335 @Override
336 public void addView(View child, int index) {
337 if (getChildCount() > 0) {
338 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
339 }
340
341 super.addView(child, index);
342 }
343
344 @Override
345 public void addView(View child, ViewGroup.LayoutParams params) {
346 if (getChildCount() > 0) {
347 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
348 }
349
350 super.addView(child, params);
351 }
352
353 @Override
354 public void addView(View child, int index, ViewGroup.LayoutParams params) {
355 if (getChildCount() > 0) {
356 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
357 }
358
359 super.addView(child, index, params);
360 }
361
362 /**
363 * @return Returns true this HorizontalScrollView can be scrolled
364 */
365 private boolean canScroll() {
366 View child = getChildAt(0);
367 if (child != null) {
368 int childWidth = child.getWidth();
369 return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
370 }
371 return false;
372 }
373
374 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700375 * Indicates whether this HorizontalScrollView's content is stretched to
376 * fill the viewport.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800377 *
378 * @return True if the content fills the viewport, false otherwise.
Romain Guyfdbf4842010-08-16 10:55:49 -0700379 *
380 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800381 */
Ashley Rose55f9f922019-01-28 19:29:36 -0500382 @InspectableProperty
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800383 public boolean isFillViewport() {
384 return mFillViewport;
385 }
386
387 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700388 * Indicates this HorizontalScrollView whether it should stretch its content width
389 * to fill the viewport or not.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800390 *
391 * @param fillViewport True to stretch the content's width to the viewport's
392 * boundaries, false otherwise.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800393 *
Romain Guyfdbf4842010-08-16 10:55:49 -0700394 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800395 */
396 public void setFillViewport(boolean fillViewport) {
397 if (fillViewport != mFillViewport) {
398 mFillViewport = fillViewport;
399 requestLayout();
400 }
401 }
402
403 /**
404 * @return Whether arrow scrolling will animate its transition.
405 */
406 public boolean isSmoothScrollingEnabled() {
407 return mSmoothScrollingEnabled;
408 }
409
410 /**
411 * Set whether arrow scrolling will animate its transition.
412 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
413 */
414 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
415 mSmoothScrollingEnabled = smoothScrollingEnabled;
416 }
417
418 @Override
419 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
420 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
421
422 if (!mFillViewport) {
423 return;
424 }
425
426 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
427 if (widthMode == MeasureSpec.UNSPECIFIED) {
428 return;
429 }
430
Romain Guyef0e9ae2009-07-10 14:11:26 -0700431 if (getChildCount() > 0) {
432 final View child = getChildAt(0);
Yigit Boyar115a6f42016-03-22 13:36:47 -0700433 final int widthPadding;
434 final int heightPadding;
435 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
436 final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
437 if (targetSdkVersion >= Build.VERSION_CODES.M) {
438 widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
439 heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
440 } else {
441 widthPadding = mPaddingLeft + mPaddingRight;
442 heightPadding = mPaddingTop + mPaddingBottom;
443 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800444
Yigit Boyar115a6f42016-03-22 13:36:47 -0700445 int desiredWidth = getMeasuredWidth() - widthPadding;
446 if (child.getMeasuredWidth() < desiredWidth) {
447 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
448 desiredWidth, MeasureSpec.EXACTLY);
449 final int childHeightMeasureSpec = getChildMeasureSpec(
450 heightMeasureSpec, heightPadding, lp.height);
Romain Guyef0e9ae2009-07-10 14:11:26 -0700451 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
452 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800453 }
454 }
455
456 @Override
457 public boolean dispatchKeyEvent(KeyEvent event) {
458 // Let the focused view and/or our descendants get the key first
Romain Guy8e618e52010-03-08 12:18:20 -0800459 return super.dispatchKeyEvent(event) || executeKeyEvent(event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800460 }
461
462 /**
463 * You can call this function yourself to have the scroll view perform
464 * scrolling from a key event, just as if the event had been dispatched to
465 * it by the view hierarchy.
466 *
467 * @param event The key event to execute.
468 * @return Return true if the event was handled, else false.
469 */
470 public boolean executeKeyEvent(KeyEvent event) {
471 mTempRect.setEmpty();
472
473 if (!canScroll()) {
474 if (isFocused()) {
475 View currentFocused = findFocus();
476 if (currentFocused == this) currentFocused = null;
477 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
478 currentFocused, View.FOCUS_RIGHT);
479 return nextFocused != null && nextFocused != this &&
480 nextFocused.requestFocus(View.FOCUS_RIGHT);
481 }
482 return false;
483 }
484
485 boolean handled = false;
486 if (event.getAction() == KeyEvent.ACTION_DOWN) {
487 switch (event.getKeyCode()) {
488 case KeyEvent.KEYCODE_DPAD_LEFT:
489 if (!event.isAltPressed()) {
490 handled = arrowScroll(View.FOCUS_LEFT);
491 } else {
492 handled = fullScroll(View.FOCUS_LEFT);
493 }
494 break;
495 case KeyEvent.KEYCODE_DPAD_RIGHT:
496 if (!event.isAltPressed()) {
497 handled = arrowScroll(View.FOCUS_RIGHT);
498 } else {
499 handled = fullScroll(View.FOCUS_RIGHT);
500 }
501 break;
502 case KeyEvent.KEYCODE_SPACE:
503 pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
504 break;
505 }
506 }
507
508 return handled;
509 }
510
Adam Powell4cd47702010-02-25 11:21:14 -0800511 private boolean inChild(int x, int y) {
512 if (getChildCount() > 0) {
Adam Powell352b9782010-03-24 14:23:43 -0700513 final int scrollX = mScrollX;
Adam Powell4cd47702010-02-25 11:21:14 -0800514 final View child = getChildAt(0);
515 return !(y < child.getTop()
516 || y >= child.getBottom()
Adam Powell352b9782010-03-24 14:23:43 -0700517 || x < child.getLeft() - scrollX
518 || x >= child.getRight() - scrollX);
Adam Powell4cd47702010-02-25 11:21:14 -0800519 }
520 return false;
521 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800522
Michael Jurka13451a42011-08-22 15:54:21 -0700523 private void initOrResetVelocityTracker() {
524 if (mVelocityTracker == null) {
525 mVelocityTracker = VelocityTracker.obtain();
526 } else {
527 mVelocityTracker.clear();
528 }
529 }
530
531 private void initVelocityTrackerIfNotExists() {
532 if (mVelocityTracker == null) {
533 mVelocityTracker = VelocityTracker.obtain();
534 }
535 }
536
Mathew Inwood978c6e22018-08-21 15:58:55 +0100537 @UnsupportedAppUsage
Michael Jurka13451a42011-08-22 15:54:21 -0700538 private void recycleVelocityTracker() {
539 if (mVelocityTracker != null) {
540 mVelocityTracker.recycle();
541 mVelocityTracker = null;
542 }
543 }
544
545 @Override
546 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
547 if (disallowIntercept) {
548 recycleVelocityTracker();
549 }
550 super.requestDisallowInterceptTouchEvent(disallowIntercept);
551 }
552
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800553 @Override
554 public boolean onInterceptTouchEvent(MotionEvent ev) {
555 /*
556 * This method JUST determines whether we want to intercept the motion.
557 * If we return true, onMotionEvent will be called and we do the actual
558 * scrolling there.
559 */
560
561 /*
562 * Shortcut the most recurring case: the user is in the dragging
563 * state and he is moving his finger. We want to intercept this
564 * motion.
565 */
566 final int action = ev.getAction();
567 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
568 return true;
569 }
570
Keisuke Kuroyanagid85bc502016-01-21 14:50:38 +0900571 if (super.onInterceptTouchEvent(ev)) {
572 return true;
573 }
574
Adam Powell4cd47702010-02-25 11:21:14 -0800575 switch (action & MotionEvent.ACTION_MASK) {
576 case MotionEvent.ACTION_MOVE: {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800577 /*
578 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
579 * whether the user has moved far enough from his original down touch.
580 */
581
582 /*
583 * Locally do absolute value. mLastMotionX is set to the x value
584 * of the down event.
585 */
Adam Powell9d0335b2010-03-24 13:42:51 -0700586 final int activePointerId = mActivePointerId;
587 if (activePointerId == INVALID_POINTER) {
588 // If we don't have a valid id, the touch down wasn't on content.
589 break;
590 }
591
592 final int pointerIndex = ev.findPointerIndex(activePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100593 if (pointerIndex == -1) {
594 Log.e(TAG, "Invalid pointerId=" + activePointerId
595 + " in onInterceptTouchEvent");
596 break;
597 }
598
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700599 final int x = (int) ev.getX(pointerIndex);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800600 final int xDiff = (int) Math.abs(x - mLastMotionX);
601 if (xDiff > mTouchSlop) {
602 mIsBeingDragged = true;
Adam Powell4cd47702010-02-25 11:21:14 -0800603 mLastMotionX = x;
Michael Jurka13451a42011-08-22 15:54:21 -0700604 initVelocityTrackerIfNotExists();
605 mVelocityTracker.addMovement(ev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800606 if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
607 }
608 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800609 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800610
Adam Powell4cd47702010-02-25 11:21:14 -0800611 case MotionEvent.ACTION_DOWN: {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700612 final int x = (int) ev.getX();
Adam Powell4cd47702010-02-25 11:21:14 -0800613 if (!inChild((int) x, (int) ev.getY())) {
614 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700615 recycleVelocityTracker();
Adam Powell4cd47702010-02-25 11:21:14 -0800616 break;
617 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800618
Adam Powell4cd47702010-02-25 11:21:14 -0800619 /*
620 * Remember location of down touch.
621 * ACTION_DOWN always refers to pointer index 0.
622 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800623 mLastMotionX = x;
Adam Powell4cd47702010-02-25 11:21:14 -0800624 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800625
Michael Jurka13451a42011-08-22 15:54:21 -0700626 initOrResetVelocityTracker();
627 mVelocityTracker.addMovement(ev);
628
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800629 /*
630 * If being flinged and user touches the screen, initiate drag;
631 * otherwise don't. mScroller.isFinished should be false when
632 * being flinged.
633 */
634 mIsBeingDragged = !mScroller.isFinished();
635 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800636 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800637
638 case MotionEvent.ACTION_CANCEL:
639 case MotionEvent.ACTION_UP:
640 /* Release the drag */
641 mIsBeingDragged = false;
Adam Powell4cd47702010-02-25 11:21:14 -0800642 mActivePointerId = INVALID_POINTER;
Adam Powell637d3372010-08-25 14:37:03 -0700643 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700644 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700645 }
Adam Powell4cd47702010-02-25 11:21:14 -0800646 break;
Adam Powell9bc30d32011-02-28 10:27:49 -0800647 case MotionEvent.ACTION_POINTER_DOWN: {
648 final int index = ev.getActionIndex();
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700649 mLastMotionX = (int) ev.getX(index);
Adam Powell9bc30d32011-02-28 10:27:49 -0800650 mActivePointerId = ev.getPointerId(index);
651 break;
652 }
Adam Powell4cd47702010-02-25 11:21:14 -0800653 case MotionEvent.ACTION_POINTER_UP:
654 onSecondaryPointerUp(ev);
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700655 mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800656 break;
657 }
658
659 /*
660 * The only time we want to intercept motion events is if we are in the
661 * drag mode.
662 */
663 return mIsBeingDragged;
664 }
665
666 @Override
667 public boolean onTouchEvent(MotionEvent ev) {
Michael Jurka13451a42011-08-22 15:54:21 -0700668 initVelocityTrackerIfNotExists();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800669 mVelocityTracker.addMovement(ev);
670
671 final int action = ev.getAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800672
Adam Powell4cd47702010-02-25 11:21:14 -0800673 switch (action & MotionEvent.ACTION_MASK) {
674 case MotionEvent.ACTION_DOWN: {
Adam Powellb3e02c42012-05-02 22:05:46 -0700675 if (getChildCount() == 0) {
Jeff Brownfb757382011-01-18 18:42:33 -0800676 return false;
677 }
Adam Powellb3e02c42012-05-02 22:05:46 -0700678 if ((mIsBeingDragged = !mScroller.isFinished())) {
679 final ViewParent parent = getParent();
680 if (parent != null) {
681 parent.requestDisallowInterceptTouchEvent(true);
682 }
683 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800684
Adam Powell352b9782010-03-24 14:23:43 -0700685 /*
686 * If being flinged and user touches, stop the fling. isFinished
687 * will be false if being flinged.
688 */
689 if (!mScroller.isFinished()) {
690 mScroller.abortAnimation();
691 }
692
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800693 // Remember where the motion event started
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700694 mLastMotionX = (int) ev.getX();
Adam Powell352b9782010-03-24 14:23:43 -0700695 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800696 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800697 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800698 case MotionEvent.ACTION_MOVE:
Adam Powellb3e02c42012-05-02 22:05:46 -0700699 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100700 if (activePointerIndex == -1) {
701 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
702 break;
703 }
704
Adam Powellb3e02c42012-05-02 22:05:46 -0700705 final int x = (int) ev.getX(activePointerIndex);
706 int deltaX = mLastMotionX - x;
707 if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
708 final ViewParent parent = getParent();
709 if (parent != null) {
710 parent.requestDisallowInterceptTouchEvent(true);
711 }
712 mIsBeingDragged = true;
713 if (deltaX > 0) {
714 deltaX -= mTouchSlop;
715 } else {
716 deltaX += mTouchSlop;
717 }
718 }
Adam Powell4cd47702010-02-25 11:21:14 -0800719 if (mIsBeingDragged) {
720 // Scroll to follow the motion event
Adam Powell4cd47702010-02-25 11:21:14 -0800721 mLastMotionX = x;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800722
Adam Powell637d3372010-08-25 14:37:03 -0700723 final int oldX = mScrollX;
724 final int oldY = mScrollY;
725 final int range = getScrollRange();
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700726 final int overscrollMode = getOverScrollMode();
727 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
728 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
729
Alan Viverettecb25bd82013-06-03 17:10:44 -0700730 // Calling overScrollBy will call onOverScrolled, which
731 // calls onScrollChanged if applicable.
Adam Powellf6a6c972011-09-28 23:30:20 -0700732 if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -0700733 mOverscrollDistance, 0, true)) {
734 // Break our velocity if we hit a scroll barrier.
735 mVelocityTracker.clear();
736 }
Adam Powell637d3372010-08-25 14:37:03 -0700737
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700738 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -0700739 final int pulledToX = oldX + deltaX;
740 if (pulledToX < 0) {
Adam Powellc501db9f2014-05-08 12:50:10 -0700741 mEdgeGlowLeft.onPull((float) deltaX / getWidth(),
742 1.f - ev.getY(activePointerIndex) / getHeight());
Adam Powell637d3372010-08-25 14:37:03 -0700743 if (!mEdgeGlowRight.isFinished()) {
744 mEdgeGlowRight.onRelease();
745 }
746 } else if (pulledToX > range) {
Adam Powellc501db9f2014-05-08 12:50:10 -0700747 mEdgeGlowRight.onPull((float) deltaX / getWidth(),
748 ev.getY(activePointerIndex) / getHeight());
Adam Powell637d3372010-08-25 14:37:03 -0700749 if (!mEdgeGlowLeft.isFinished()) {
750 mEdgeGlowLeft.onRelease();
751 }
752 }
Yigit Boyar08b1d992019-02-06 12:34:21 -0800753 if (shouldDisplayEdgeEffects()
Adam Powell637d3372010-08-25 14:37:03 -0700754 && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700755 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700756 }
757 }
Adam Powell4cd47702010-02-25 11:21:14 -0800758 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800759 break;
760 case MotionEvent.ACTION_UP:
Adam Powell4cd47702010-02-25 11:21:14 -0800761 if (mIsBeingDragged) {
762 final VelocityTracker velocityTracker = mVelocityTracker;
763 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
764 int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800765
Adam Powellf6a6c972011-09-28 23:30:20 -0700766 if (getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700767 if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
768 fling(-initialVelocity);
769 } else {
Adam Powellf6a6c972011-09-28 23:30:20 -0700770 if (mScroller.springBack(mScrollX, mScrollY, 0,
771 getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700772 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700773 }
774 }
Adam Powell17dfce12010-01-25 18:38:22 -0800775 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800776
Adam Powell4cd47702010-02-25 11:21:14 -0800777 mActivePointerId = INVALID_POINTER;
778 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700779 recycleVelocityTracker();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800780
Yigit Boyar08b1d992019-02-06 12:34:21 -0800781 if (shouldDisplayEdgeEffects()) {
Adam Powell637d3372010-08-25 14:37:03 -0700782 mEdgeGlowLeft.onRelease();
783 mEdgeGlowRight.onRelease();
784 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800785 }
Adam Powell4cd47702010-02-25 11:21:14 -0800786 break;
Adam Powell352b9782010-03-24 14:23:43 -0700787 case MotionEvent.ACTION_CANCEL:
788 if (mIsBeingDragged && getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700789 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700790 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700791 }
Adam Powell352b9782010-03-24 14:23:43 -0700792 mActivePointerId = INVALID_POINTER;
793 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700794 recycleVelocityTracker();
795
Yigit Boyar08b1d992019-02-06 12:34:21 -0800796 if (shouldDisplayEdgeEffects()) {
Adam Powell637d3372010-08-25 14:37:03 -0700797 mEdgeGlowLeft.onRelease();
798 mEdgeGlowRight.onRelease();
799 }
Adam Powell352b9782010-03-24 14:23:43 -0700800 }
801 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800802 case MotionEvent.ACTION_POINTER_UP:
803 onSecondaryPointerUp(ev);
804 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800805 }
806 return true;
807 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800808
Adam Powell4cd47702010-02-25 11:21:14 -0800809 private void onSecondaryPointerUp(MotionEvent ev) {
810 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
811 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
812 final int pointerId = ev.getPointerId(pointerIndex);
813 if (pointerId == mActivePointerId) {
814 // This was our active pointer going up. Choose a new
815 // active pointer and adjust accordingly.
816 // TODO: Make this decision more intelligent.
817 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700818 mLastMotionX = (int) ev.getX(newPointerIndex);
Adam Powell4cd47702010-02-25 11:21:14 -0800819 mActivePointerId = ev.getPointerId(newPointerIndex);
820 if (mVelocityTracker != null) {
821 mVelocityTracker.clear();
822 }
823 }
824 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800825
Adam Powell637d3372010-08-25 14:37:03 -0700826 @Override
Jeff Brown33bbfd22011-02-24 20:55:35 -0800827 public boolean onGenericMotionEvent(MotionEvent event) {
Ned Burns20ad0732016-08-18 14:22:57 -0400828 switch (event.getAction()) {
829 case MotionEvent.ACTION_SCROLL: {
830 if (!mIsBeingDragged) {
831 final float axisValue;
832 if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
Jeff Brown33bbfd22011-02-24 20:55:35 -0800833 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
Ned Burns20ad0732016-08-18 14:22:57 -0400834 axisValue = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
Jeff Brown33bbfd22011-02-24 20:55:35 -0800835 } else {
Ned Burns20ad0732016-08-18 14:22:57 -0400836 axisValue = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
Jeff Brown33bbfd22011-02-24 20:55:35 -0800837 }
Ned Burns20ad0732016-08-18 14:22:57 -0400838 } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
839 axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
840 } else {
841 axisValue = 0;
842 }
843
Aaron Whytef8306522017-03-22 16:30:58 -0700844 final int delta = Math.round(axisValue * mHorizontalScrollFactor);
Ned Burns20ad0732016-08-18 14:22:57 -0400845 if (delta != 0) {
846 final int range = getScrollRange();
847 int oldScrollX = mScrollX;
848 int newScrollX = oldScrollX + delta;
849 if (newScrollX < 0) {
850 newScrollX = 0;
851 } else if (newScrollX > range) {
852 newScrollX = range;
853 }
854 if (newScrollX != oldScrollX) {
855 super.scrollTo(newScrollX, mScrollY);
856 return true;
Jeff Brown33bbfd22011-02-24 20:55:35 -0800857 }
858 }
859 }
860 }
861 }
862 return super.onGenericMotionEvent(event);
863 }
864
865 @Override
Michael Jurka9edd58e2011-10-28 16:39:18 -0700866 public boolean shouldDelayChildPressedState() {
867 return true;
868 }
869
870 @Override
Adam Powell637d3372010-08-25 14:37:03 -0700871 protected void onOverScrolled(int scrollX, int scrollY,
872 boolean clampedX, boolean clampedY) {
873 // Treat animating scrolls differently; see #computeScroll() for why.
874 if (!mScroller.isFinished()) {
Alan Viverettecb25bd82013-06-03 17:10:44 -0700875 final int oldX = mScrollX;
876 final int oldY = mScrollY;
Adam Powell637d3372010-08-25 14:37:03 -0700877 mScrollX = scrollX;
878 mScrollY = scrollY;
Romain Guy0fd89bf2011-01-26 15:41:30 -0800879 invalidateParentIfNeeded();
Alan Viverettecb25bd82013-06-03 17:10:44 -0700880 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
Adam Powell637d3372010-08-25 14:37:03 -0700881 if (clampedX) {
882 mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
883 }
884 } else {
885 super.scrollTo(scrollX, scrollY);
886 }
Svet Ganovb3fa2782014-12-05 13:02:24 -0800887
Romain Guye72cf732012-03-20 14:23:09 -0700888 awakenScrollBars();
Adam Powell637d3372010-08-25 14:37:03 -0700889 }
890
Alan Viverettea54956a2015-01-07 16:05:02 -0800891 /** @hide */
Svetoslav Ganova0156172011-06-26 17:55:44 -0700892 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800893 public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
894 if (super.performAccessibilityActionInternal(action, arguments)) {
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700895 return true;
896 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700897 switch (action) {
Maxim Bogatovac6ffce2015-04-27 13:45:52 -0700898 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
899 case R.id.accessibilityActionScrollRight: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700900 if (!isEnabled()) {
901 return false;
902 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700903 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
904 final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
905 if (targetScrollX != mScrollX) {
906 smoothScrollTo(targetScrollX, 0);
907 return true;
908 }
909 } return false;
Maxim Bogatovac6ffce2015-04-27 13:45:52 -0700910 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
911 case R.id.accessibilityActionScrollLeft: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700912 if (!isEnabled()) {
913 return false;
914 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700915 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
916 final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
917 if (targetScrollX != mScrollX) {
918 smoothScrollTo(targetScrollX, 0);
919 return true;
920 }
921 } return false;
922 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700923 return false;
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700924 }
925
Dianne Hackborna7bb6fb2015-02-03 18:13:40 -0800926 @Override
927 public CharSequence getAccessibilityClassName() {
928 return HorizontalScrollView.class.getName();
929 }
930
Alan Viverettea54956a2015-01-07 16:05:02 -0800931 /** @hide */
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700932 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800933 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
934 super.onInitializeAccessibilityNodeInfoInternal(info);
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700935 final int scrollRange = getScrollRange();
936 if (scrollRange > 0) {
937 info.setScrollable(true);
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700938 if (isEnabled() && mScrollX > 0) {
Maxim Bogatovac6ffce2015-04-27 13:45:52 -0700939 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
940 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_LEFT);
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700941 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700942 if (isEnabled() && mScrollX < scrollRange) {
Maxim Bogatovac6ffce2015-04-27 13:45:52 -0700943 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
944 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_RIGHT);
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700945 }
946 }
Svetoslav Ganova0156172011-06-26 17:55:44 -0700947 }
948
Alan Viverettea54956a2015-01-07 16:05:02 -0800949 /** @hide */
Svetoslav Ganova0156172011-06-26 17:55:44 -0700950 @Override
Alan Viverettea54956a2015-01-07 16:05:02 -0800951 public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
952 super.onInitializeAccessibilityEventInternal(event);
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700953 event.setScrollable(getScrollRange() > 0);
954 event.setScrollX(mScrollX);
955 event.setScrollY(mScrollY);
956 event.setMaxScrollX(getScrollRange());
957 event.setMaxScrollY(mScrollY);
Svetoslav Ganova0156172011-06-26 17:55:44 -0700958 }
959
Adam Powell0b8bb422010-02-08 14:30:45 -0800960 private int getScrollRange() {
961 int scrollRange = 0;
962 if (getChildCount() > 0) {
963 View child = getChildAt(0);
964 scrollRange = Math.max(0,
Adam Powell637d3372010-08-25 14:37:03 -0700965 child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
Adam Powell0b8bb422010-02-08 14:30:45 -0800966 }
967 return scrollRange;
968 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800969
970 /**
971 * <p>
972 * Finds the next focusable component that fits in this View's bounds
973 * (excluding fading edges) pretending that this View's left is located at
974 * the parameter left.
975 * </p>
976 *
977 * @param leftFocus look for a candidate is the one at the left of the bounds
978 * if leftFocus is true, or at the right of the bounds if leftFocus
979 * is false
980 * @param left the left offset of the bounds in which a focusable must be
981 * found (the fading edge is assumed to start at this position)
982 * @param preferredFocusable the View that has highest priority and will be
983 * returned if it is within my bounds (null is valid)
984 * @return the next focusable component in the bounds or null if none can be found
985 */
986 private View findFocusableViewInMyBounds(final boolean leftFocus,
987 final int left, View preferredFocusable) {
988 /*
989 * The fading edge's transparent side should be considered for focus
990 * since it's mostly visible, so we divide the actual fading edge length
991 * by 2.
992 */
993 final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
994 final int leftWithoutFadingEdge = left + fadingEdgeLength;
995 final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
996
997 if ((preferredFocusable != null)
998 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
999 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
1000 return preferredFocusable;
1001 }
1002
1003 return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
1004 rightWithoutFadingEdge);
1005 }
1006
1007 /**
1008 * <p>
1009 * Finds the next focusable component that fits in the specified bounds.
1010 * </p>
1011 *
1012 * @param leftFocus look for a candidate is the one at the left of the bounds
1013 * if leftFocus is true, or at the right of the bounds if
1014 * leftFocus is false
1015 * @param left the left offset of the bounds in which a focusable must be
1016 * found
1017 * @param right the right offset of the bounds in which a focusable must
1018 * be found
1019 * @return the next focusable component in the bounds or null if none can
1020 * be found
1021 */
1022 private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
1023
1024 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
1025 View focusCandidate = null;
1026
1027 /*
1028 * A fully contained focusable is one where its left is below the bound's
1029 * left, and its right is above the bound's right. A partially
1030 * contained focusable is one where some part of it is within the
1031 * bounds, but it also has some part that is not within bounds. A fully contained
1032 * focusable is preferred to a partially contained focusable.
1033 */
1034 boolean foundFullyContainedFocusable = false;
1035
1036 int count = focusables.size();
1037 for (int i = 0; i < count; i++) {
1038 View view = focusables.get(i);
1039 int viewLeft = view.getLeft();
1040 int viewRight = view.getRight();
1041
1042 if (left < viewRight && viewLeft < right) {
1043 /*
1044 * the focusable is in the target area, it is a candidate for
1045 * focusing
1046 */
1047
1048 final boolean viewIsFullyContained = (left < viewLeft) &&
1049 (viewRight < right);
1050
1051 if (focusCandidate == null) {
1052 /* No candidate, take this one */
1053 focusCandidate = view;
1054 foundFullyContainedFocusable = viewIsFullyContained;
1055 } else {
1056 final boolean viewIsCloserToBoundary =
1057 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
1058 (!leftFocus && viewRight > focusCandidate.getRight());
1059
1060 if (foundFullyContainedFocusable) {
1061 if (viewIsFullyContained && viewIsCloserToBoundary) {
1062 /*
1063 * We're dealing with only fully contained views, so
1064 * it has to be closer to the boundary to beat our
1065 * candidate
1066 */
1067 focusCandidate = view;
1068 }
1069 } else {
1070 if (viewIsFullyContained) {
1071 /* Any fully contained view beats a partially contained view */
1072 focusCandidate = view;
1073 foundFullyContainedFocusable = true;
1074 } else if (viewIsCloserToBoundary) {
1075 /*
1076 * Partially contained view beats another partially
1077 * contained view if it's closer
1078 */
1079 focusCandidate = view;
1080 }
1081 }
1082 }
1083 }
1084 }
1085
1086 return focusCandidate;
1087 }
1088
1089 /**
1090 * <p>Handles scrolling in response to a "page up/down" shortcut press. This
1091 * method will scroll the view by one page left or right and give the focus
1092 * to the leftmost/rightmost component in the new visible area. If no
1093 * component is a good candidate for focus, this scrollview reclaims the
1094 * focus.</p>
1095 *
1096 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1097 * to go one page left or {@link android.view.View#FOCUS_RIGHT}
1098 * to go one page right
1099 * @return true if the key event is consumed by this method, false otherwise
1100 */
1101 public boolean pageScroll(int direction) {
1102 boolean right = direction == View.FOCUS_RIGHT;
1103 int width = getWidth();
1104
1105 if (right) {
1106 mTempRect.left = getScrollX() + width;
1107 int count = getChildCount();
1108 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001109 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001110 if (mTempRect.left + width > view.getRight()) {
1111 mTempRect.left = view.getRight() - width;
1112 }
1113 }
1114 } else {
1115 mTempRect.left = getScrollX() - width;
1116 if (mTempRect.left < 0) {
1117 mTempRect.left = 0;
1118 }
1119 }
1120 mTempRect.right = mTempRect.left + width;
1121
1122 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
1123 }
1124
1125 /**
1126 * <p>Handles scrolling in response to a "home/end" shortcut press. This
1127 * method will scroll the view to the left or right and give the focus
1128 * to the leftmost/rightmost component in the new visible area. If no
1129 * component is a good candidate for focus, this scrollview reclaims the
1130 * focus.</p>
1131 *
1132 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1133 * to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
1134 * to go the right
1135 * @return true if the key event is consumed by this method, false otherwise
1136 */
1137 public boolean fullScroll(int direction) {
1138 boolean right = direction == View.FOCUS_RIGHT;
1139 int width = getWidth();
1140
1141 mTempRect.left = 0;
1142 mTempRect.right = width;
1143
1144 if (right) {
1145 int count = getChildCount();
1146 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001147 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001148 mTempRect.right = view.getRight();
1149 mTempRect.left = mTempRect.right - width;
1150 }
1151 }
1152
1153 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
1154 }
1155
1156 /**
1157 * <p>Scrolls the view to make the area defined by <code>left</code> and
1158 * <code>right</code> visible. This method attempts to give the focus
1159 * to a component visible in this area. If no component can be focused in
1160 * the new visible area, the focus is reclaimed by this scrollview.</p>
1161 *
1162 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1163 * to go left {@link android.view.View#FOCUS_RIGHT} to right
1164 * @param left the left offset of the new area to be made visible
1165 * @param right the right offset of the new area to be made visible
1166 * @return true if the key event is consumed by this method, false otherwise
1167 */
1168 private boolean scrollAndFocus(int direction, int left, int right) {
1169 boolean handled = true;
1170
1171 int width = getWidth();
1172 int containerLeft = getScrollX();
1173 int containerRight = containerLeft + width;
1174 boolean goLeft = direction == View.FOCUS_LEFT;
1175
1176 View newFocused = findFocusableViewInBounds(goLeft, left, right);
1177 if (newFocused == null) {
1178 newFocused = this;
1179 }
1180
1181 if (left >= containerLeft && right <= containerRight) {
1182 handled = false;
1183 } else {
1184 int delta = goLeft ? (left - containerLeft) : (right - containerRight);
1185 doScrollX(delta);
1186 }
1187
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001188 if (newFocused != findFocus()) newFocused.requestFocus(direction);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001189
1190 return handled;
1191 }
1192
1193 /**
1194 * Handle scrolling in response to a left or right arrow click.
1195 *
1196 * @param direction The direction corresponding to the arrow key that was
1197 * pressed
1198 * @return True if we consumed the event, false otherwise
1199 */
1200 public boolean arrowScroll(int direction) {
1201
1202 View currentFocused = findFocus();
1203 if (currentFocused == this) currentFocused = null;
1204
1205 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1206
1207 final int maxJump = getMaxScrollAmount();
1208
1209 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
1210 nextFocused.getDrawingRect(mTempRect);
1211 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1212 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1213 doScrollX(scrollDelta);
1214 nextFocused.requestFocus(direction);
1215 } else {
1216 // no new focus
1217 int scrollDelta = maxJump;
1218
1219 if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
1220 scrollDelta = getScrollX();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001221 } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001222
Romain Guyef0e9ae2009-07-10 14:11:26 -07001223 int daRight = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001224
1225 int screenRight = getScrollX() + getWidth();
1226
1227 if (daRight - screenRight < maxJump) {
1228 scrollDelta = daRight - screenRight;
1229 }
1230 }
1231 if (scrollDelta == 0) {
1232 return false;
1233 }
1234 doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
1235 }
1236
1237 if (currentFocused != null && currentFocused.isFocused()
1238 && isOffScreen(currentFocused)) {
1239 // previously focused item still has focus and is off screen, give
1240 // it up (take it back to ourselves)
1241 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1242 // sure to
1243 // get it)
1244 final int descendantFocusability = getDescendantFocusability(); // save
1245 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1246 requestFocus();
1247 setDescendantFocusability(descendantFocusability); // restore
1248 }
1249 return true;
1250 }
1251
1252 /**
1253 * @return whether the descendant of this scroll view is scrolled off
1254 * screen.
1255 */
1256 private boolean isOffScreen(View descendant) {
1257 return !isWithinDeltaOfScreen(descendant, 0);
1258 }
1259
1260 /**
1261 * @return whether the descendant of this scroll view is within delta
1262 * pixels of being on the screen.
1263 */
1264 private boolean isWithinDeltaOfScreen(View descendant, int delta) {
1265 descendant.getDrawingRect(mTempRect);
1266 offsetDescendantRectToMyCoords(descendant, mTempRect);
1267
1268 return (mTempRect.right + delta) >= getScrollX()
1269 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
1270 }
1271
1272 /**
1273 * Smooth scroll by a X delta
1274 *
1275 * @param delta the number of pixels to scroll by on the X axis
1276 */
1277 private void doScrollX(int delta) {
1278 if (delta != 0) {
1279 if (mSmoothScrollingEnabled) {
1280 smoothScrollBy(delta, 0);
1281 } else {
1282 scrollBy(delta, 0);
1283 }
1284 }
1285 }
1286
1287 /**
1288 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1289 *
1290 * @param dx the number of pixels to scroll by on the X axis
1291 * @param dy the number of pixels to scroll by on the Y axis
1292 */
1293 public final void smoothScrollBy(int dx, int dy) {
Adam Powell3fc37372010-01-28 13:52:24 -08001294 if (getChildCount() == 0) {
1295 // Nothing to do.
1296 return;
1297 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001298 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1299 if (duration > ANIMATED_SCROLL_GAP) {
Adam Powellf5446052010-01-28 17:24:56 -08001300 final int width = getWidth() - mPaddingRight - mPaddingLeft;
1301 final int right = getChildAt(0).getWidth();
1302 final int maxX = Math.max(0, right - width);
1303 final int scrollX = mScrollX;
1304 dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
1305
1306 mScroller.startScroll(scrollX, mScrollY, dx, 0);
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001307 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001308 } else {
1309 if (!mScroller.isFinished()) {
1310 mScroller.abortAnimation();
1311 }
1312 scrollBy(dx, dy);
1313 }
1314 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1315 }
1316
1317 /**
1318 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1319 *
1320 * @param x the position where to scroll on the X axis
1321 * @param y the position where to scroll on the Y axis
1322 */
1323 public final void smoothScrollTo(int x, int y) {
1324 smoothScrollBy(x - mScrollX, y - mScrollY);
1325 }
1326
1327 /**
1328 * <p>The scroll range of a scroll view is the overall width of all of its
1329 * children.</p>
1330 */
1331 @Override
1332 protected int computeHorizontalScrollRange() {
Adam Powella2f91012010-02-16 12:26:33 -08001333 final int count = getChildCount();
1334 final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
Adam Powell0b8bb422010-02-08 14:30:45 -08001335 if (count == 0) {
Adam Powella2f91012010-02-16 12:26:33 -08001336 return contentWidth;
Adam Powell0b8bb422010-02-08 14:30:45 -08001337 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001338
Adam Powell637d3372010-08-25 14:37:03 -07001339 int scrollRange = getChildAt(0).getRight();
1340 final int scrollX = mScrollX;
1341 final int overscrollRight = Math.max(0, scrollRange - contentWidth);
1342 if (scrollX < 0) {
1343 scrollRange -= scrollX;
1344 } else if (scrollX > overscrollRight) {
1345 scrollRange += scrollX - overscrollRight;
1346 }
1347
1348 return scrollRange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001349 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001350
Adam Powell0b8bb422010-02-08 14:30:45 -08001351 @Override
1352 protected int computeHorizontalScrollOffset() {
1353 return Math.max(0, super.computeHorizontalScrollOffset());
1354 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001355
1356 @Override
Yigit Boyar115a6f42016-03-22 13:36:47 -07001357 protected void measureChild(View child, int parentWidthMeasureSpec,
1358 int parentHeightMeasureSpec) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001359 ViewGroup.LayoutParams lp = child.getLayoutParams();
1360
Yigit Boyar115a6f42016-03-22 13:36:47 -07001361 final int horizontalPadding = mPaddingLeft + mPaddingRight;
1362 final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
1363 Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - horizontalPadding),
1364 MeasureSpec.UNSPECIFIED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001365
Yigit Boyar115a6f42016-03-22 13:36:47 -07001366 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1367 mPaddingTop + mPaddingBottom, lp.height);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001368 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1369 }
1370
1371 @Override
1372 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1373 int parentHeightMeasureSpec, int heightUsed) {
1374 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1375
1376 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1377 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
1378 + heightUsed, lp.height);
Yigit Boyar115a6f42016-03-22 13:36:47 -07001379 final int usedTotal = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
1380 widthUsed;
1381 final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
1382 Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
1383 MeasureSpec.UNSPECIFIED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001384
1385 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1386 }
1387
1388 @Override
1389 public void computeScroll() {
1390 if (mScroller.computeScrollOffset()) {
1391 // This is called at drawing time by ViewGroup. We don't want to
1392 // re-show the scrollbars at this point, which scrollTo will do,
1393 // so we replicate most of scrollTo here.
1394 //
1395 // It's a little odd to call onScrollChanged from inside the drawing.
1396 //
1397 // It is, except when you remember that computeScroll() is used to
1398 // animate scrolling. So unless we want to defer the onScrollChanged()
1399 // until the end of the animated scrolling, we don't really have a
1400 // choice here.
1401 //
1402 // I agree. The alternative, which I think would be worse, is to post
1403 // something and tell the subclasses later. This is bad because there
1404 // will be a window where mScrollX/Y is different from what the app
1405 // thinks it is.
1406 //
1407 int oldX = mScrollX;
1408 int oldY = mScrollY;
1409 int x = mScroller.getCurrX();
1410 int y = mScroller.getCurrY();
Adam Powell17dfce12010-01-25 18:38:22 -08001411
Adam Powell637d3372010-08-25 14:37:03 -07001412 if (oldX != x || oldY != y) {
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001413 final int range = getScrollRange();
1414 final int overscrollMode = getOverScrollMode();
1415 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
1416 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
1417
1418 overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001419 mOverflingDistance, 0, false);
1420 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1421
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001422 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -07001423 if (x < 0 && oldX >= 0) {
1424 mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
1425 } else if (x > range && oldX <= range) {
1426 mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
1427 }
Adam Powell9d32d242010-03-29 16:02:07 -07001428 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001429 }
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001430
Romain Guye979e622012-03-20 13:50:27 -07001431 if (!awakenScrollBars()) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001432 postInvalidateOnAnimation();
Romain Guye979e622012-03-20 13:50:27 -07001433 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001434 }
1435 }
1436
1437 /**
1438 * Scrolls the view to the given child.
1439 *
1440 * @param child the View to scroll to
1441 */
1442 private void scrollToChild(View child) {
1443 child.getDrawingRect(mTempRect);
1444
1445 /* Offset from child's local coordinates to ScrollView coordinates */
1446 offsetDescendantRectToMyCoords(child, mTempRect);
1447
1448 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1449
1450 if (scrollDelta != 0) {
1451 scrollBy(scrollDelta, 0);
1452 }
1453 }
1454
1455 /**
1456 * If rect is off screen, scroll just enough to get it (or at least the
1457 * first screen size chunk of it) on screen.
1458 *
1459 * @param rect The rectangle.
1460 * @param immediate True to scroll immediately without animation
1461 * @return true if scrolling was performed
1462 */
1463 private boolean scrollToChildRect(Rect rect, boolean immediate) {
1464 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1465 final boolean scroll = delta != 0;
1466 if (scroll) {
1467 if (immediate) {
1468 scrollBy(delta, 0);
1469 } else {
1470 smoothScrollBy(delta, 0);
1471 }
1472 }
1473 return scroll;
1474 }
1475
1476 /**
1477 * Compute the amount to scroll in the X direction in order to get
1478 * a rectangle completely on the screen (or, if taller than the screen,
1479 * at least the first screen size chunk of it).
1480 *
1481 * @param rect The rect.
1482 * @return The scroll delta.
1483 */
1484 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001485 if (getChildCount() == 0) return 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001486
1487 int width = getWidth();
1488 int screenLeft = getScrollX();
1489 int screenRight = screenLeft + width;
1490
1491 int fadingEdge = getHorizontalFadingEdgeLength();
1492
1493 // leave room for left fading edge as long as rect isn't at very left
1494 if (rect.left > 0) {
1495 screenLeft += fadingEdge;
1496 }
1497
1498 // leave room for right fading edge as long as rect isn't at very right
1499 if (rect.right < getChildAt(0).getWidth()) {
1500 screenRight -= fadingEdge;
1501 }
1502
1503 int scrollXDelta = 0;
1504
1505 if (rect.right > screenRight && rect.left > screenLeft) {
1506 // need to move right to get it in view: move right just enough so
1507 // that the entire rectangle is in view (or at least the first
1508 // screen size chunk).
1509
1510 if (rect.width() > width) {
1511 // just enough to get screen size chunk on
1512 scrollXDelta += (rect.left - screenLeft);
1513 } else {
1514 // get entire rect at right of screen
1515 scrollXDelta += (rect.right - screenRight);
1516 }
1517
1518 // make sure we aren't scrolling beyond the end of our content
Romain Guyef0e9ae2009-07-10 14:11:26 -07001519 int right = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001520 int distanceToRight = right - screenRight;
1521 scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1522
1523 } else if (rect.left < screenLeft && rect.right < screenRight) {
1524 // need to move right to get it in view: move right just enough so that
1525 // entire rectangle is in view (or at least the first screen
1526 // size chunk of it).
1527
1528 if (rect.width() > width) {
1529 // screen size chunk
1530 scrollXDelta -= (screenRight - rect.right);
1531 } else {
1532 // entire rect at left
1533 scrollXDelta -= (screenLeft - rect.left);
1534 }
1535
1536 // make sure we aren't scrolling any further than the left our content
1537 scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1538 }
1539 return scrollXDelta;
1540 }
1541
1542 @Override
1543 public void requestChildFocus(View child, View focused) {
Adam Powell9b24e5c2017-04-04 15:33:23 -07001544 if (focused != null && focused.getRevealOnFocusHint()) {
Adam Powell2fe301d2016-08-15 16:34:37 -07001545 if (!mIsLayoutDirty) {
1546 scrollToChild(focused);
1547 } else {
1548 // The child may not be laid out yet, we can't compute the scroll yet
1549 mChildToScrollTo = focused;
1550 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001551 }
1552 super.requestChildFocus(child, focused);
1553 }
1554
1555
1556 /**
1557 * When looking for focus in children of a scroll view, need to be a little
1558 * more careful not to give focus to something that is scrolled off screen.
1559 *
1560 * This is more expensive than the default {@link android.view.ViewGroup}
1561 * implementation, otherwise this behavior might have been made the default.
1562 */
1563 @Override
1564 protected boolean onRequestFocusInDescendants(int direction,
1565 Rect previouslyFocusedRect) {
1566
1567 // convert from forward / backward notation to up / down / left / right
1568 // (ugh).
1569 if (direction == View.FOCUS_FORWARD) {
1570 direction = View.FOCUS_RIGHT;
1571 } else if (direction == View.FOCUS_BACKWARD) {
1572 direction = View.FOCUS_LEFT;
1573 }
1574
1575 final View nextFocus = previouslyFocusedRect == null ?
1576 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1577 FocusFinder.getInstance().findNextFocusFromRect(this,
1578 previouslyFocusedRect, direction);
1579
1580 if (nextFocus == null) {
1581 return false;
1582 }
1583
1584 if (isOffScreen(nextFocus)) {
1585 return false;
1586 }
1587
1588 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1589 }
1590
1591 @Override
1592 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1593 boolean immediate) {
1594 // offset into coordinate space of this scroll view
1595 rectangle.offset(child.getLeft() - child.getScrollX(),
1596 child.getTop() - child.getScrollY());
1597
1598 return scrollToChildRect(rectangle, immediate);
1599 }
1600
1601 @Override
1602 public void requestLayout() {
1603 mIsLayoutDirty = true;
1604 super.requestLayout();
1605 }
1606
1607 @Override
1608 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001609 int childWidth = 0;
1610 int childMargins = 0;
1611
1612 if (getChildCount() > 0) {
1613 childWidth = getChildAt(0).getMeasuredWidth();
1614 LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams();
1615 childMargins = childParams.leftMargin + childParams.rightMargin;
1616 }
1617
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001618 final int available = r - l - getPaddingLeftWithForeground() -
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001619 getPaddingRightWithForeground() - childMargins;
1620
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001621 final boolean forceLeftGravity = (childWidth > available);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001622
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001623 layoutChildren(l, t, r, b, forceLeftGravity);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001624
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001625 mIsLayoutDirty = false;
1626 // Give a child focus if it needs it
1627 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
Adam Powell90f339a2013-06-13 17:44:04 -07001628 scrollToChild(mChildToScrollTo);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001629 }
1630 mChildToScrollTo = null;
1631
Chet Haase7a46dde2013-07-17 10:22:53 -07001632 if (!isLaidOut()) {
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001633 final int scrollRange = Math.max(0,
1634 childWidth - (r - l - mPaddingLeft - mPaddingRight));
1635 if (mSavedState != null) {
Adam Powella3aa6d822015-06-02 17:47:50 -07001636 mScrollX = isLayoutRtl()
1637 ? scrollRange - mSavedState.scrollOffsetFromStart
1638 : mSavedState.scrollOffsetFromStart;
Fabrice Di Megliofafe88c2013-06-12 18:18:08 -07001639 mSavedState = null;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001640 } else {
1641 if (isLayoutRtl()) {
1642 mScrollX = scrollRange - mScrollX;
1643 } // mScrollX default value is "0" for LTR
1644 }
1645 // Don't forget to clamp
1646 if (mScrollX > scrollRange) {
1647 mScrollX = scrollRange;
1648 } else if (mScrollX < 0) {
1649 mScrollX = 0;
1650 }
1651 }
1652
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001653 // Calling this with the present values causes it to re-claim them
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001654 scrollTo(mScrollX, mScrollY);
1655 }
1656
1657 @Override
1658 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1659 super.onSizeChanged(w, h, oldw, oldh);
1660
1661 View currentFocused = findFocus();
1662 if (null == currentFocused || this == currentFocused)
1663 return;
1664
1665 final int maxJump = mRight - mLeft;
1666
1667 if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
1668 currentFocused.getDrawingRect(mTempRect);
1669 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1670 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1671 doScrollX(scrollDelta);
1672 }
1673 }
1674
1675 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001676 * Return true if child is a descendant of parent, (or equal to the parent).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001677 */
Romain Guye979e622012-03-20 13:50:27 -07001678 private static boolean isViewDescendantOf(View child, View parent) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001679 if (child == parent) {
1680 return true;
1681 }
1682
1683 final ViewParent theParent = child.getParent();
1684 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1685 }
1686
1687 /**
1688 * Fling the scroll view
1689 *
1690 * @param velocityX The initial velocity in the X direction. Positive
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001691 * numbers mean that the finger/cursor is moving down the screen,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001692 * which means we want to scroll towards the left.
1693 */
1694 public void fling(int velocityX) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001695 if (getChildCount() > 0) {
1696 int width = getWidth() - mPaddingRight - mPaddingLeft;
1697 int right = getChildAt(0).getWidth();
Mindy Pereira4e30d892010-11-24 15:32:39 -08001698
1699 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001700 Math.max(0, right - width), 0, 0, width/2, 0);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001701
Romain Guyef0e9ae2009-07-10 14:11:26 -07001702 final boolean movingRight = velocityX > 0;
Mindy Pereira4e30d892010-11-24 15:32:39 -08001703
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001704 View currentFocused = findFocus();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001705 View newFocused = findFocusableViewInMyBounds(movingRight,
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001706 mScroller.getFinalX(), currentFocused);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001707
Romain Guyef0e9ae2009-07-10 14:11:26 -07001708 if (newFocused == null) {
1709 newFocused = this;
1710 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001711
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001712 if (newFocused != currentFocused) {
1713 newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
Romain Guyef0e9ae2009-07-10 14:11:26 -07001714 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001715
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001716 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001717 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001718 }
1719
1720 /**
1721 * {@inheritDoc}
1722 *
1723 * <p>This version also clamps the scrolling to the bounds of our child.
1724 */
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001725 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001726 public void scrollTo(int x, int y) {
1727 // we rely on the fact the View.scrollBy calls scrollTo.
1728 if (getChildCount() > 0) {
1729 View child = getChildAt(0);
1730 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1731 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1732 if (x != mScrollX || y != mScrollY) {
1733 super.scrollTo(x, y);
1734 }
1735 }
1736 }
1737
Yigit Boyar08b1d992019-02-06 12:34:21 -08001738 private boolean shouldDisplayEdgeEffects() {
1739 return getOverScrollMode() != OVER_SCROLL_NEVER;
Adam Powell637d3372010-08-25 14:37:03 -07001740 }
1741
Romain Guy2243e552011-03-08 11:46:28 -08001742 @SuppressWarnings({"SuspiciousNameCombination"})
Adam Powell637d3372010-08-25 14:37:03 -07001743 @Override
1744 public void draw(Canvas canvas) {
1745 super.draw(canvas);
Yigit Boyar08b1d992019-02-06 12:34:21 -08001746 if (shouldDisplayEdgeEffects()) {
Adam Powell637d3372010-08-25 14:37:03 -07001747 final int scrollX = mScrollX;
1748 if (!mEdgeGlowLeft.isFinished()) {
1749 final int restoreCount = canvas.save();
Adam Powell7d863782011-02-15 15:05:03 -08001750 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001751
1752 canvas.rotate(270);
Adam Powell7d863782011-02-15 15:05:03 -08001753 canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
1754 mEdgeGlowLeft.setSize(height, getWidth());
Adam Powell637d3372010-08-25 14:37:03 -07001755 if (mEdgeGlowLeft.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001756 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001757 }
1758 canvas.restoreToCount(restoreCount);
1759 }
1760 if (!mEdgeGlowRight.isFinished()) {
1761 final int restoreCount = canvas.save();
1762 final int width = getWidth();
Adam Powell7d863782011-02-15 15:05:03 -08001763 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001764
1765 canvas.rotate(90);
Adam Powell7d863782011-02-15 15:05:03 -08001766 canvas.translate(-mPaddingTop,
Mindy Pereirab1297f72010-12-07 15:06:47 -08001767 -(Math.max(getScrollRange(), scrollX) + width));
1768 mEdgeGlowRight.setSize(height, width);
Adam Powell637d3372010-08-25 14:37:03 -07001769 if (mEdgeGlowRight.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001770 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001771 }
1772 canvas.restoreToCount(restoreCount);
1773 }
1774 }
1775 }
1776
Romain Guye979e622012-03-20 13:50:27 -07001777 private static int clamp(int n, int my, int child) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001778 if (my >= child || n < 0) {
1779 return 0;
1780 }
1781 if ((my + n) > child) {
1782 return child - my;
1783 }
1784 return n;
1785 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001786
1787 @Override
1788 protected void onRestoreInstanceState(Parcelable state) {
Adam Powell90f339a2013-06-13 17:44:04 -07001789 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1790 // Some old apps reused IDs in ways they shouldn't have.
1791 // Don't break them, but they don't get scroll state restoration.
1792 super.onRestoreInstanceState(state);
1793 return;
1794 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001795 SavedState ss = (SavedState) state;
1796 super.onRestoreInstanceState(ss.getSuperState());
1797 mSavedState = ss;
1798 requestLayout();
1799 }
1800
1801 @Override
1802 protected Parcelable onSaveInstanceState() {
Adam Powell90f339a2013-06-13 17:44:04 -07001803 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1804 // Some old apps reused IDs in ways they shouldn't have.
1805 // Don't break them, but they don't get scroll state restoration.
1806 return super.onSaveInstanceState();
1807 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001808 Parcelable superState = super.onSaveInstanceState();
1809 SavedState ss = new SavedState(superState);
Adam Powella3aa6d822015-06-02 17:47:50 -07001810 ss.scrollOffsetFromStart = isLayoutRtl() ? -mScrollX : mScrollX;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001811 return ss;
1812 }
1813
Siva Velusamy94a6d152015-05-05 15:07:00 -07001814 /** @hide */
1815 @Override
1816 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
1817 super.encodeProperties(encoder);
1818 encoder.addProperty("layout:fillViewPort", mFillViewport);
1819 }
1820
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001821 static class SavedState extends BaseSavedState {
Adam Powella3aa6d822015-06-02 17:47:50 -07001822 public int scrollOffsetFromStart;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001823
1824 SavedState(Parcelable superState) {
1825 super(superState);
1826 }
1827
1828 public SavedState(Parcel source) {
1829 super(source);
Adam Powella3aa6d822015-06-02 17:47:50 -07001830 scrollOffsetFromStart = source.readInt();
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001831 }
1832
1833 @Override
1834 public void writeToParcel(Parcel dest, int flags) {
1835 super.writeToParcel(dest, flags);
Adam Powella3aa6d822015-06-02 17:47:50 -07001836 dest.writeInt(scrollOffsetFromStart);
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001837 }
1838
1839 @Override
1840 public String toString() {
1841 return "HorizontalScrollView.SavedState{"
1842 + Integer.toHexString(System.identityHashCode(this))
Adam Powella3aa6d822015-06-02 17:47:50 -07001843 + " scrollPosition=" + scrollOffsetFromStart
1844 + "}";
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001845 }
1846
1847 public static final Parcelable.Creator<SavedState> CREATOR
1848 = new Parcelable.Creator<SavedState>() {
1849 public SavedState createFromParcel(Parcel in) {
1850 return new SavedState(in);
1851 }
1852
1853 public SavedState[] newArray(int size) {
1854 return new SavedState[size];
1855 }
1856 };
1857 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001858}