blob: b33660a200c22bf40aa20a12e5b2eaa28dc1c170 [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;
Artur Satayeved5a6ae2019-12-10 17:47:54 +000021import android.compat.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);
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700954 event.setMaxScrollX(getScrollRange());
955 event.setMaxScrollY(mScrollY);
Svetoslav Ganova0156172011-06-26 17:55:44 -0700956 }
957
Adam Powell0b8bb422010-02-08 14:30:45 -0800958 private int getScrollRange() {
959 int scrollRange = 0;
960 if (getChildCount() > 0) {
961 View child = getChildAt(0);
962 scrollRange = Math.max(0,
Adam Powell637d3372010-08-25 14:37:03 -0700963 child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
Adam Powell0b8bb422010-02-08 14:30:45 -0800964 }
965 return scrollRange;
966 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800967
968 /**
969 * <p>
970 * Finds the next focusable component that fits in this View's bounds
971 * (excluding fading edges) pretending that this View's left is located at
972 * the parameter left.
973 * </p>
974 *
975 * @param leftFocus look for a candidate is the one at the left of the bounds
976 * if leftFocus is true, or at the right of the bounds if leftFocus
977 * is false
978 * @param left the left offset of the bounds in which a focusable must be
979 * found (the fading edge is assumed to start at this position)
980 * @param preferredFocusable the View that has highest priority and will be
981 * returned if it is within my bounds (null is valid)
982 * @return the next focusable component in the bounds or null if none can be found
983 */
984 private View findFocusableViewInMyBounds(final boolean leftFocus,
985 final int left, View preferredFocusable) {
986 /*
987 * The fading edge's transparent side should be considered for focus
988 * since it's mostly visible, so we divide the actual fading edge length
989 * by 2.
990 */
991 final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
992 final int leftWithoutFadingEdge = left + fadingEdgeLength;
993 final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
994
995 if ((preferredFocusable != null)
996 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
997 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
998 return preferredFocusable;
999 }
1000
1001 return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
1002 rightWithoutFadingEdge);
1003 }
1004
1005 /**
1006 * <p>
1007 * Finds the next focusable component that fits in the specified bounds.
1008 * </p>
1009 *
1010 * @param leftFocus look for a candidate is the one at the left of the bounds
1011 * if leftFocus is true, or at the right of the bounds if
1012 * leftFocus is false
1013 * @param left the left offset of the bounds in which a focusable must be
1014 * found
1015 * @param right the right offset of the bounds in which a focusable must
1016 * be found
1017 * @return the next focusable component in the bounds or null if none can
1018 * be found
1019 */
1020 private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
1021
1022 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
1023 View focusCandidate = null;
1024
1025 /*
1026 * A fully contained focusable is one where its left is below the bound's
1027 * left, and its right is above the bound's right. A partially
1028 * contained focusable is one where some part of it is within the
1029 * bounds, but it also has some part that is not within bounds. A fully contained
1030 * focusable is preferred to a partially contained focusable.
1031 */
1032 boolean foundFullyContainedFocusable = false;
1033
1034 int count = focusables.size();
1035 for (int i = 0; i < count; i++) {
1036 View view = focusables.get(i);
1037 int viewLeft = view.getLeft();
1038 int viewRight = view.getRight();
1039
1040 if (left < viewRight && viewLeft < right) {
1041 /*
1042 * the focusable is in the target area, it is a candidate for
1043 * focusing
1044 */
1045
1046 final boolean viewIsFullyContained = (left < viewLeft) &&
1047 (viewRight < right);
1048
1049 if (focusCandidate == null) {
1050 /* No candidate, take this one */
1051 focusCandidate = view;
1052 foundFullyContainedFocusable = viewIsFullyContained;
1053 } else {
1054 final boolean viewIsCloserToBoundary =
1055 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
1056 (!leftFocus && viewRight > focusCandidate.getRight());
1057
1058 if (foundFullyContainedFocusable) {
1059 if (viewIsFullyContained && viewIsCloserToBoundary) {
1060 /*
1061 * We're dealing with only fully contained views, so
1062 * it has to be closer to the boundary to beat our
1063 * candidate
1064 */
1065 focusCandidate = view;
1066 }
1067 } else {
1068 if (viewIsFullyContained) {
1069 /* Any fully contained view beats a partially contained view */
1070 focusCandidate = view;
1071 foundFullyContainedFocusable = true;
1072 } else if (viewIsCloserToBoundary) {
1073 /*
1074 * Partially contained view beats another partially
1075 * contained view if it's closer
1076 */
1077 focusCandidate = view;
1078 }
1079 }
1080 }
1081 }
1082 }
1083
1084 return focusCandidate;
1085 }
1086
1087 /**
1088 * <p>Handles scrolling in response to a "page up/down" shortcut press. This
1089 * method will scroll the view by one page left or right and give the focus
1090 * to the leftmost/rightmost component in the new visible area. If no
1091 * component is a good candidate for focus, this scrollview reclaims the
1092 * focus.</p>
1093 *
1094 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1095 * to go one page left or {@link android.view.View#FOCUS_RIGHT}
1096 * to go one page right
1097 * @return true if the key event is consumed by this method, false otherwise
1098 */
1099 public boolean pageScroll(int direction) {
1100 boolean right = direction == View.FOCUS_RIGHT;
1101 int width = getWidth();
1102
1103 if (right) {
1104 mTempRect.left = getScrollX() + width;
1105 int count = getChildCount();
1106 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001107 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001108 if (mTempRect.left + width > view.getRight()) {
1109 mTempRect.left = view.getRight() - width;
1110 }
1111 }
1112 } else {
1113 mTempRect.left = getScrollX() - width;
1114 if (mTempRect.left < 0) {
1115 mTempRect.left = 0;
1116 }
1117 }
1118 mTempRect.right = mTempRect.left + width;
1119
1120 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
1121 }
1122
1123 /**
1124 * <p>Handles scrolling in response to a "home/end" shortcut press. This
1125 * method will scroll the view to the left or right and give the focus
1126 * to the leftmost/rightmost component in the new visible area. If no
1127 * component is a good candidate for focus, this scrollview reclaims the
1128 * focus.</p>
1129 *
1130 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1131 * to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
1132 * to go the right
1133 * @return true if the key event is consumed by this method, false otherwise
1134 */
1135 public boolean fullScroll(int direction) {
1136 boolean right = direction == View.FOCUS_RIGHT;
1137 int width = getWidth();
1138
1139 mTempRect.left = 0;
1140 mTempRect.right = width;
1141
1142 if (right) {
1143 int count = getChildCount();
1144 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001145 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001146 mTempRect.right = view.getRight();
1147 mTempRect.left = mTempRect.right - width;
1148 }
1149 }
1150
1151 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
1152 }
1153
1154 /**
1155 * <p>Scrolls the view to make the area defined by <code>left</code> and
1156 * <code>right</code> visible. This method attempts to give the focus
1157 * to a component visible in this area. If no component can be focused in
1158 * the new visible area, the focus is reclaimed by this scrollview.</p>
1159 *
1160 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1161 * to go left {@link android.view.View#FOCUS_RIGHT} to right
1162 * @param left the left offset of the new area to be made visible
1163 * @param right the right offset of the new area to be made visible
1164 * @return true if the key event is consumed by this method, false otherwise
1165 */
1166 private boolean scrollAndFocus(int direction, int left, int right) {
1167 boolean handled = true;
1168
1169 int width = getWidth();
1170 int containerLeft = getScrollX();
1171 int containerRight = containerLeft + width;
1172 boolean goLeft = direction == View.FOCUS_LEFT;
1173
1174 View newFocused = findFocusableViewInBounds(goLeft, left, right);
1175 if (newFocused == null) {
1176 newFocused = this;
1177 }
1178
1179 if (left >= containerLeft && right <= containerRight) {
1180 handled = false;
1181 } else {
1182 int delta = goLeft ? (left - containerLeft) : (right - containerRight);
1183 doScrollX(delta);
1184 }
1185
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001186 if (newFocused != findFocus()) newFocused.requestFocus(direction);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001187
1188 return handled;
1189 }
1190
1191 /**
1192 * Handle scrolling in response to a left or right arrow click.
1193 *
1194 * @param direction The direction corresponding to the arrow key that was
1195 * pressed
1196 * @return True if we consumed the event, false otherwise
1197 */
1198 public boolean arrowScroll(int direction) {
1199
1200 View currentFocused = findFocus();
1201 if (currentFocused == this) currentFocused = null;
1202
1203 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1204
1205 final int maxJump = getMaxScrollAmount();
1206
1207 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
1208 nextFocused.getDrawingRect(mTempRect);
1209 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1210 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1211 doScrollX(scrollDelta);
1212 nextFocused.requestFocus(direction);
1213 } else {
1214 // no new focus
1215 int scrollDelta = maxJump;
1216
1217 if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
1218 scrollDelta = getScrollX();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001219 } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001220
Romain Guyef0e9ae2009-07-10 14:11:26 -07001221 int daRight = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001222
1223 int screenRight = getScrollX() + getWidth();
1224
1225 if (daRight - screenRight < maxJump) {
1226 scrollDelta = daRight - screenRight;
1227 }
1228 }
1229 if (scrollDelta == 0) {
1230 return false;
1231 }
1232 doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
1233 }
1234
1235 if (currentFocused != null && currentFocused.isFocused()
1236 && isOffScreen(currentFocused)) {
1237 // previously focused item still has focus and is off screen, give
1238 // it up (take it back to ourselves)
1239 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1240 // sure to
1241 // get it)
1242 final int descendantFocusability = getDescendantFocusability(); // save
1243 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1244 requestFocus();
1245 setDescendantFocusability(descendantFocusability); // restore
1246 }
1247 return true;
1248 }
1249
1250 /**
1251 * @return whether the descendant of this scroll view is scrolled off
1252 * screen.
1253 */
1254 private boolean isOffScreen(View descendant) {
1255 return !isWithinDeltaOfScreen(descendant, 0);
1256 }
1257
1258 /**
1259 * @return whether the descendant of this scroll view is within delta
1260 * pixels of being on the screen.
1261 */
1262 private boolean isWithinDeltaOfScreen(View descendant, int delta) {
1263 descendant.getDrawingRect(mTempRect);
1264 offsetDescendantRectToMyCoords(descendant, mTempRect);
1265
1266 return (mTempRect.right + delta) >= getScrollX()
1267 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
1268 }
1269
1270 /**
1271 * Smooth scroll by a X delta
1272 *
1273 * @param delta the number of pixels to scroll by on the X axis
1274 */
1275 private void doScrollX(int delta) {
1276 if (delta != 0) {
1277 if (mSmoothScrollingEnabled) {
1278 smoothScrollBy(delta, 0);
1279 } else {
1280 scrollBy(delta, 0);
1281 }
1282 }
1283 }
1284
1285 /**
1286 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1287 *
1288 * @param dx the number of pixels to scroll by on the X axis
1289 * @param dy the number of pixels to scroll by on the Y axis
1290 */
1291 public final void smoothScrollBy(int dx, int dy) {
Adam Powell3fc37372010-01-28 13:52:24 -08001292 if (getChildCount() == 0) {
1293 // Nothing to do.
1294 return;
1295 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001296 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1297 if (duration > ANIMATED_SCROLL_GAP) {
Adam Powellf5446052010-01-28 17:24:56 -08001298 final int width = getWidth() - mPaddingRight - mPaddingLeft;
1299 final int right = getChildAt(0).getWidth();
1300 final int maxX = Math.max(0, right - width);
1301 final int scrollX = mScrollX;
1302 dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
1303
1304 mScroller.startScroll(scrollX, mScrollY, dx, 0);
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001305 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001306 } else {
1307 if (!mScroller.isFinished()) {
1308 mScroller.abortAnimation();
1309 }
1310 scrollBy(dx, dy);
1311 }
1312 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1313 }
1314
1315 /**
1316 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1317 *
1318 * @param x the position where to scroll on the X axis
1319 * @param y the position where to scroll on the Y axis
1320 */
1321 public final void smoothScrollTo(int x, int y) {
1322 smoothScrollBy(x - mScrollX, y - mScrollY);
1323 }
1324
1325 /**
1326 * <p>The scroll range of a scroll view is the overall width of all of its
1327 * children.</p>
1328 */
1329 @Override
1330 protected int computeHorizontalScrollRange() {
Adam Powella2f91012010-02-16 12:26:33 -08001331 final int count = getChildCount();
1332 final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
Adam Powell0b8bb422010-02-08 14:30:45 -08001333 if (count == 0) {
Adam Powella2f91012010-02-16 12:26:33 -08001334 return contentWidth;
Adam Powell0b8bb422010-02-08 14:30:45 -08001335 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001336
Adam Powell637d3372010-08-25 14:37:03 -07001337 int scrollRange = getChildAt(0).getRight();
1338 final int scrollX = mScrollX;
1339 final int overscrollRight = Math.max(0, scrollRange - contentWidth);
1340 if (scrollX < 0) {
1341 scrollRange -= scrollX;
1342 } else if (scrollX > overscrollRight) {
1343 scrollRange += scrollX - overscrollRight;
1344 }
1345
1346 return scrollRange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001347 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001348
Adam Powell0b8bb422010-02-08 14:30:45 -08001349 @Override
1350 protected int computeHorizontalScrollOffset() {
1351 return Math.max(0, super.computeHorizontalScrollOffset());
1352 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001353
1354 @Override
Yigit Boyar115a6f42016-03-22 13:36:47 -07001355 protected void measureChild(View child, int parentWidthMeasureSpec,
1356 int parentHeightMeasureSpec) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001357 ViewGroup.LayoutParams lp = child.getLayoutParams();
1358
Yigit Boyar115a6f42016-03-22 13:36:47 -07001359 final int horizontalPadding = mPaddingLeft + mPaddingRight;
1360 final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
1361 Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - horizontalPadding),
1362 MeasureSpec.UNSPECIFIED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001363
Yigit Boyar115a6f42016-03-22 13:36:47 -07001364 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1365 mPaddingTop + mPaddingBottom, lp.height);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001366 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1367 }
1368
1369 @Override
1370 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1371 int parentHeightMeasureSpec, int heightUsed) {
1372 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1373
1374 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1375 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
1376 + heightUsed, lp.height);
Yigit Boyar115a6f42016-03-22 13:36:47 -07001377 final int usedTotal = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
1378 widthUsed;
1379 final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
1380 Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
1381 MeasureSpec.UNSPECIFIED);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001382
1383 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1384 }
1385
1386 @Override
1387 public void computeScroll() {
1388 if (mScroller.computeScrollOffset()) {
1389 // This is called at drawing time by ViewGroup. We don't want to
1390 // re-show the scrollbars at this point, which scrollTo will do,
1391 // so we replicate most of scrollTo here.
1392 //
1393 // It's a little odd to call onScrollChanged from inside the drawing.
1394 //
1395 // It is, except when you remember that computeScroll() is used to
1396 // animate scrolling. So unless we want to defer the onScrollChanged()
1397 // until the end of the animated scrolling, we don't really have a
1398 // choice here.
1399 //
1400 // I agree. The alternative, which I think would be worse, is to post
1401 // something and tell the subclasses later. This is bad because there
1402 // will be a window where mScrollX/Y is different from what the app
1403 // thinks it is.
1404 //
1405 int oldX = mScrollX;
1406 int oldY = mScrollY;
1407 int x = mScroller.getCurrX();
1408 int y = mScroller.getCurrY();
Adam Powell17dfce12010-01-25 18:38:22 -08001409
Adam Powell637d3372010-08-25 14:37:03 -07001410 if (oldX != x || oldY != y) {
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001411 final int range = getScrollRange();
1412 final int overscrollMode = getOverScrollMode();
1413 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
1414 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
1415
1416 overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001417 mOverflingDistance, 0, false);
1418 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1419
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001420 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -07001421 if (x < 0 && oldX >= 0) {
1422 mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
1423 } else if (x > range && oldX <= range) {
1424 mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
1425 }
Adam Powell9d32d242010-03-29 16:02:07 -07001426 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001427 }
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001428
Romain Guye979e622012-03-20 13:50:27 -07001429 if (!awakenScrollBars()) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001430 postInvalidateOnAnimation();
Romain Guye979e622012-03-20 13:50:27 -07001431 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001432 }
1433 }
1434
1435 /**
1436 * Scrolls the view to the given child.
1437 *
1438 * @param child the View to scroll to
1439 */
1440 private void scrollToChild(View child) {
1441 child.getDrawingRect(mTempRect);
1442
1443 /* Offset from child's local coordinates to ScrollView coordinates */
1444 offsetDescendantRectToMyCoords(child, mTempRect);
1445
1446 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1447
1448 if (scrollDelta != 0) {
1449 scrollBy(scrollDelta, 0);
1450 }
1451 }
1452
1453 /**
1454 * If rect is off screen, scroll just enough to get it (or at least the
1455 * first screen size chunk of it) on screen.
1456 *
1457 * @param rect The rectangle.
1458 * @param immediate True to scroll immediately without animation
1459 * @return true if scrolling was performed
1460 */
1461 private boolean scrollToChildRect(Rect rect, boolean immediate) {
1462 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1463 final boolean scroll = delta != 0;
1464 if (scroll) {
1465 if (immediate) {
1466 scrollBy(delta, 0);
1467 } else {
1468 smoothScrollBy(delta, 0);
1469 }
1470 }
1471 return scroll;
1472 }
1473
1474 /**
1475 * Compute the amount to scroll in the X direction in order to get
1476 * a rectangle completely on the screen (or, if taller than the screen,
1477 * at least the first screen size chunk of it).
1478 *
1479 * @param rect The rect.
1480 * @return The scroll delta.
1481 */
1482 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001483 if (getChildCount() == 0) return 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001484
1485 int width = getWidth();
1486 int screenLeft = getScrollX();
1487 int screenRight = screenLeft + width;
1488
1489 int fadingEdge = getHorizontalFadingEdgeLength();
1490
1491 // leave room for left fading edge as long as rect isn't at very left
1492 if (rect.left > 0) {
1493 screenLeft += fadingEdge;
1494 }
1495
1496 // leave room for right fading edge as long as rect isn't at very right
1497 if (rect.right < getChildAt(0).getWidth()) {
1498 screenRight -= fadingEdge;
1499 }
1500
1501 int scrollXDelta = 0;
1502
1503 if (rect.right > screenRight && rect.left > screenLeft) {
1504 // need to move right to get it in view: move right just enough so
1505 // that the entire rectangle is in view (or at least the first
1506 // screen size chunk).
1507
1508 if (rect.width() > width) {
1509 // just enough to get screen size chunk on
1510 scrollXDelta += (rect.left - screenLeft);
1511 } else {
1512 // get entire rect at right of screen
1513 scrollXDelta += (rect.right - screenRight);
1514 }
1515
1516 // make sure we aren't scrolling beyond the end of our content
Romain Guyef0e9ae2009-07-10 14:11:26 -07001517 int right = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001518 int distanceToRight = right - screenRight;
1519 scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1520
1521 } else if (rect.left < screenLeft && rect.right < screenRight) {
1522 // need to move right to get it in view: move right just enough so that
1523 // entire rectangle is in view (or at least the first screen
1524 // size chunk of it).
1525
1526 if (rect.width() > width) {
1527 // screen size chunk
1528 scrollXDelta -= (screenRight - rect.right);
1529 } else {
1530 // entire rect at left
1531 scrollXDelta -= (screenLeft - rect.left);
1532 }
1533
1534 // make sure we aren't scrolling any further than the left our content
1535 scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1536 }
1537 return scrollXDelta;
1538 }
1539
1540 @Override
1541 public void requestChildFocus(View child, View focused) {
Adam Powell9b24e5c2017-04-04 15:33:23 -07001542 if (focused != null && focused.getRevealOnFocusHint()) {
Adam Powell2fe301d2016-08-15 16:34:37 -07001543 if (!mIsLayoutDirty) {
1544 scrollToChild(focused);
1545 } else {
1546 // The child may not be laid out yet, we can't compute the scroll yet
1547 mChildToScrollTo = focused;
1548 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001549 }
1550 super.requestChildFocus(child, focused);
1551 }
1552
1553
1554 /**
1555 * When looking for focus in children of a scroll view, need to be a little
1556 * more careful not to give focus to something that is scrolled off screen.
1557 *
1558 * This is more expensive than the default {@link android.view.ViewGroup}
1559 * implementation, otherwise this behavior might have been made the default.
1560 */
1561 @Override
1562 protected boolean onRequestFocusInDescendants(int direction,
1563 Rect previouslyFocusedRect) {
1564
1565 // convert from forward / backward notation to up / down / left / right
1566 // (ugh).
1567 if (direction == View.FOCUS_FORWARD) {
1568 direction = View.FOCUS_RIGHT;
1569 } else if (direction == View.FOCUS_BACKWARD) {
1570 direction = View.FOCUS_LEFT;
1571 }
1572
1573 final View nextFocus = previouslyFocusedRect == null ?
1574 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1575 FocusFinder.getInstance().findNextFocusFromRect(this,
1576 previouslyFocusedRect, direction);
1577
1578 if (nextFocus == null) {
1579 return false;
1580 }
1581
1582 if (isOffScreen(nextFocus)) {
1583 return false;
1584 }
1585
1586 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1587 }
1588
1589 @Override
1590 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1591 boolean immediate) {
1592 // offset into coordinate space of this scroll view
1593 rectangle.offset(child.getLeft() - child.getScrollX(),
1594 child.getTop() - child.getScrollY());
1595
1596 return scrollToChildRect(rectangle, immediate);
1597 }
1598
1599 @Override
1600 public void requestLayout() {
1601 mIsLayoutDirty = true;
1602 super.requestLayout();
1603 }
1604
1605 @Override
1606 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001607 int childWidth = 0;
1608 int childMargins = 0;
1609
1610 if (getChildCount() > 0) {
1611 childWidth = getChildAt(0).getMeasuredWidth();
1612 LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams();
1613 childMargins = childParams.leftMargin + childParams.rightMargin;
1614 }
1615
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001616 final int available = r - l - getPaddingLeftWithForeground() -
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001617 getPaddingRightWithForeground() - childMargins;
1618
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001619 final boolean forceLeftGravity = (childWidth > available);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001620
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001621 layoutChildren(l, t, r, b, forceLeftGravity);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001622
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001623 mIsLayoutDirty = false;
1624 // Give a child focus if it needs it
1625 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
Adam Powell90f339a2013-06-13 17:44:04 -07001626 scrollToChild(mChildToScrollTo);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001627 }
1628 mChildToScrollTo = null;
1629
Chet Haase7a46dde2013-07-17 10:22:53 -07001630 if (!isLaidOut()) {
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001631 final int scrollRange = Math.max(0,
1632 childWidth - (r - l - mPaddingLeft - mPaddingRight));
1633 if (mSavedState != null) {
Adam Powella3aa6d822015-06-02 17:47:50 -07001634 mScrollX = isLayoutRtl()
1635 ? scrollRange - mSavedState.scrollOffsetFromStart
1636 : mSavedState.scrollOffsetFromStart;
Fabrice Di Megliofafe88c2013-06-12 18:18:08 -07001637 mSavedState = null;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001638 } else {
1639 if (isLayoutRtl()) {
1640 mScrollX = scrollRange - mScrollX;
1641 } // mScrollX default value is "0" for LTR
1642 }
1643 // Don't forget to clamp
1644 if (mScrollX > scrollRange) {
1645 mScrollX = scrollRange;
1646 } else if (mScrollX < 0) {
1647 mScrollX = 0;
1648 }
1649 }
1650
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001651 // Calling this with the present values causes it to re-claim them
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001652 scrollTo(mScrollX, mScrollY);
1653 }
1654
1655 @Override
1656 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1657 super.onSizeChanged(w, h, oldw, oldh);
1658
1659 View currentFocused = findFocus();
1660 if (null == currentFocused || this == currentFocused)
1661 return;
1662
1663 final int maxJump = mRight - mLeft;
1664
1665 if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
1666 currentFocused.getDrawingRect(mTempRect);
1667 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1668 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1669 doScrollX(scrollDelta);
1670 }
1671 }
1672
1673 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001674 * Return true if child is a descendant of parent, (or equal to the parent).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001675 */
Romain Guye979e622012-03-20 13:50:27 -07001676 private static boolean isViewDescendantOf(View child, View parent) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001677 if (child == parent) {
1678 return true;
1679 }
1680
1681 final ViewParent theParent = child.getParent();
1682 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1683 }
1684
1685 /**
1686 * Fling the scroll view
1687 *
1688 * @param velocityX The initial velocity in the X direction. Positive
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001689 * numbers mean that the finger/cursor is moving down the screen,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001690 * which means we want to scroll towards the left.
1691 */
1692 public void fling(int velocityX) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001693 if (getChildCount() > 0) {
1694 int width = getWidth() - mPaddingRight - mPaddingLeft;
1695 int right = getChildAt(0).getWidth();
Mindy Pereira4e30d892010-11-24 15:32:39 -08001696
1697 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001698 Math.max(0, right - width), 0, 0, width/2, 0);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001699
Romain Guyef0e9ae2009-07-10 14:11:26 -07001700 final boolean movingRight = velocityX > 0;
Mindy Pereira4e30d892010-11-24 15:32:39 -08001701
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001702 View currentFocused = findFocus();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001703 View newFocused = findFocusableViewInMyBounds(movingRight,
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001704 mScroller.getFinalX(), currentFocused);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001705
Romain Guyef0e9ae2009-07-10 14:11:26 -07001706 if (newFocused == null) {
1707 newFocused = this;
1708 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001709
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001710 if (newFocused != currentFocused) {
1711 newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
Romain Guyef0e9ae2009-07-10 14:11:26 -07001712 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001713
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001714 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001715 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001716 }
1717
1718 /**
1719 * {@inheritDoc}
1720 *
1721 * <p>This version also clamps the scrolling to the bounds of our child.
1722 */
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001723 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001724 public void scrollTo(int x, int y) {
1725 // we rely on the fact the View.scrollBy calls scrollTo.
1726 if (getChildCount() > 0) {
1727 View child = getChildAt(0);
1728 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1729 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1730 if (x != mScrollX || y != mScrollY) {
1731 super.scrollTo(x, y);
1732 }
1733 }
1734 }
1735
Yigit Boyar08b1d992019-02-06 12:34:21 -08001736 private boolean shouldDisplayEdgeEffects() {
1737 return getOverScrollMode() != OVER_SCROLL_NEVER;
Adam Powell637d3372010-08-25 14:37:03 -07001738 }
1739
Romain Guy2243e552011-03-08 11:46:28 -08001740 @SuppressWarnings({"SuspiciousNameCombination"})
Adam Powell637d3372010-08-25 14:37:03 -07001741 @Override
1742 public void draw(Canvas canvas) {
1743 super.draw(canvas);
Yigit Boyar08b1d992019-02-06 12:34:21 -08001744 if (shouldDisplayEdgeEffects()) {
Adam Powell637d3372010-08-25 14:37:03 -07001745 final int scrollX = mScrollX;
1746 if (!mEdgeGlowLeft.isFinished()) {
1747 final int restoreCount = canvas.save();
Adam Powell7d863782011-02-15 15:05:03 -08001748 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001749
1750 canvas.rotate(270);
Adam Powell7d863782011-02-15 15:05:03 -08001751 canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
1752 mEdgeGlowLeft.setSize(height, getWidth());
Adam Powell637d3372010-08-25 14:37:03 -07001753 if (mEdgeGlowLeft.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001754 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001755 }
1756 canvas.restoreToCount(restoreCount);
1757 }
1758 if (!mEdgeGlowRight.isFinished()) {
1759 final int restoreCount = canvas.save();
1760 final int width = getWidth();
Adam Powell7d863782011-02-15 15:05:03 -08001761 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001762
1763 canvas.rotate(90);
Adam Powell7d863782011-02-15 15:05:03 -08001764 canvas.translate(-mPaddingTop,
Mindy Pereirab1297f72010-12-07 15:06:47 -08001765 -(Math.max(getScrollRange(), scrollX) + width));
1766 mEdgeGlowRight.setSize(height, width);
Adam Powell637d3372010-08-25 14:37:03 -07001767 if (mEdgeGlowRight.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001768 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001769 }
1770 canvas.restoreToCount(restoreCount);
1771 }
1772 }
1773 }
1774
Romain Guye979e622012-03-20 13:50:27 -07001775 private static int clamp(int n, int my, int child) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001776 if (my >= child || n < 0) {
1777 return 0;
1778 }
1779 if ((my + n) > child) {
1780 return child - my;
1781 }
1782 return n;
1783 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001784
1785 @Override
1786 protected void onRestoreInstanceState(Parcelable state) {
Adam Powell90f339a2013-06-13 17:44:04 -07001787 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1788 // Some old apps reused IDs in ways they shouldn't have.
1789 // Don't break them, but they don't get scroll state restoration.
1790 super.onRestoreInstanceState(state);
1791 return;
1792 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001793 SavedState ss = (SavedState) state;
1794 super.onRestoreInstanceState(ss.getSuperState());
1795 mSavedState = ss;
1796 requestLayout();
1797 }
1798
1799 @Override
1800 protected Parcelable onSaveInstanceState() {
Adam Powell90f339a2013-06-13 17:44:04 -07001801 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1802 // Some old apps reused IDs in ways they shouldn't have.
1803 // Don't break them, but they don't get scroll state restoration.
1804 return super.onSaveInstanceState();
1805 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001806 Parcelable superState = super.onSaveInstanceState();
1807 SavedState ss = new SavedState(superState);
Adam Powella3aa6d822015-06-02 17:47:50 -07001808 ss.scrollOffsetFromStart = isLayoutRtl() ? -mScrollX : mScrollX;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001809 return ss;
1810 }
1811
Siva Velusamy94a6d152015-05-05 15:07:00 -07001812 /** @hide */
1813 @Override
1814 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
1815 super.encodeProperties(encoder);
1816 encoder.addProperty("layout:fillViewPort", mFillViewport);
1817 }
1818
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001819 static class SavedState extends BaseSavedState {
Adam Powella3aa6d822015-06-02 17:47:50 -07001820 public int scrollOffsetFromStart;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001821
1822 SavedState(Parcelable superState) {
1823 super(superState);
1824 }
1825
1826 public SavedState(Parcel source) {
1827 super(source);
Adam Powella3aa6d822015-06-02 17:47:50 -07001828 scrollOffsetFromStart = source.readInt();
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001829 }
1830
1831 @Override
1832 public void writeToParcel(Parcel dest, int flags) {
1833 super.writeToParcel(dest, flags);
Adam Powella3aa6d822015-06-02 17:47:50 -07001834 dest.writeInt(scrollOffsetFromStart);
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001835 }
1836
1837 @Override
1838 public String toString() {
1839 return "HorizontalScrollView.SavedState{"
1840 + Integer.toHexString(System.identityHashCode(this))
Adam Powella3aa6d822015-06-02 17:47:50 -07001841 + " scrollPosition=" + scrollOffsetFromStart
1842 + "}";
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001843 }
1844
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -07001845 public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001846 = new Parcelable.Creator<SavedState>() {
1847 public SavedState createFromParcel(Parcel in) {
1848 return new SavedState(in);
1849 }
1850
1851 public SavedState[] newArray(int size) {
1852 return new SavedState[size];
1853 }
1854 };
1855 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001856}