blob: 25d4f42970421d75876e52561ca66f83fb940274 [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
Adam Powell637d3372010-08-25 14:37:03 -070019import android.content.Context;
Adam Powell637d3372010-08-25 14:37:03 -070020import android.content.res.TypedArray;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080021import android.graphics.Canvas;
22import android.graphics.Rect;
Adam Powell90f339a2013-06-13 17:44:04 -070023import android.os.Build;
Svetoslav Ganova1dc7612012-05-10 04:14:53 -070024import android.os.Bundle;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -070025import android.os.Parcel;
26import android.os.Parcelable;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080027import android.util.AttributeSet;
Johan Rosengren0dc291e2011-02-21 09:49:45 +010028import android.util.Log;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080029import android.view.FocusFinder;
Jeff Brown33bbfd22011-02-24 20:55:35 -080030import android.view.InputDevice;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080031import android.view.KeyEvent;
32import android.view.MotionEvent;
33import android.view.VelocityTracker;
34import android.view.View;
35import android.view.ViewConfiguration;
36import android.view.ViewDebug;
37import android.view.ViewGroup;
38import android.view.ViewParent;
Svetoslav Ganova0156172011-06-26 17:55:44 -070039import android.view.accessibility.AccessibilityEvent;
40import android.view.accessibility.AccessibilityNodeInfo;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080041import android.view.animation.AnimationUtils;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042
43import java.util.List;
44
45/**
46 * Layout container for a view hierarchy that can be scrolled by the user,
47 * allowing it to be larger than the physical display. A HorizontalScrollView
48 * is a {@link FrameLayout}, meaning you should place one child in it
49 * containing the entire contents to scroll; this child may itself be a layout
50 * manager with a complex hierarchy of objects. A child that is often used
51 * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
52 * array of top-level items that the user can scroll through.
53 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054 * <p>The {@link TextView} class also
Scott Main15279cf2012-07-02 21:49:47 -070055 * takes care of its own scrolling, so does not require a HorizontalScrollView, but
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056 * using the two together is possible to achieve the effect of a text view
57 * within a larger container.
58 *
Scott Main15279cf2012-07-02 21:49:47 -070059 * <p>HorizontalScrollView only supports horizontal scrolling. For vertical scrolling,
60 * use either {@link ScrollView} or {@link ListView}.
Mindy Pereira4e30d892010-11-24 15:32:39 -080061 *
62 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063 */
64public class HorizontalScrollView extends FrameLayout {
65 private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
66
67 private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
68
Johan Rosengren0dc291e2011-02-21 09:49:45 +010069 private static final String TAG = "HorizontalScrollView";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080070
71 private long mLastScroll;
72
73 private final Rect mTempRect = new Rect();
Adam Powell637d3372010-08-25 14:37:03 -070074 private OverScroller mScroller;
Adam Powell89935e42011-08-31 14:26:12 -070075 private EdgeEffect mEdgeGlowLeft;
76 private EdgeEffect mEdgeGlowRight;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080077
78 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080079 * Position of the last motion event.
80 */
Adam Powelldf3ae4f2012-04-10 18:55:22 -070081 private int mLastMotionX;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080082
83 /**
84 * True when the layout has changed but the traversal has not come through yet.
85 * Ideally the view hierarchy would keep track of this for us.
86 */
87 private boolean mIsLayoutDirty = true;
88
89 /**
90 * The child to give focus to in the event that a child has requested focus while the
91 * layout is dirty. This prevents the scroll from being wrong if the child has not been
92 * laid out before requesting focus.
93 */
94 private View mChildToScrollTo = null;
95
96 /**
97 * True if the user is currently dragging this ScrollView around. This is
98 * not the same as 'is being flinged', which can be checked by
99 * mScroller.isFinished() (flinging begins when the user lifts his finger).
100 */
101 private boolean mIsBeingDragged = false;
102
103 /**
104 * Determines speed during touch scrolling
105 */
106 private VelocityTracker mVelocityTracker;
107
108 /**
109 * When set to true, the scroll view measure its child to make it fill the currently
110 * visible area.
111 */
Romain Guya174d7a2011-01-07 13:27:39 -0800112 @ViewDebug.ExportedProperty(category = "layout")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800113 private boolean mFillViewport;
114
115 /**
116 * Whether arrow scrolling is animated.
117 */
118 private boolean mSmoothScrollingEnabled = true;
119
120 private int mTouchSlop;
Romain Guy4296fc42009-07-06 11:48:52 -0700121 private int mMinimumVelocity;
122 private int mMaximumVelocity;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800123
Adam Powell637d3372010-08-25 14:37:03 -0700124 private int mOverscrollDistance;
125 private int mOverflingDistance;
126
Adam Powell4cd47702010-02-25 11:21:14 -0800127 /**
128 * ID of the active pointer. This is used to retain consistency during
129 * drags/flings if multiple pointers are used.
130 */
131 private int mActivePointerId = INVALID_POINTER;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800132
Adam Powell4cd47702010-02-25 11:21:14 -0800133 /**
134 * Sentinel value for no current active pointer.
135 * Used by {@link #mActivePointerId}.
136 */
137 private static final int INVALID_POINTER = -1;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800138
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -0700139 private SavedState mSavedState;
140
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800141 public HorizontalScrollView(Context context) {
142 this(context, null);
143 }
144
145 public HorizontalScrollView(Context context, AttributeSet attrs) {
146 this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
147 }
148
Alan Viverette617feb92013-09-09 18:09:13 -0700149 public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
150 this(context, attrs, defStyleAttr, 0);
151 }
152
153 public HorizontalScrollView(
154 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
155 super(context, attrs, defStyleAttr, defStyleRes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800156 initScrollView();
157
Alan Viverette617feb92013-09-09 18:09:13 -0700158 final TypedArray a = context.obtainStyledAttributes(
159 attrs, android.R.styleable.HorizontalScrollView, defStyleAttr, defStyleRes);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800160
161 setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
162
163 a.recycle();
164 }
165
166 @Override
167 protected float getLeftFadingEdgeStrength() {
168 if (getChildCount() == 0) {
169 return 0.0f;
170 }
171
172 final int length = getHorizontalFadingEdgeLength();
173 if (mScrollX < length) {
174 return mScrollX / (float) length;
175 }
176
177 return 1.0f;
178 }
179
180 @Override
181 protected float getRightFadingEdgeStrength() {
182 if (getChildCount() == 0) {
183 return 0.0f;
184 }
185
186 final int length = getHorizontalFadingEdgeLength();
187 final int rightEdge = getWidth() - mPaddingRight;
188 final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
189 if (span < length) {
190 return span / (float) length;
191 }
192
193 return 1.0f;
194 }
195
196 /**
197 * @return The maximum amount this scroll view will scroll in response to
198 * an arrow event.
199 */
200 public int getMaxScrollAmount() {
201 return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
202 }
203
204
205 private void initScrollView() {
Adam Powell637d3372010-08-25 14:37:03 -0700206 mScroller = new OverScroller(getContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800207 setFocusable(true);
208 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
209 setWillNotDraw(false);
Romain Guy4296fc42009-07-06 11:48:52 -0700210 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
211 mTouchSlop = configuration.getScaledTouchSlop();
212 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
213 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Adam Powell637d3372010-08-25 14:37:03 -0700214 mOverscrollDistance = configuration.getScaledOverscrollDistance();
215 mOverflingDistance = configuration.getScaledOverflingDistance();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800216 }
217
218 @Override
219 public void addView(View child) {
220 if (getChildCount() > 0) {
221 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
222 }
223
224 super.addView(child);
225 }
226
227 @Override
228 public void addView(View child, int index) {
229 if (getChildCount() > 0) {
230 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
231 }
232
233 super.addView(child, index);
234 }
235
236 @Override
237 public void addView(View child, ViewGroup.LayoutParams params) {
238 if (getChildCount() > 0) {
239 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
240 }
241
242 super.addView(child, params);
243 }
244
245 @Override
246 public void addView(View child, int index, ViewGroup.LayoutParams params) {
247 if (getChildCount() > 0) {
248 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
249 }
250
251 super.addView(child, index, params);
252 }
253
254 /**
255 * @return Returns true this HorizontalScrollView can be scrolled
256 */
257 private boolean canScroll() {
258 View child = getChildAt(0);
259 if (child != null) {
260 int childWidth = child.getWidth();
261 return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
262 }
263 return false;
264 }
265
266 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700267 * Indicates whether this HorizontalScrollView's content is stretched to
268 * fill the viewport.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800269 *
270 * @return True if the content fills the viewport, false otherwise.
Romain Guyfdbf4842010-08-16 10:55:49 -0700271 *
272 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800273 */
274 public boolean isFillViewport() {
275 return mFillViewport;
276 }
277
278 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700279 * Indicates this HorizontalScrollView whether it should stretch its content width
280 * to fill the viewport or not.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800281 *
282 * @param fillViewport True to stretch the content's width to the viewport's
283 * boundaries, false otherwise.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800284 *
Romain Guyfdbf4842010-08-16 10:55:49 -0700285 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800286 */
287 public void setFillViewport(boolean fillViewport) {
288 if (fillViewport != mFillViewport) {
289 mFillViewport = fillViewport;
290 requestLayout();
291 }
292 }
293
294 /**
295 * @return Whether arrow scrolling will animate its transition.
296 */
297 public boolean isSmoothScrollingEnabled() {
298 return mSmoothScrollingEnabled;
299 }
300
301 /**
302 * Set whether arrow scrolling will animate its transition.
303 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
304 */
305 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
306 mSmoothScrollingEnabled = smoothScrollingEnabled;
307 }
308
309 @Override
310 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
311 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
312
313 if (!mFillViewport) {
314 return;
315 }
316
317 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
318 if (widthMode == MeasureSpec.UNSPECIFIED) {
319 return;
320 }
321
Romain Guyef0e9ae2009-07-10 14:11:26 -0700322 if (getChildCount() > 0) {
323 final View child = getChildAt(0);
324 int width = getMeasuredWidth();
325 if (child.getMeasuredWidth() < width) {
326 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
Mindy Pereira4e30d892010-11-24 15:32:39 -0800327
Romain Guyef0e9ae2009-07-10 14:11:26 -0700328 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop
329 + mPaddingBottom, lp.height);
330 width -= mPaddingLeft;
331 width -= mPaddingRight;
332 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
Mindy Pereira4e30d892010-11-24 15:32:39 -0800333
Romain Guyef0e9ae2009-07-10 14:11:26 -0700334 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
335 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800336 }
337 }
338
339 @Override
340 public boolean dispatchKeyEvent(KeyEvent event) {
341 // Let the focused view and/or our descendants get the key first
Romain Guy8e618e52010-03-08 12:18:20 -0800342 return super.dispatchKeyEvent(event) || executeKeyEvent(event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800343 }
344
345 /**
346 * You can call this function yourself to have the scroll view perform
347 * scrolling from a key event, just as if the event had been dispatched to
348 * it by the view hierarchy.
349 *
350 * @param event The key event to execute.
351 * @return Return true if the event was handled, else false.
352 */
353 public boolean executeKeyEvent(KeyEvent event) {
354 mTempRect.setEmpty();
355
356 if (!canScroll()) {
357 if (isFocused()) {
358 View currentFocused = findFocus();
359 if (currentFocused == this) currentFocused = null;
360 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
361 currentFocused, View.FOCUS_RIGHT);
362 return nextFocused != null && nextFocused != this &&
363 nextFocused.requestFocus(View.FOCUS_RIGHT);
364 }
365 return false;
366 }
367
368 boolean handled = false;
369 if (event.getAction() == KeyEvent.ACTION_DOWN) {
370 switch (event.getKeyCode()) {
371 case KeyEvent.KEYCODE_DPAD_LEFT:
372 if (!event.isAltPressed()) {
373 handled = arrowScroll(View.FOCUS_LEFT);
374 } else {
375 handled = fullScroll(View.FOCUS_LEFT);
376 }
377 break;
378 case KeyEvent.KEYCODE_DPAD_RIGHT:
379 if (!event.isAltPressed()) {
380 handled = arrowScroll(View.FOCUS_RIGHT);
381 } else {
382 handled = fullScroll(View.FOCUS_RIGHT);
383 }
384 break;
385 case KeyEvent.KEYCODE_SPACE:
386 pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
387 break;
388 }
389 }
390
391 return handled;
392 }
393
Adam Powell4cd47702010-02-25 11:21:14 -0800394 private boolean inChild(int x, int y) {
395 if (getChildCount() > 0) {
Adam Powell352b9782010-03-24 14:23:43 -0700396 final int scrollX = mScrollX;
Adam Powell4cd47702010-02-25 11:21:14 -0800397 final View child = getChildAt(0);
398 return !(y < child.getTop()
399 || y >= child.getBottom()
Adam Powell352b9782010-03-24 14:23:43 -0700400 || x < child.getLeft() - scrollX
401 || x >= child.getRight() - scrollX);
Adam Powell4cd47702010-02-25 11:21:14 -0800402 }
403 return false;
404 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800405
Michael Jurka13451a42011-08-22 15:54:21 -0700406 private void initOrResetVelocityTracker() {
407 if (mVelocityTracker == null) {
408 mVelocityTracker = VelocityTracker.obtain();
409 } else {
410 mVelocityTracker.clear();
411 }
412 }
413
414 private void initVelocityTrackerIfNotExists() {
415 if (mVelocityTracker == null) {
416 mVelocityTracker = VelocityTracker.obtain();
417 }
418 }
419
420 private void recycleVelocityTracker() {
421 if (mVelocityTracker != null) {
422 mVelocityTracker.recycle();
423 mVelocityTracker = null;
424 }
425 }
426
427 @Override
428 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
429 if (disallowIntercept) {
430 recycleVelocityTracker();
431 }
432 super.requestDisallowInterceptTouchEvent(disallowIntercept);
433 }
434
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800435 @Override
436 public boolean onInterceptTouchEvent(MotionEvent ev) {
437 /*
438 * This method JUST determines whether we want to intercept the motion.
439 * If we return true, onMotionEvent will be called and we do the actual
440 * scrolling there.
441 */
442
443 /*
444 * Shortcut the most recurring case: the user is in the dragging
445 * state and he is moving his finger. We want to intercept this
446 * motion.
447 */
448 final int action = ev.getAction();
449 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
450 return true;
451 }
452
Adam Powell4cd47702010-02-25 11:21:14 -0800453 switch (action & MotionEvent.ACTION_MASK) {
454 case MotionEvent.ACTION_MOVE: {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800455 /*
456 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
457 * whether the user has moved far enough from his original down touch.
458 */
459
460 /*
461 * Locally do absolute value. mLastMotionX is set to the x value
462 * of the down event.
463 */
Adam Powell9d0335b2010-03-24 13:42:51 -0700464 final int activePointerId = mActivePointerId;
465 if (activePointerId == INVALID_POINTER) {
466 // If we don't have a valid id, the touch down wasn't on content.
467 break;
468 }
469
470 final int pointerIndex = ev.findPointerIndex(activePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100471 if (pointerIndex == -1) {
472 Log.e(TAG, "Invalid pointerId=" + activePointerId
473 + " in onInterceptTouchEvent");
474 break;
475 }
476
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700477 final int x = (int) ev.getX(pointerIndex);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800478 final int xDiff = (int) Math.abs(x - mLastMotionX);
479 if (xDiff > mTouchSlop) {
480 mIsBeingDragged = true;
Adam Powell4cd47702010-02-25 11:21:14 -0800481 mLastMotionX = x;
Michael Jurka13451a42011-08-22 15:54:21 -0700482 initVelocityTrackerIfNotExists();
483 mVelocityTracker.addMovement(ev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800484 if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
485 }
486 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800487 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488
Adam Powell4cd47702010-02-25 11:21:14 -0800489 case MotionEvent.ACTION_DOWN: {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700490 final int x = (int) ev.getX();
Adam Powell4cd47702010-02-25 11:21:14 -0800491 if (!inChild((int) x, (int) ev.getY())) {
492 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700493 recycleVelocityTracker();
Adam Powell4cd47702010-02-25 11:21:14 -0800494 break;
495 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800496
Adam Powell4cd47702010-02-25 11:21:14 -0800497 /*
498 * Remember location of down touch.
499 * ACTION_DOWN always refers to pointer index 0.
500 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800501 mLastMotionX = x;
Adam Powell4cd47702010-02-25 11:21:14 -0800502 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503
Michael Jurka13451a42011-08-22 15:54:21 -0700504 initOrResetVelocityTracker();
505 mVelocityTracker.addMovement(ev);
506
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800507 /*
508 * If being flinged and user touches the screen, initiate drag;
509 * otherwise don't. mScroller.isFinished should be false when
510 * being flinged.
511 */
512 mIsBeingDragged = !mScroller.isFinished();
513 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800514 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800515
516 case MotionEvent.ACTION_CANCEL:
517 case MotionEvent.ACTION_UP:
518 /* Release the drag */
519 mIsBeingDragged = false;
Adam Powell4cd47702010-02-25 11:21:14 -0800520 mActivePointerId = INVALID_POINTER;
Adam Powell637d3372010-08-25 14:37:03 -0700521 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700522 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700523 }
Adam Powell4cd47702010-02-25 11:21:14 -0800524 break;
Adam Powell9bc30d32011-02-28 10:27:49 -0800525 case MotionEvent.ACTION_POINTER_DOWN: {
526 final int index = ev.getActionIndex();
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700527 mLastMotionX = (int) ev.getX(index);
Adam Powell9bc30d32011-02-28 10:27:49 -0800528 mActivePointerId = ev.getPointerId(index);
529 break;
530 }
Adam Powell4cd47702010-02-25 11:21:14 -0800531 case MotionEvent.ACTION_POINTER_UP:
532 onSecondaryPointerUp(ev);
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700533 mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800534 break;
535 }
536
537 /*
538 * The only time we want to intercept motion events is if we are in the
539 * drag mode.
540 */
541 return mIsBeingDragged;
542 }
543
544 @Override
545 public boolean onTouchEvent(MotionEvent ev) {
Michael Jurka13451a42011-08-22 15:54:21 -0700546 initVelocityTrackerIfNotExists();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800547 mVelocityTracker.addMovement(ev);
548
549 final int action = ev.getAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800550
Adam Powell4cd47702010-02-25 11:21:14 -0800551 switch (action & MotionEvent.ACTION_MASK) {
552 case MotionEvent.ACTION_DOWN: {
Adam Powellb3e02c42012-05-02 22:05:46 -0700553 if (getChildCount() == 0) {
Jeff Brownfb757382011-01-18 18:42:33 -0800554 return false;
555 }
Adam Powellb3e02c42012-05-02 22:05:46 -0700556 if ((mIsBeingDragged = !mScroller.isFinished())) {
557 final ViewParent parent = getParent();
558 if (parent != null) {
559 parent.requestDisallowInterceptTouchEvent(true);
560 }
561 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800562
Adam Powell352b9782010-03-24 14:23:43 -0700563 /*
564 * If being flinged and user touches, stop the fling. isFinished
565 * will be false if being flinged.
566 */
567 if (!mScroller.isFinished()) {
568 mScroller.abortAnimation();
569 }
570
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800571 // Remember where the motion event started
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700572 mLastMotionX = (int) ev.getX();
Adam Powell352b9782010-03-24 14:23:43 -0700573 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800574 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800575 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800576 case MotionEvent.ACTION_MOVE:
Adam Powellb3e02c42012-05-02 22:05:46 -0700577 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100578 if (activePointerIndex == -1) {
579 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
580 break;
581 }
582
Adam Powellb3e02c42012-05-02 22:05:46 -0700583 final int x = (int) ev.getX(activePointerIndex);
584 int deltaX = mLastMotionX - x;
585 if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
586 final ViewParent parent = getParent();
587 if (parent != null) {
588 parent.requestDisallowInterceptTouchEvent(true);
589 }
590 mIsBeingDragged = true;
591 if (deltaX > 0) {
592 deltaX -= mTouchSlop;
593 } else {
594 deltaX += mTouchSlop;
595 }
596 }
Adam Powell4cd47702010-02-25 11:21:14 -0800597 if (mIsBeingDragged) {
598 // Scroll to follow the motion event
Adam Powell4cd47702010-02-25 11:21:14 -0800599 mLastMotionX = x;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800600
Adam Powell637d3372010-08-25 14:37:03 -0700601 final int oldX = mScrollX;
602 final int oldY = mScrollY;
603 final int range = getScrollRange();
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700604 final int overscrollMode = getOverScrollMode();
605 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
606 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
607
Alan Viverettecb25bd82013-06-03 17:10:44 -0700608 // Calling overScrollBy will call onOverScrolled, which
609 // calls onScrollChanged if applicable.
Adam Powellf6a6c972011-09-28 23:30:20 -0700610 if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -0700611 mOverscrollDistance, 0, true)) {
612 // Break our velocity if we hit a scroll barrier.
613 mVelocityTracker.clear();
614 }
Adam Powell637d3372010-08-25 14:37:03 -0700615
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700616 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -0700617 final int pulledToX = oldX + deltaX;
618 if (pulledToX < 0) {
619 mEdgeGlowLeft.onPull((float) deltaX / getWidth());
620 if (!mEdgeGlowRight.isFinished()) {
621 mEdgeGlowRight.onRelease();
622 }
623 } else if (pulledToX > range) {
624 mEdgeGlowRight.onPull((float) deltaX / getWidth());
625 if (!mEdgeGlowLeft.isFinished()) {
626 mEdgeGlowLeft.onRelease();
627 }
628 }
629 if (mEdgeGlowLeft != null
630 && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700631 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700632 }
633 }
Adam Powell4cd47702010-02-25 11:21:14 -0800634 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800635 break;
636 case MotionEvent.ACTION_UP:
Adam Powell4cd47702010-02-25 11:21:14 -0800637 if (mIsBeingDragged) {
638 final VelocityTracker velocityTracker = mVelocityTracker;
639 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
640 int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800641
Adam Powellf6a6c972011-09-28 23:30:20 -0700642 if (getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700643 if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
644 fling(-initialVelocity);
645 } else {
Adam Powellf6a6c972011-09-28 23:30:20 -0700646 if (mScroller.springBack(mScrollX, mScrollY, 0,
647 getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700648 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700649 }
650 }
Adam Powell17dfce12010-01-25 18:38:22 -0800651 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800652
Adam Powell4cd47702010-02-25 11:21:14 -0800653 mActivePointerId = INVALID_POINTER;
654 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700655 recycleVelocityTracker();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800656
Adam Powell637d3372010-08-25 14:37:03 -0700657 if (mEdgeGlowLeft != null) {
658 mEdgeGlowLeft.onRelease();
659 mEdgeGlowRight.onRelease();
660 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800661 }
Adam Powell4cd47702010-02-25 11:21:14 -0800662 break;
Adam Powell352b9782010-03-24 14:23:43 -0700663 case MotionEvent.ACTION_CANCEL:
664 if (mIsBeingDragged && getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700665 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700666 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700667 }
Adam Powell352b9782010-03-24 14:23:43 -0700668 mActivePointerId = INVALID_POINTER;
669 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700670 recycleVelocityTracker();
671
Adam Powell637d3372010-08-25 14:37:03 -0700672 if (mEdgeGlowLeft != null) {
673 mEdgeGlowLeft.onRelease();
674 mEdgeGlowRight.onRelease();
675 }
Adam Powell352b9782010-03-24 14:23:43 -0700676 }
677 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800678 case MotionEvent.ACTION_POINTER_UP:
679 onSecondaryPointerUp(ev);
680 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800681 }
682 return true;
683 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800684
Adam Powell4cd47702010-02-25 11:21:14 -0800685 private void onSecondaryPointerUp(MotionEvent ev) {
686 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
687 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
688 final int pointerId = ev.getPointerId(pointerIndex);
689 if (pointerId == mActivePointerId) {
690 // This was our active pointer going up. Choose a new
691 // active pointer and adjust accordingly.
692 // TODO: Make this decision more intelligent.
693 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700694 mLastMotionX = (int) ev.getX(newPointerIndex);
Adam Powell4cd47702010-02-25 11:21:14 -0800695 mActivePointerId = ev.getPointerId(newPointerIndex);
696 if (mVelocityTracker != null) {
697 mVelocityTracker.clear();
698 }
699 }
700 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800701
Adam Powell637d3372010-08-25 14:37:03 -0700702 @Override
Jeff Brown33bbfd22011-02-24 20:55:35 -0800703 public boolean onGenericMotionEvent(MotionEvent event) {
704 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
705 switch (event.getAction()) {
706 case MotionEvent.ACTION_SCROLL: {
707 if (!mIsBeingDragged) {
708 final float hscroll;
709 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
710 hscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
711 } else {
712 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
713 }
714 if (hscroll != 0) {
715 final int delta = (int) (hscroll * getHorizontalScrollFactor());
716 final int range = getScrollRange();
717 int oldScrollX = mScrollX;
718 int newScrollX = oldScrollX + delta;
719 if (newScrollX < 0) {
720 newScrollX = 0;
721 } else if (newScrollX > range) {
722 newScrollX = range;
723 }
724 if (newScrollX != oldScrollX) {
725 super.scrollTo(newScrollX, mScrollY);
726 return true;
727 }
728 }
729 }
730 }
731 }
732 }
733 return super.onGenericMotionEvent(event);
734 }
735
736 @Override
Michael Jurka9edd58e2011-10-28 16:39:18 -0700737 public boolean shouldDelayChildPressedState() {
738 return true;
739 }
740
741 @Override
Adam Powell637d3372010-08-25 14:37:03 -0700742 protected void onOverScrolled(int scrollX, int scrollY,
743 boolean clampedX, boolean clampedY) {
744 // Treat animating scrolls differently; see #computeScroll() for why.
745 if (!mScroller.isFinished()) {
Alan Viverettecb25bd82013-06-03 17:10:44 -0700746 final int oldX = mScrollX;
747 final int oldY = mScrollY;
Adam Powell637d3372010-08-25 14:37:03 -0700748 mScrollX = scrollX;
749 mScrollY = scrollY;
Romain Guy0fd89bf2011-01-26 15:41:30 -0800750 invalidateParentIfNeeded();
Alan Viverettecb25bd82013-06-03 17:10:44 -0700751 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
Adam Powell637d3372010-08-25 14:37:03 -0700752 if (clampedX) {
753 mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
754 }
755 } else {
756 super.scrollTo(scrollX, scrollY);
757 }
Romain Guye979e622012-03-20 13:50:27 -0700758
Romain Guye72cf732012-03-20 14:23:09 -0700759 awakenScrollBars();
Adam Powell637d3372010-08-25 14:37:03 -0700760 }
761
Svetoslav Ganova0156172011-06-26 17:55:44 -0700762 @Override
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700763 public boolean performAccessibilityAction(int action, Bundle arguments) {
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700764 if (super.performAccessibilityAction(action, arguments)) {
765 return true;
766 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700767 switch (action) {
768 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700769 if (!isEnabled()) {
770 return false;
771 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700772 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
773 final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
774 if (targetScrollX != mScrollX) {
775 smoothScrollTo(targetScrollX, 0);
776 return true;
777 }
778 } return false;
779 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700780 if (!isEnabled()) {
781 return false;
782 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700783 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
784 final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
785 if (targetScrollX != mScrollX) {
786 smoothScrollTo(targetScrollX, 0);
787 return true;
788 }
789 } return false;
790 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700791 return false;
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700792 }
793
794 @Override
Svetoslav Ganova0156172011-06-26 17:55:44 -0700795 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
796 super.onInitializeAccessibilityNodeInfo(info);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800797 info.setClassName(HorizontalScrollView.class.getName());
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700798 final int scrollRange = getScrollRange();
799 if (scrollRange > 0) {
800 info.setScrollable(true);
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700801 if (isEnabled() && mScrollX > 0) {
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700802 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
803 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700804 if (isEnabled() && mScrollX < scrollRange) {
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700805 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
806 }
807 }
Svetoslav Ganova0156172011-06-26 17:55:44 -0700808 }
809
810 @Override
811 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
812 super.onInitializeAccessibilityEvent(event);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800813 event.setClassName(HorizontalScrollView.class.getName());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700814 event.setScrollable(getScrollRange() > 0);
815 event.setScrollX(mScrollX);
816 event.setScrollY(mScrollY);
817 event.setMaxScrollX(getScrollRange());
818 event.setMaxScrollY(mScrollY);
Svetoslav Ganova0156172011-06-26 17:55:44 -0700819 }
820
Adam Powell0b8bb422010-02-08 14:30:45 -0800821 private int getScrollRange() {
822 int scrollRange = 0;
823 if (getChildCount() > 0) {
824 View child = getChildAt(0);
825 scrollRange = Math.max(0,
Adam Powell637d3372010-08-25 14:37:03 -0700826 child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
Adam Powell0b8bb422010-02-08 14:30:45 -0800827 }
828 return scrollRange;
829 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800830
831 /**
832 * <p>
833 * Finds the next focusable component that fits in this View's bounds
834 * (excluding fading edges) pretending that this View's left is located at
835 * the parameter left.
836 * </p>
837 *
838 * @param leftFocus look for a candidate is the one at the left of the bounds
839 * if leftFocus is true, or at the right of the bounds if leftFocus
840 * is false
841 * @param left the left offset of the bounds in which a focusable must be
842 * found (the fading edge is assumed to start at this position)
843 * @param preferredFocusable the View that has highest priority and will be
844 * returned if it is within my bounds (null is valid)
845 * @return the next focusable component in the bounds or null if none can be found
846 */
847 private View findFocusableViewInMyBounds(final boolean leftFocus,
848 final int left, View preferredFocusable) {
849 /*
850 * The fading edge's transparent side should be considered for focus
851 * since it's mostly visible, so we divide the actual fading edge length
852 * by 2.
853 */
854 final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
855 final int leftWithoutFadingEdge = left + fadingEdgeLength;
856 final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
857
858 if ((preferredFocusable != null)
859 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
860 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
861 return preferredFocusable;
862 }
863
864 return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
865 rightWithoutFadingEdge);
866 }
867
868 /**
869 * <p>
870 * Finds the next focusable component that fits in the specified bounds.
871 * </p>
872 *
873 * @param leftFocus look for a candidate is the one at the left of the bounds
874 * if leftFocus is true, or at the right of the bounds if
875 * leftFocus is false
876 * @param left the left offset of the bounds in which a focusable must be
877 * found
878 * @param right the right offset of the bounds in which a focusable must
879 * be found
880 * @return the next focusable component in the bounds or null if none can
881 * be found
882 */
883 private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
884
885 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
886 View focusCandidate = null;
887
888 /*
889 * A fully contained focusable is one where its left is below the bound's
890 * left, and its right is above the bound's right. A partially
891 * contained focusable is one where some part of it is within the
892 * bounds, but it also has some part that is not within bounds. A fully contained
893 * focusable is preferred to a partially contained focusable.
894 */
895 boolean foundFullyContainedFocusable = false;
896
897 int count = focusables.size();
898 for (int i = 0; i < count; i++) {
899 View view = focusables.get(i);
900 int viewLeft = view.getLeft();
901 int viewRight = view.getRight();
902
903 if (left < viewRight && viewLeft < right) {
904 /*
905 * the focusable is in the target area, it is a candidate for
906 * focusing
907 */
908
909 final boolean viewIsFullyContained = (left < viewLeft) &&
910 (viewRight < right);
911
912 if (focusCandidate == null) {
913 /* No candidate, take this one */
914 focusCandidate = view;
915 foundFullyContainedFocusable = viewIsFullyContained;
916 } else {
917 final boolean viewIsCloserToBoundary =
918 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
919 (!leftFocus && viewRight > focusCandidate.getRight());
920
921 if (foundFullyContainedFocusable) {
922 if (viewIsFullyContained && viewIsCloserToBoundary) {
923 /*
924 * We're dealing with only fully contained views, so
925 * it has to be closer to the boundary to beat our
926 * candidate
927 */
928 focusCandidate = view;
929 }
930 } else {
931 if (viewIsFullyContained) {
932 /* Any fully contained view beats a partially contained view */
933 focusCandidate = view;
934 foundFullyContainedFocusable = true;
935 } else if (viewIsCloserToBoundary) {
936 /*
937 * Partially contained view beats another partially
938 * contained view if it's closer
939 */
940 focusCandidate = view;
941 }
942 }
943 }
944 }
945 }
946
947 return focusCandidate;
948 }
949
950 /**
951 * <p>Handles scrolling in response to a "page up/down" shortcut press. This
952 * method will scroll the view by one page left or right and give the focus
953 * to the leftmost/rightmost component in the new visible area. If no
954 * component is a good candidate for focus, this scrollview reclaims the
955 * focus.</p>
956 *
957 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
958 * to go one page left or {@link android.view.View#FOCUS_RIGHT}
959 * to go one page right
960 * @return true if the key event is consumed by this method, false otherwise
961 */
962 public boolean pageScroll(int direction) {
963 boolean right = direction == View.FOCUS_RIGHT;
964 int width = getWidth();
965
966 if (right) {
967 mTempRect.left = getScrollX() + width;
968 int count = getChildCount();
969 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -0700970 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800971 if (mTempRect.left + width > view.getRight()) {
972 mTempRect.left = view.getRight() - width;
973 }
974 }
975 } else {
976 mTempRect.left = getScrollX() - width;
977 if (mTempRect.left < 0) {
978 mTempRect.left = 0;
979 }
980 }
981 mTempRect.right = mTempRect.left + width;
982
983 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
984 }
985
986 /**
987 * <p>Handles scrolling in response to a "home/end" shortcut press. This
988 * method will scroll the view to the left or right and give the focus
989 * to the leftmost/rightmost component in the new visible area. If no
990 * component is a good candidate for focus, this scrollview reclaims the
991 * focus.</p>
992 *
993 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
994 * to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
995 * to go the right
996 * @return true if the key event is consumed by this method, false otherwise
997 */
998 public boolean fullScroll(int direction) {
999 boolean right = direction == View.FOCUS_RIGHT;
1000 int width = getWidth();
1001
1002 mTempRect.left = 0;
1003 mTempRect.right = width;
1004
1005 if (right) {
1006 int count = getChildCount();
1007 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001008 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001009 mTempRect.right = view.getRight();
1010 mTempRect.left = mTempRect.right - width;
1011 }
1012 }
1013
1014 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
1015 }
1016
1017 /**
1018 * <p>Scrolls the view to make the area defined by <code>left</code> and
1019 * <code>right</code> visible. This method attempts to give the focus
1020 * to a component visible in this area. If no component can be focused in
1021 * the new visible area, the focus is reclaimed by this scrollview.</p>
1022 *
1023 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1024 * to go left {@link android.view.View#FOCUS_RIGHT} to right
1025 * @param left the left offset of the new area to be made visible
1026 * @param right the right offset of the new area to be made visible
1027 * @return true if the key event is consumed by this method, false otherwise
1028 */
1029 private boolean scrollAndFocus(int direction, int left, int right) {
1030 boolean handled = true;
1031
1032 int width = getWidth();
1033 int containerLeft = getScrollX();
1034 int containerRight = containerLeft + width;
1035 boolean goLeft = direction == View.FOCUS_LEFT;
1036
1037 View newFocused = findFocusableViewInBounds(goLeft, left, right);
1038 if (newFocused == null) {
1039 newFocused = this;
1040 }
1041
1042 if (left >= containerLeft && right <= containerRight) {
1043 handled = false;
1044 } else {
1045 int delta = goLeft ? (left - containerLeft) : (right - containerRight);
1046 doScrollX(delta);
1047 }
1048
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001049 if (newFocused != findFocus()) newFocused.requestFocus(direction);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001050
1051 return handled;
1052 }
1053
1054 /**
1055 * Handle scrolling in response to a left or right arrow click.
1056 *
1057 * @param direction The direction corresponding to the arrow key that was
1058 * pressed
1059 * @return True if we consumed the event, false otherwise
1060 */
1061 public boolean arrowScroll(int direction) {
1062
1063 View currentFocused = findFocus();
1064 if (currentFocused == this) currentFocused = null;
1065
1066 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1067
1068 final int maxJump = getMaxScrollAmount();
1069
1070 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
1071 nextFocused.getDrawingRect(mTempRect);
1072 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1073 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1074 doScrollX(scrollDelta);
1075 nextFocused.requestFocus(direction);
1076 } else {
1077 // no new focus
1078 int scrollDelta = maxJump;
1079
1080 if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
1081 scrollDelta = getScrollX();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001082 } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001083
Romain Guyef0e9ae2009-07-10 14:11:26 -07001084 int daRight = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001085
1086 int screenRight = getScrollX() + getWidth();
1087
1088 if (daRight - screenRight < maxJump) {
1089 scrollDelta = daRight - screenRight;
1090 }
1091 }
1092 if (scrollDelta == 0) {
1093 return false;
1094 }
1095 doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
1096 }
1097
1098 if (currentFocused != null && currentFocused.isFocused()
1099 && isOffScreen(currentFocused)) {
1100 // previously focused item still has focus and is off screen, give
1101 // it up (take it back to ourselves)
1102 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1103 // sure to
1104 // get it)
1105 final int descendantFocusability = getDescendantFocusability(); // save
1106 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1107 requestFocus();
1108 setDescendantFocusability(descendantFocusability); // restore
1109 }
1110 return true;
1111 }
1112
1113 /**
1114 * @return whether the descendant of this scroll view is scrolled off
1115 * screen.
1116 */
1117 private boolean isOffScreen(View descendant) {
1118 return !isWithinDeltaOfScreen(descendant, 0);
1119 }
1120
1121 /**
1122 * @return whether the descendant of this scroll view is within delta
1123 * pixels of being on the screen.
1124 */
1125 private boolean isWithinDeltaOfScreen(View descendant, int delta) {
1126 descendant.getDrawingRect(mTempRect);
1127 offsetDescendantRectToMyCoords(descendant, mTempRect);
1128
1129 return (mTempRect.right + delta) >= getScrollX()
1130 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
1131 }
1132
1133 /**
1134 * Smooth scroll by a X delta
1135 *
1136 * @param delta the number of pixels to scroll by on the X axis
1137 */
1138 private void doScrollX(int delta) {
1139 if (delta != 0) {
1140 if (mSmoothScrollingEnabled) {
1141 smoothScrollBy(delta, 0);
1142 } else {
1143 scrollBy(delta, 0);
1144 }
1145 }
1146 }
1147
1148 /**
1149 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1150 *
1151 * @param dx the number of pixels to scroll by on the X axis
1152 * @param dy the number of pixels to scroll by on the Y axis
1153 */
1154 public final void smoothScrollBy(int dx, int dy) {
Adam Powell3fc37372010-01-28 13:52:24 -08001155 if (getChildCount() == 0) {
1156 // Nothing to do.
1157 return;
1158 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001159 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1160 if (duration > ANIMATED_SCROLL_GAP) {
Adam Powellf5446052010-01-28 17:24:56 -08001161 final int width = getWidth() - mPaddingRight - mPaddingLeft;
1162 final int right = getChildAt(0).getWidth();
1163 final int maxX = Math.max(0, right - width);
1164 final int scrollX = mScrollX;
1165 dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
1166
1167 mScroller.startScroll(scrollX, mScrollY, dx, 0);
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001168 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001169 } else {
1170 if (!mScroller.isFinished()) {
1171 mScroller.abortAnimation();
1172 }
1173 scrollBy(dx, dy);
1174 }
1175 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1176 }
1177
1178 /**
1179 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1180 *
1181 * @param x the position where to scroll on the X axis
1182 * @param y the position where to scroll on the Y axis
1183 */
1184 public final void smoothScrollTo(int x, int y) {
1185 smoothScrollBy(x - mScrollX, y - mScrollY);
1186 }
1187
1188 /**
1189 * <p>The scroll range of a scroll view is the overall width of all of its
1190 * children.</p>
1191 */
1192 @Override
1193 protected int computeHorizontalScrollRange() {
Adam Powella2f91012010-02-16 12:26:33 -08001194 final int count = getChildCount();
1195 final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
Adam Powell0b8bb422010-02-08 14:30:45 -08001196 if (count == 0) {
Adam Powella2f91012010-02-16 12:26:33 -08001197 return contentWidth;
Adam Powell0b8bb422010-02-08 14:30:45 -08001198 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001199
Adam Powell637d3372010-08-25 14:37:03 -07001200 int scrollRange = getChildAt(0).getRight();
1201 final int scrollX = mScrollX;
1202 final int overscrollRight = Math.max(0, scrollRange - contentWidth);
1203 if (scrollX < 0) {
1204 scrollRange -= scrollX;
1205 } else if (scrollX > overscrollRight) {
1206 scrollRange += scrollX - overscrollRight;
1207 }
1208
1209 return scrollRange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001210 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001211
Adam Powell0b8bb422010-02-08 14:30:45 -08001212 @Override
1213 protected int computeHorizontalScrollOffset() {
1214 return Math.max(0, super.computeHorizontalScrollOffset());
1215 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001216
1217 @Override
1218 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
1219 ViewGroup.LayoutParams lp = child.getLayoutParams();
1220
1221 int childWidthMeasureSpec;
1222 int childHeightMeasureSpec;
1223
1224 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
1225 + mPaddingBottom, lp.height);
1226
1227 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1228
1229 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1230 }
1231
1232 @Override
1233 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1234 int parentHeightMeasureSpec, int heightUsed) {
1235 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1236
1237 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1238 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
1239 + heightUsed, lp.height);
1240 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1241 lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
1242
1243 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1244 }
1245
1246 @Override
1247 public void computeScroll() {
1248 if (mScroller.computeScrollOffset()) {
1249 // This is called at drawing time by ViewGroup. We don't want to
1250 // re-show the scrollbars at this point, which scrollTo will do,
1251 // so we replicate most of scrollTo here.
1252 //
1253 // It's a little odd to call onScrollChanged from inside the drawing.
1254 //
1255 // It is, except when you remember that computeScroll() is used to
1256 // animate scrolling. So unless we want to defer the onScrollChanged()
1257 // until the end of the animated scrolling, we don't really have a
1258 // choice here.
1259 //
1260 // I agree. The alternative, which I think would be worse, is to post
1261 // something and tell the subclasses later. This is bad because there
1262 // will be a window where mScrollX/Y is different from what the app
1263 // thinks it is.
1264 //
1265 int oldX = mScrollX;
1266 int oldY = mScrollY;
1267 int x = mScroller.getCurrX();
1268 int y = mScroller.getCurrY();
Adam Powell17dfce12010-01-25 18:38:22 -08001269
Adam Powell637d3372010-08-25 14:37:03 -07001270 if (oldX != x || oldY != y) {
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001271 final int range = getScrollRange();
1272 final int overscrollMode = getOverScrollMode();
1273 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
1274 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
1275
1276 overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001277 mOverflingDistance, 0, false);
1278 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1279
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001280 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -07001281 if (x < 0 && oldX >= 0) {
1282 mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
1283 } else if (x > range && oldX <= range) {
1284 mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
1285 }
Adam Powell9d32d242010-03-29 16:02:07 -07001286 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001287 }
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001288
Romain Guye979e622012-03-20 13:50:27 -07001289 if (!awakenScrollBars()) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001290 postInvalidateOnAnimation();
Romain Guye979e622012-03-20 13:50:27 -07001291 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001292 }
1293 }
1294
1295 /**
1296 * Scrolls the view to the given child.
1297 *
1298 * @param child the View to scroll to
1299 */
1300 private void scrollToChild(View child) {
1301 child.getDrawingRect(mTempRect);
1302
1303 /* Offset from child's local coordinates to ScrollView coordinates */
1304 offsetDescendantRectToMyCoords(child, mTempRect);
1305
1306 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1307
1308 if (scrollDelta != 0) {
1309 scrollBy(scrollDelta, 0);
1310 }
1311 }
1312
1313 /**
1314 * If rect is off screen, scroll just enough to get it (or at least the
1315 * first screen size chunk of it) on screen.
1316 *
1317 * @param rect The rectangle.
1318 * @param immediate True to scroll immediately without animation
1319 * @return true if scrolling was performed
1320 */
1321 private boolean scrollToChildRect(Rect rect, boolean immediate) {
1322 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1323 final boolean scroll = delta != 0;
1324 if (scroll) {
1325 if (immediate) {
1326 scrollBy(delta, 0);
1327 } else {
1328 smoothScrollBy(delta, 0);
1329 }
1330 }
1331 return scroll;
1332 }
1333
1334 /**
1335 * Compute the amount to scroll in the X direction in order to get
1336 * a rectangle completely on the screen (or, if taller than the screen,
1337 * at least the first screen size chunk of it).
1338 *
1339 * @param rect The rect.
1340 * @return The scroll delta.
1341 */
1342 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001343 if (getChildCount() == 0) return 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001344
1345 int width = getWidth();
1346 int screenLeft = getScrollX();
1347 int screenRight = screenLeft + width;
1348
1349 int fadingEdge = getHorizontalFadingEdgeLength();
1350
1351 // leave room for left fading edge as long as rect isn't at very left
1352 if (rect.left > 0) {
1353 screenLeft += fadingEdge;
1354 }
1355
1356 // leave room for right fading edge as long as rect isn't at very right
1357 if (rect.right < getChildAt(0).getWidth()) {
1358 screenRight -= fadingEdge;
1359 }
1360
1361 int scrollXDelta = 0;
1362
1363 if (rect.right > screenRight && rect.left > screenLeft) {
1364 // need to move right to get it in view: move right just enough so
1365 // that the entire rectangle is in view (or at least the first
1366 // screen size chunk).
1367
1368 if (rect.width() > width) {
1369 // just enough to get screen size chunk on
1370 scrollXDelta += (rect.left - screenLeft);
1371 } else {
1372 // get entire rect at right of screen
1373 scrollXDelta += (rect.right - screenRight);
1374 }
1375
1376 // make sure we aren't scrolling beyond the end of our content
Romain Guyef0e9ae2009-07-10 14:11:26 -07001377 int right = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001378 int distanceToRight = right - screenRight;
1379 scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1380
1381 } else if (rect.left < screenLeft && rect.right < screenRight) {
1382 // need to move right to get it in view: move right just enough so that
1383 // entire rectangle is in view (or at least the first screen
1384 // size chunk of it).
1385
1386 if (rect.width() > width) {
1387 // screen size chunk
1388 scrollXDelta -= (screenRight - rect.right);
1389 } else {
1390 // entire rect at left
1391 scrollXDelta -= (screenLeft - rect.left);
1392 }
1393
1394 // make sure we aren't scrolling any further than the left our content
1395 scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1396 }
1397 return scrollXDelta;
1398 }
1399
1400 @Override
1401 public void requestChildFocus(View child, View focused) {
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001402 if (!mIsLayoutDirty) {
1403 scrollToChild(focused);
1404 } else {
1405 // The child may not be laid out yet, we can't compute the scroll yet
1406 mChildToScrollTo = focused;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001407 }
1408 super.requestChildFocus(child, focused);
1409 }
1410
1411
1412 /**
1413 * When looking for focus in children of a scroll view, need to be a little
1414 * more careful not to give focus to something that is scrolled off screen.
1415 *
1416 * This is more expensive than the default {@link android.view.ViewGroup}
1417 * implementation, otherwise this behavior might have been made the default.
1418 */
1419 @Override
1420 protected boolean onRequestFocusInDescendants(int direction,
1421 Rect previouslyFocusedRect) {
1422
1423 // convert from forward / backward notation to up / down / left / right
1424 // (ugh).
1425 if (direction == View.FOCUS_FORWARD) {
1426 direction = View.FOCUS_RIGHT;
1427 } else if (direction == View.FOCUS_BACKWARD) {
1428 direction = View.FOCUS_LEFT;
1429 }
1430
1431 final View nextFocus = previouslyFocusedRect == null ?
1432 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1433 FocusFinder.getInstance().findNextFocusFromRect(this,
1434 previouslyFocusedRect, direction);
1435
1436 if (nextFocus == null) {
1437 return false;
1438 }
1439
1440 if (isOffScreen(nextFocus)) {
1441 return false;
1442 }
1443
1444 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1445 }
1446
1447 @Override
1448 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1449 boolean immediate) {
1450 // offset into coordinate space of this scroll view
1451 rectangle.offset(child.getLeft() - child.getScrollX(),
1452 child.getTop() - child.getScrollY());
1453
1454 return scrollToChildRect(rectangle, immediate);
1455 }
1456
1457 @Override
1458 public void requestLayout() {
1459 mIsLayoutDirty = true;
1460 super.requestLayout();
1461 }
1462
1463 @Override
1464 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001465 int childWidth = 0;
1466 int childMargins = 0;
1467
1468 if (getChildCount() > 0) {
1469 childWidth = getChildAt(0).getMeasuredWidth();
1470 LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams();
1471 childMargins = childParams.leftMargin + childParams.rightMargin;
1472 }
1473
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001474 final int available = r - l - getPaddingLeftWithForeground() -
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001475 getPaddingRightWithForeground() - childMargins;
1476
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001477 final boolean forceLeftGravity = (childWidth > available);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001478
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001479 layoutChildren(l, t, r, b, forceLeftGravity);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001480
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001481 mIsLayoutDirty = false;
1482 // Give a child focus if it needs it
1483 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
Adam Powell90f339a2013-06-13 17:44:04 -07001484 scrollToChild(mChildToScrollTo);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001485 }
1486 mChildToScrollTo = null;
1487
Chet Haase7a46dde2013-07-17 10:22:53 -07001488 if (!isLaidOut()) {
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001489 final int scrollRange = Math.max(0,
1490 childWidth - (r - l - mPaddingLeft - mPaddingRight));
1491 if (mSavedState != null) {
1492 if (isLayoutRtl() == mSavedState.isLayoutRtl) {
1493 mScrollX = mSavedState.scrollPosition;
1494 } else {
1495 mScrollX = scrollRange - mSavedState.scrollPosition;
1496 }
Fabrice Di Megliofafe88c2013-06-12 18:18:08 -07001497 mSavedState = null;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001498 } else {
1499 if (isLayoutRtl()) {
1500 mScrollX = scrollRange - mScrollX;
1501 } // mScrollX default value is "0" for LTR
1502 }
1503 // Don't forget to clamp
1504 if (mScrollX > scrollRange) {
1505 mScrollX = scrollRange;
1506 } else if (mScrollX < 0) {
1507 mScrollX = 0;
1508 }
1509 }
1510
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001511 // Calling this with the present values causes it to re-claim them
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001512 scrollTo(mScrollX, mScrollY);
1513 }
1514
1515 @Override
1516 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1517 super.onSizeChanged(w, h, oldw, oldh);
1518
1519 View currentFocused = findFocus();
1520 if (null == currentFocused || this == currentFocused)
1521 return;
1522
1523 final int maxJump = mRight - mLeft;
1524
1525 if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
1526 currentFocused.getDrawingRect(mTempRect);
1527 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1528 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1529 doScrollX(scrollDelta);
1530 }
1531 }
1532
1533 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001534 * Return true if child is a descendant of parent, (or equal to the parent).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001535 */
Romain Guye979e622012-03-20 13:50:27 -07001536 private static boolean isViewDescendantOf(View child, View parent) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001537 if (child == parent) {
1538 return true;
1539 }
1540
1541 final ViewParent theParent = child.getParent();
1542 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1543 }
1544
1545 /**
1546 * Fling the scroll view
1547 *
1548 * @param velocityX The initial velocity in the X direction. Positive
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001549 * numbers mean that the finger/cursor is moving down the screen,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001550 * which means we want to scroll towards the left.
1551 */
1552 public void fling(int velocityX) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001553 if (getChildCount() > 0) {
1554 int width = getWidth() - mPaddingRight - mPaddingLeft;
1555 int right = getChildAt(0).getWidth();
Mindy Pereira4e30d892010-11-24 15:32:39 -08001556
1557 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001558 Math.max(0, right - width), 0, 0, width/2, 0);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001559
Romain Guyef0e9ae2009-07-10 14:11:26 -07001560 final boolean movingRight = velocityX > 0;
Mindy Pereira4e30d892010-11-24 15:32:39 -08001561
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001562 View currentFocused = findFocus();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001563 View newFocused = findFocusableViewInMyBounds(movingRight,
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001564 mScroller.getFinalX(), currentFocused);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001565
Romain Guyef0e9ae2009-07-10 14:11:26 -07001566 if (newFocused == null) {
1567 newFocused = this;
1568 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001569
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001570 if (newFocused != currentFocused) {
1571 newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
Romain Guyef0e9ae2009-07-10 14:11:26 -07001572 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001573
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001574 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001575 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001576 }
1577
1578 /**
1579 * {@inheritDoc}
1580 *
1581 * <p>This version also clamps the scrolling to the bounds of our child.
1582 */
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001583 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001584 public void scrollTo(int x, int y) {
1585 // we rely on the fact the View.scrollBy calls scrollTo.
1586 if (getChildCount() > 0) {
1587 View child = getChildAt(0);
1588 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1589 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1590 if (x != mScrollX || y != mScrollY) {
1591 super.scrollTo(x, y);
1592 }
1593 }
1594 }
1595
Adam Powell637d3372010-08-25 14:37:03 -07001596 @Override
1597 public void setOverScrollMode(int mode) {
1598 if (mode != OVER_SCROLL_NEVER) {
1599 if (mEdgeGlowLeft == null) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001600 Context context = getContext();
Adam Powell89935e42011-08-31 14:26:12 -07001601 mEdgeGlowLeft = new EdgeEffect(context);
1602 mEdgeGlowRight = new EdgeEffect(context);
Adam Powell637d3372010-08-25 14:37:03 -07001603 }
1604 } else {
1605 mEdgeGlowLeft = null;
1606 mEdgeGlowRight = null;
1607 }
1608 super.setOverScrollMode(mode);
1609 }
1610
Romain Guy2243e552011-03-08 11:46:28 -08001611 @SuppressWarnings({"SuspiciousNameCombination"})
Adam Powell637d3372010-08-25 14:37:03 -07001612 @Override
1613 public void draw(Canvas canvas) {
1614 super.draw(canvas);
1615 if (mEdgeGlowLeft != null) {
1616 final int scrollX = mScrollX;
1617 if (!mEdgeGlowLeft.isFinished()) {
1618 final int restoreCount = canvas.save();
Adam Powell7d863782011-02-15 15:05:03 -08001619 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001620
1621 canvas.rotate(270);
Adam Powell7d863782011-02-15 15:05:03 -08001622 canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
1623 mEdgeGlowLeft.setSize(height, getWidth());
Adam Powell637d3372010-08-25 14:37:03 -07001624 if (mEdgeGlowLeft.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001625 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001626 }
1627 canvas.restoreToCount(restoreCount);
1628 }
1629 if (!mEdgeGlowRight.isFinished()) {
1630 final int restoreCount = canvas.save();
1631 final int width = getWidth();
Adam Powell7d863782011-02-15 15:05:03 -08001632 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001633
1634 canvas.rotate(90);
Adam Powell7d863782011-02-15 15:05:03 -08001635 canvas.translate(-mPaddingTop,
Mindy Pereirab1297f72010-12-07 15:06:47 -08001636 -(Math.max(getScrollRange(), scrollX) + width));
1637 mEdgeGlowRight.setSize(height, width);
Adam Powell637d3372010-08-25 14:37:03 -07001638 if (mEdgeGlowRight.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001639 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001640 }
1641 canvas.restoreToCount(restoreCount);
1642 }
1643 }
1644 }
1645
Romain Guye979e622012-03-20 13:50:27 -07001646 private static int clamp(int n, int my, int child) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001647 if (my >= child || n < 0) {
1648 return 0;
1649 }
1650 if ((my + n) > child) {
1651 return child - my;
1652 }
1653 return n;
1654 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001655
1656 @Override
1657 protected void onRestoreInstanceState(Parcelable state) {
Adam Powell90f339a2013-06-13 17:44:04 -07001658 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1659 // Some old apps reused IDs in ways they shouldn't have.
1660 // Don't break them, but they don't get scroll state restoration.
1661 super.onRestoreInstanceState(state);
1662 return;
1663 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001664 SavedState ss = (SavedState) state;
1665 super.onRestoreInstanceState(ss.getSuperState());
1666 mSavedState = ss;
1667 requestLayout();
1668 }
1669
1670 @Override
1671 protected Parcelable onSaveInstanceState() {
Adam Powell90f339a2013-06-13 17:44:04 -07001672 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1673 // Some old apps reused IDs in ways they shouldn't have.
1674 // Don't break them, but they don't get scroll state restoration.
1675 return super.onSaveInstanceState();
1676 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001677 Parcelable superState = super.onSaveInstanceState();
1678 SavedState ss = new SavedState(superState);
1679 ss.scrollPosition = mScrollX;
1680 ss.isLayoutRtl = isLayoutRtl();
1681 return ss;
1682 }
1683
1684 static class SavedState extends BaseSavedState {
1685 public int scrollPosition;
1686 public boolean isLayoutRtl;
1687
1688 SavedState(Parcelable superState) {
1689 super(superState);
1690 }
1691
1692 public SavedState(Parcel source) {
1693 super(source);
1694 scrollPosition = source.readInt();
1695 isLayoutRtl = (source.readInt() == 0) ? true : false;
1696 }
1697
1698 @Override
1699 public void writeToParcel(Parcel dest, int flags) {
1700 super.writeToParcel(dest, flags);
1701 dest.writeInt(scrollPosition);
1702 dest.writeInt(isLayoutRtl ? 1 : 0);
1703 }
1704
1705 @Override
1706 public String toString() {
1707 return "HorizontalScrollView.SavedState{"
1708 + Integer.toHexString(System.identityHashCode(this))
1709 + " scrollPosition=" + scrollPosition
1710 + " isLayoutRtl=" + isLayoutRtl + "}";
1711 }
1712
1713 public static final Parcelable.Creator<SavedState> CREATOR
1714 = new Parcelable.Creator<SavedState>() {
1715 public SavedState createFromParcel(Parcel in) {
1716 return new SavedState(in);
1717 }
1718
1719 public SavedState[] newArray(int size) {
1720 return new SavedState[size];
1721 }
1722 };
1723 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001724}