blob: dab0962f969404608cd85535d03f2881c27bf058 [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
149 public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
150 super(context, attrs, defStyle);
151 initScrollView();
152
153 TypedArray a = context.obtainStyledAttributes(attrs,
154 android.R.styleable.HorizontalScrollView, defStyle, 0);
155
156 setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
157
158 a.recycle();
159 }
160
161 @Override
162 protected float getLeftFadingEdgeStrength() {
163 if (getChildCount() == 0) {
164 return 0.0f;
165 }
166
167 final int length = getHorizontalFadingEdgeLength();
168 if (mScrollX < length) {
169 return mScrollX / (float) length;
170 }
171
172 return 1.0f;
173 }
174
175 @Override
176 protected float getRightFadingEdgeStrength() {
177 if (getChildCount() == 0) {
178 return 0.0f;
179 }
180
181 final int length = getHorizontalFadingEdgeLength();
182 final int rightEdge = getWidth() - mPaddingRight;
183 final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
184 if (span < length) {
185 return span / (float) length;
186 }
187
188 return 1.0f;
189 }
190
191 /**
192 * @return The maximum amount this scroll view will scroll in response to
193 * an arrow event.
194 */
195 public int getMaxScrollAmount() {
196 return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
197 }
198
199
200 private void initScrollView() {
Adam Powell637d3372010-08-25 14:37:03 -0700201 mScroller = new OverScroller(getContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800202 setFocusable(true);
203 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
204 setWillNotDraw(false);
Romain Guy4296fc42009-07-06 11:48:52 -0700205 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
206 mTouchSlop = configuration.getScaledTouchSlop();
207 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
208 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Adam Powell637d3372010-08-25 14:37:03 -0700209 mOverscrollDistance = configuration.getScaledOverscrollDistance();
210 mOverflingDistance = configuration.getScaledOverflingDistance();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800211 }
212
213 @Override
214 public void addView(View child) {
215 if (getChildCount() > 0) {
216 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
217 }
218
219 super.addView(child);
220 }
221
222 @Override
223 public void addView(View child, int index) {
224 if (getChildCount() > 0) {
225 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
226 }
227
228 super.addView(child, index);
229 }
230
231 @Override
232 public void addView(View child, ViewGroup.LayoutParams params) {
233 if (getChildCount() > 0) {
234 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
235 }
236
237 super.addView(child, params);
238 }
239
240 @Override
241 public void addView(View child, int index, ViewGroup.LayoutParams params) {
242 if (getChildCount() > 0) {
243 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
244 }
245
246 super.addView(child, index, params);
247 }
248
249 /**
250 * @return Returns true this HorizontalScrollView can be scrolled
251 */
252 private boolean canScroll() {
253 View child = getChildAt(0);
254 if (child != null) {
255 int childWidth = child.getWidth();
256 return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
257 }
258 return false;
259 }
260
261 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700262 * Indicates whether this HorizontalScrollView's content is stretched to
263 * fill the viewport.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800264 *
265 * @return True if the content fills the viewport, false otherwise.
Romain Guyfdbf4842010-08-16 10:55:49 -0700266 *
267 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800268 */
269 public boolean isFillViewport() {
270 return mFillViewport;
271 }
272
273 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700274 * Indicates this HorizontalScrollView whether it should stretch its content width
275 * to fill the viewport or not.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800276 *
277 * @param fillViewport True to stretch the content's width to the viewport's
278 * boundaries, false otherwise.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800279 *
Romain Guyfdbf4842010-08-16 10:55:49 -0700280 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800281 */
282 public void setFillViewport(boolean fillViewport) {
283 if (fillViewport != mFillViewport) {
284 mFillViewport = fillViewport;
285 requestLayout();
286 }
287 }
288
289 /**
290 * @return Whether arrow scrolling will animate its transition.
291 */
292 public boolean isSmoothScrollingEnabled() {
293 return mSmoothScrollingEnabled;
294 }
295
296 /**
297 * Set whether arrow scrolling will animate its transition.
298 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
299 */
300 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
301 mSmoothScrollingEnabled = smoothScrollingEnabled;
302 }
303
304 @Override
305 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
306 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
307
308 if (!mFillViewport) {
309 return;
310 }
311
312 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
313 if (widthMode == MeasureSpec.UNSPECIFIED) {
314 return;
315 }
316
Romain Guyef0e9ae2009-07-10 14:11:26 -0700317 if (getChildCount() > 0) {
318 final View child = getChildAt(0);
319 int width = getMeasuredWidth();
320 if (child.getMeasuredWidth() < width) {
321 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
Mindy Pereira4e30d892010-11-24 15:32:39 -0800322
Romain Guyef0e9ae2009-07-10 14:11:26 -0700323 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop
324 + mPaddingBottom, lp.height);
325 width -= mPaddingLeft;
326 width -= mPaddingRight;
327 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
Mindy Pereira4e30d892010-11-24 15:32:39 -0800328
Romain Guyef0e9ae2009-07-10 14:11:26 -0700329 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
330 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800331 }
332 }
333
334 @Override
335 public boolean dispatchKeyEvent(KeyEvent event) {
336 // Let the focused view and/or our descendants get the key first
Romain Guy8e618e52010-03-08 12:18:20 -0800337 return super.dispatchKeyEvent(event) || executeKeyEvent(event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800338 }
339
340 /**
341 * You can call this function yourself to have the scroll view perform
342 * scrolling from a key event, just as if the event had been dispatched to
343 * it by the view hierarchy.
344 *
345 * @param event The key event to execute.
346 * @return Return true if the event was handled, else false.
347 */
348 public boolean executeKeyEvent(KeyEvent event) {
349 mTempRect.setEmpty();
350
351 if (!canScroll()) {
352 if (isFocused()) {
353 View currentFocused = findFocus();
354 if (currentFocused == this) currentFocused = null;
355 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
356 currentFocused, View.FOCUS_RIGHT);
357 return nextFocused != null && nextFocused != this &&
358 nextFocused.requestFocus(View.FOCUS_RIGHT);
359 }
360 return false;
361 }
362
363 boolean handled = false;
364 if (event.getAction() == KeyEvent.ACTION_DOWN) {
365 switch (event.getKeyCode()) {
366 case KeyEvent.KEYCODE_DPAD_LEFT:
367 if (!event.isAltPressed()) {
368 handled = arrowScroll(View.FOCUS_LEFT);
369 } else {
370 handled = fullScroll(View.FOCUS_LEFT);
371 }
372 break;
373 case KeyEvent.KEYCODE_DPAD_RIGHT:
374 if (!event.isAltPressed()) {
375 handled = arrowScroll(View.FOCUS_RIGHT);
376 } else {
377 handled = fullScroll(View.FOCUS_RIGHT);
378 }
379 break;
380 case KeyEvent.KEYCODE_SPACE:
381 pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
382 break;
383 }
384 }
385
386 return handled;
387 }
388
Adam Powell4cd47702010-02-25 11:21:14 -0800389 private boolean inChild(int x, int y) {
390 if (getChildCount() > 0) {
Adam Powell352b9782010-03-24 14:23:43 -0700391 final int scrollX = mScrollX;
Adam Powell4cd47702010-02-25 11:21:14 -0800392 final View child = getChildAt(0);
393 return !(y < child.getTop()
394 || y >= child.getBottom()
Adam Powell352b9782010-03-24 14:23:43 -0700395 || x < child.getLeft() - scrollX
396 || x >= child.getRight() - scrollX);
Adam Powell4cd47702010-02-25 11:21:14 -0800397 }
398 return false;
399 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800400
Michael Jurka13451a42011-08-22 15:54:21 -0700401 private void initOrResetVelocityTracker() {
402 if (mVelocityTracker == null) {
403 mVelocityTracker = VelocityTracker.obtain();
404 } else {
405 mVelocityTracker.clear();
406 }
407 }
408
409 private void initVelocityTrackerIfNotExists() {
410 if (mVelocityTracker == null) {
411 mVelocityTracker = VelocityTracker.obtain();
412 }
413 }
414
415 private void recycleVelocityTracker() {
416 if (mVelocityTracker != null) {
417 mVelocityTracker.recycle();
418 mVelocityTracker = null;
419 }
420 }
421
422 @Override
423 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
424 if (disallowIntercept) {
425 recycleVelocityTracker();
426 }
427 super.requestDisallowInterceptTouchEvent(disallowIntercept);
428 }
429
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800430 @Override
431 public boolean onInterceptTouchEvent(MotionEvent ev) {
432 /*
433 * This method JUST determines whether we want to intercept the motion.
434 * If we return true, onMotionEvent will be called and we do the actual
435 * scrolling there.
436 */
437
438 /*
439 * Shortcut the most recurring case: the user is in the dragging
440 * state and he is moving his finger. We want to intercept this
441 * motion.
442 */
443 final int action = ev.getAction();
444 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
445 return true;
446 }
447
Adam Powell4cd47702010-02-25 11:21:14 -0800448 switch (action & MotionEvent.ACTION_MASK) {
449 case MotionEvent.ACTION_MOVE: {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800450 /*
451 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
452 * whether the user has moved far enough from his original down touch.
453 */
454
455 /*
456 * Locally do absolute value. mLastMotionX is set to the x value
457 * of the down event.
458 */
Adam Powell9d0335b2010-03-24 13:42:51 -0700459 final int activePointerId = mActivePointerId;
460 if (activePointerId == INVALID_POINTER) {
461 // If we don't have a valid id, the touch down wasn't on content.
462 break;
463 }
464
465 final int pointerIndex = ev.findPointerIndex(activePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100466 if (pointerIndex == -1) {
467 Log.e(TAG, "Invalid pointerId=" + activePointerId
468 + " in onInterceptTouchEvent");
469 break;
470 }
471
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700472 final int x = (int) ev.getX(pointerIndex);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800473 final int xDiff = (int) Math.abs(x - mLastMotionX);
474 if (xDiff > mTouchSlop) {
475 mIsBeingDragged = true;
Adam Powell4cd47702010-02-25 11:21:14 -0800476 mLastMotionX = x;
Michael Jurka13451a42011-08-22 15:54:21 -0700477 initVelocityTrackerIfNotExists();
478 mVelocityTracker.addMovement(ev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800479 if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
480 }
481 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800482 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800483
Adam Powell4cd47702010-02-25 11:21:14 -0800484 case MotionEvent.ACTION_DOWN: {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700485 final int x = (int) ev.getX();
Adam Powell4cd47702010-02-25 11:21:14 -0800486 if (!inChild((int) x, (int) ev.getY())) {
487 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700488 recycleVelocityTracker();
Adam Powell4cd47702010-02-25 11:21:14 -0800489 break;
490 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800491
Adam Powell4cd47702010-02-25 11:21:14 -0800492 /*
493 * Remember location of down touch.
494 * ACTION_DOWN always refers to pointer index 0.
495 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800496 mLastMotionX = x;
Adam Powell4cd47702010-02-25 11:21:14 -0800497 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800498
Michael Jurka13451a42011-08-22 15:54:21 -0700499 initOrResetVelocityTracker();
500 mVelocityTracker.addMovement(ev);
501
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800502 /*
503 * If being flinged and user touches the screen, initiate drag;
504 * otherwise don't. mScroller.isFinished should be false when
505 * being flinged.
506 */
507 mIsBeingDragged = !mScroller.isFinished();
508 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800509 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800510
511 case MotionEvent.ACTION_CANCEL:
512 case MotionEvent.ACTION_UP:
513 /* Release the drag */
514 mIsBeingDragged = false;
Adam Powell4cd47702010-02-25 11:21:14 -0800515 mActivePointerId = INVALID_POINTER;
Adam Powell637d3372010-08-25 14:37:03 -0700516 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700517 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700518 }
Adam Powell4cd47702010-02-25 11:21:14 -0800519 break;
Adam Powell9bc30d32011-02-28 10:27:49 -0800520 case MotionEvent.ACTION_POINTER_DOWN: {
521 final int index = ev.getActionIndex();
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700522 mLastMotionX = (int) ev.getX(index);
Adam Powell9bc30d32011-02-28 10:27:49 -0800523 mActivePointerId = ev.getPointerId(index);
524 break;
525 }
Adam Powell4cd47702010-02-25 11:21:14 -0800526 case MotionEvent.ACTION_POINTER_UP:
527 onSecondaryPointerUp(ev);
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700528 mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800529 break;
530 }
531
532 /*
533 * The only time we want to intercept motion events is if we are in the
534 * drag mode.
535 */
536 return mIsBeingDragged;
537 }
538
539 @Override
540 public boolean onTouchEvent(MotionEvent ev) {
Michael Jurka13451a42011-08-22 15:54:21 -0700541 initVelocityTrackerIfNotExists();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800542 mVelocityTracker.addMovement(ev);
543
544 final int action = ev.getAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800545
Adam Powell4cd47702010-02-25 11:21:14 -0800546 switch (action & MotionEvent.ACTION_MASK) {
547 case MotionEvent.ACTION_DOWN: {
Adam Powellb3e02c42012-05-02 22:05:46 -0700548 if (getChildCount() == 0) {
Jeff Brownfb757382011-01-18 18:42:33 -0800549 return false;
550 }
Adam Powellb3e02c42012-05-02 22:05:46 -0700551 if ((mIsBeingDragged = !mScroller.isFinished())) {
552 final ViewParent parent = getParent();
553 if (parent != null) {
554 parent.requestDisallowInterceptTouchEvent(true);
555 }
556 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800557
Adam Powell352b9782010-03-24 14:23:43 -0700558 /*
559 * If being flinged and user touches, stop the fling. isFinished
560 * will be false if being flinged.
561 */
562 if (!mScroller.isFinished()) {
563 mScroller.abortAnimation();
564 }
565
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800566 // Remember where the motion event started
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700567 mLastMotionX = (int) ev.getX();
Adam Powell352b9782010-03-24 14:23:43 -0700568 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800569 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800570 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800571 case MotionEvent.ACTION_MOVE:
Adam Powellb3e02c42012-05-02 22:05:46 -0700572 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100573 if (activePointerIndex == -1) {
574 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
575 break;
576 }
577
Adam Powellb3e02c42012-05-02 22:05:46 -0700578 final int x = (int) ev.getX(activePointerIndex);
579 int deltaX = mLastMotionX - x;
580 if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
581 final ViewParent parent = getParent();
582 if (parent != null) {
583 parent.requestDisallowInterceptTouchEvent(true);
584 }
585 mIsBeingDragged = true;
586 if (deltaX > 0) {
587 deltaX -= mTouchSlop;
588 } else {
589 deltaX += mTouchSlop;
590 }
591 }
Adam Powell4cd47702010-02-25 11:21:14 -0800592 if (mIsBeingDragged) {
593 // Scroll to follow the motion event
Adam Powell4cd47702010-02-25 11:21:14 -0800594 mLastMotionX = x;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800595
Adam Powell637d3372010-08-25 14:37:03 -0700596 final int oldX = mScrollX;
597 final int oldY = mScrollY;
598 final int range = getScrollRange();
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700599 final int overscrollMode = getOverScrollMode();
600 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
601 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
602
Alan Viverettecb25bd82013-06-03 17:10:44 -0700603 // Calling overScrollBy will call onOverScrolled, which
604 // calls onScrollChanged if applicable.
Adam Powellf6a6c972011-09-28 23:30:20 -0700605 if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -0700606 mOverscrollDistance, 0, true)) {
607 // Break our velocity if we hit a scroll barrier.
608 mVelocityTracker.clear();
609 }
Adam Powell637d3372010-08-25 14:37:03 -0700610
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700611 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -0700612 final int pulledToX = oldX + deltaX;
613 if (pulledToX < 0) {
614 mEdgeGlowLeft.onPull((float) deltaX / getWidth());
615 if (!mEdgeGlowRight.isFinished()) {
616 mEdgeGlowRight.onRelease();
617 }
618 } else if (pulledToX > range) {
619 mEdgeGlowRight.onPull((float) deltaX / getWidth());
620 if (!mEdgeGlowLeft.isFinished()) {
621 mEdgeGlowLeft.onRelease();
622 }
623 }
624 if (mEdgeGlowLeft != null
625 && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700626 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700627 }
628 }
Adam Powell4cd47702010-02-25 11:21:14 -0800629 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800630 break;
631 case MotionEvent.ACTION_UP:
Adam Powell4cd47702010-02-25 11:21:14 -0800632 if (mIsBeingDragged) {
633 final VelocityTracker velocityTracker = mVelocityTracker;
634 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
635 int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800636
Adam Powellf6a6c972011-09-28 23:30:20 -0700637 if (getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700638 if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
639 fling(-initialVelocity);
640 } else {
Adam Powellf6a6c972011-09-28 23:30:20 -0700641 if (mScroller.springBack(mScrollX, mScrollY, 0,
642 getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700643 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700644 }
645 }
Adam Powell17dfce12010-01-25 18:38:22 -0800646 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800647
Adam Powell4cd47702010-02-25 11:21:14 -0800648 mActivePointerId = INVALID_POINTER;
649 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700650 recycleVelocityTracker();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800651
Adam Powell637d3372010-08-25 14:37:03 -0700652 if (mEdgeGlowLeft != null) {
653 mEdgeGlowLeft.onRelease();
654 mEdgeGlowRight.onRelease();
655 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800656 }
Adam Powell4cd47702010-02-25 11:21:14 -0800657 break;
Adam Powell352b9782010-03-24 14:23:43 -0700658 case MotionEvent.ACTION_CANCEL:
659 if (mIsBeingDragged && getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700660 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700661 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700662 }
Adam Powell352b9782010-03-24 14:23:43 -0700663 mActivePointerId = INVALID_POINTER;
664 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700665 recycleVelocityTracker();
666
Adam Powell637d3372010-08-25 14:37:03 -0700667 if (mEdgeGlowLeft != null) {
668 mEdgeGlowLeft.onRelease();
669 mEdgeGlowRight.onRelease();
670 }
Adam Powell352b9782010-03-24 14:23:43 -0700671 }
672 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800673 case MotionEvent.ACTION_POINTER_UP:
674 onSecondaryPointerUp(ev);
675 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800676 }
677 return true;
678 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800679
Adam Powell4cd47702010-02-25 11:21:14 -0800680 private void onSecondaryPointerUp(MotionEvent ev) {
681 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
682 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
683 final int pointerId = ev.getPointerId(pointerIndex);
684 if (pointerId == mActivePointerId) {
685 // This was our active pointer going up. Choose a new
686 // active pointer and adjust accordingly.
687 // TODO: Make this decision more intelligent.
688 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700689 mLastMotionX = (int) ev.getX(newPointerIndex);
Adam Powell4cd47702010-02-25 11:21:14 -0800690 mActivePointerId = ev.getPointerId(newPointerIndex);
691 if (mVelocityTracker != null) {
692 mVelocityTracker.clear();
693 }
694 }
695 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800696
Adam Powell637d3372010-08-25 14:37:03 -0700697 @Override
Jeff Brown33bbfd22011-02-24 20:55:35 -0800698 public boolean onGenericMotionEvent(MotionEvent event) {
699 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
700 switch (event.getAction()) {
701 case MotionEvent.ACTION_SCROLL: {
702 if (!mIsBeingDragged) {
703 final float hscroll;
704 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
705 hscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
706 } else {
707 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
708 }
709 if (hscroll != 0) {
710 final int delta = (int) (hscroll * getHorizontalScrollFactor());
711 final int range = getScrollRange();
712 int oldScrollX = mScrollX;
713 int newScrollX = oldScrollX + delta;
714 if (newScrollX < 0) {
715 newScrollX = 0;
716 } else if (newScrollX > range) {
717 newScrollX = range;
718 }
719 if (newScrollX != oldScrollX) {
720 super.scrollTo(newScrollX, mScrollY);
721 return true;
722 }
723 }
724 }
725 }
726 }
727 }
728 return super.onGenericMotionEvent(event);
729 }
730
731 @Override
Michael Jurka9edd58e2011-10-28 16:39:18 -0700732 public boolean shouldDelayChildPressedState() {
733 return true;
734 }
735
736 @Override
Adam Powell637d3372010-08-25 14:37:03 -0700737 protected void onOverScrolled(int scrollX, int scrollY,
738 boolean clampedX, boolean clampedY) {
739 // Treat animating scrolls differently; see #computeScroll() for why.
740 if (!mScroller.isFinished()) {
Alan Viverettecb25bd82013-06-03 17:10:44 -0700741 final int oldX = mScrollX;
742 final int oldY = mScrollY;
Adam Powell637d3372010-08-25 14:37:03 -0700743 mScrollX = scrollX;
744 mScrollY = scrollY;
Romain Guy0fd89bf2011-01-26 15:41:30 -0800745 invalidateParentIfNeeded();
Alan Viverettecb25bd82013-06-03 17:10:44 -0700746 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
Adam Powell637d3372010-08-25 14:37:03 -0700747 if (clampedX) {
748 mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
749 }
750 } else {
751 super.scrollTo(scrollX, scrollY);
752 }
Romain Guye979e622012-03-20 13:50:27 -0700753
Romain Guye72cf732012-03-20 14:23:09 -0700754 awakenScrollBars();
Adam Powell637d3372010-08-25 14:37:03 -0700755 }
756
Svetoslav Ganova0156172011-06-26 17:55:44 -0700757 @Override
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700758 public boolean performAccessibilityAction(int action, Bundle arguments) {
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700759 if (super.performAccessibilityAction(action, arguments)) {
760 return true;
761 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700762 switch (action) {
763 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700764 if (!isEnabled()) {
765 return false;
766 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700767 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
768 final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
769 if (targetScrollX != mScrollX) {
770 smoothScrollTo(targetScrollX, 0);
771 return true;
772 }
773 } return false;
774 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700775 if (!isEnabled()) {
776 return false;
777 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700778 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
779 final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
780 if (targetScrollX != mScrollX) {
781 smoothScrollTo(targetScrollX, 0);
782 return true;
783 }
784 } return false;
785 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700786 return false;
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700787 }
788
789 @Override
Svetoslav Ganova0156172011-06-26 17:55:44 -0700790 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
791 super.onInitializeAccessibilityNodeInfo(info);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800792 info.setClassName(HorizontalScrollView.class.getName());
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700793 final int scrollRange = getScrollRange();
794 if (scrollRange > 0) {
795 info.setScrollable(true);
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700796 if (isEnabled() && mScrollX > 0) {
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700797 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
798 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700799 if (isEnabled() && mScrollX < scrollRange) {
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700800 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
801 }
802 }
Svetoslav Ganova0156172011-06-26 17:55:44 -0700803 }
804
805 @Override
806 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
807 super.onInitializeAccessibilityEvent(event);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800808 event.setClassName(HorizontalScrollView.class.getName());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700809 event.setScrollable(getScrollRange() > 0);
810 event.setScrollX(mScrollX);
811 event.setScrollY(mScrollY);
812 event.setMaxScrollX(getScrollRange());
813 event.setMaxScrollY(mScrollY);
Svetoslav Ganova0156172011-06-26 17:55:44 -0700814 }
815
Adam Powell0b8bb422010-02-08 14:30:45 -0800816 private int getScrollRange() {
817 int scrollRange = 0;
818 if (getChildCount() > 0) {
819 View child = getChildAt(0);
820 scrollRange = Math.max(0,
Adam Powell637d3372010-08-25 14:37:03 -0700821 child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
Adam Powell0b8bb422010-02-08 14:30:45 -0800822 }
823 return scrollRange;
824 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800825
826 /**
827 * <p>
828 * Finds the next focusable component that fits in this View's bounds
829 * (excluding fading edges) pretending that this View's left is located at
830 * the parameter left.
831 * </p>
832 *
833 * @param leftFocus look for a candidate is the one at the left of the bounds
834 * if leftFocus is true, or at the right of the bounds if leftFocus
835 * is false
836 * @param left the left offset of the bounds in which a focusable must be
837 * found (the fading edge is assumed to start at this position)
838 * @param preferredFocusable the View that has highest priority and will be
839 * returned if it is within my bounds (null is valid)
840 * @return the next focusable component in the bounds or null if none can be found
841 */
842 private View findFocusableViewInMyBounds(final boolean leftFocus,
843 final int left, View preferredFocusable) {
844 /*
845 * The fading edge's transparent side should be considered for focus
846 * since it's mostly visible, so we divide the actual fading edge length
847 * by 2.
848 */
849 final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
850 final int leftWithoutFadingEdge = left + fadingEdgeLength;
851 final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
852
853 if ((preferredFocusable != null)
854 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
855 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
856 return preferredFocusable;
857 }
858
859 return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
860 rightWithoutFadingEdge);
861 }
862
863 /**
864 * <p>
865 * Finds the next focusable component that fits in the specified bounds.
866 * </p>
867 *
868 * @param leftFocus look for a candidate is the one at the left of the bounds
869 * if leftFocus is true, or at the right of the bounds if
870 * leftFocus is false
871 * @param left the left offset of the bounds in which a focusable must be
872 * found
873 * @param right the right offset of the bounds in which a focusable must
874 * be found
875 * @return the next focusable component in the bounds or null if none can
876 * be found
877 */
878 private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
879
880 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
881 View focusCandidate = null;
882
883 /*
884 * A fully contained focusable is one where its left is below the bound's
885 * left, and its right is above the bound's right. A partially
886 * contained focusable is one where some part of it is within the
887 * bounds, but it also has some part that is not within bounds. A fully contained
888 * focusable is preferred to a partially contained focusable.
889 */
890 boolean foundFullyContainedFocusable = false;
891
892 int count = focusables.size();
893 for (int i = 0; i < count; i++) {
894 View view = focusables.get(i);
895 int viewLeft = view.getLeft();
896 int viewRight = view.getRight();
897
898 if (left < viewRight && viewLeft < right) {
899 /*
900 * the focusable is in the target area, it is a candidate for
901 * focusing
902 */
903
904 final boolean viewIsFullyContained = (left < viewLeft) &&
905 (viewRight < right);
906
907 if (focusCandidate == null) {
908 /* No candidate, take this one */
909 focusCandidate = view;
910 foundFullyContainedFocusable = viewIsFullyContained;
911 } else {
912 final boolean viewIsCloserToBoundary =
913 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
914 (!leftFocus && viewRight > focusCandidate.getRight());
915
916 if (foundFullyContainedFocusable) {
917 if (viewIsFullyContained && viewIsCloserToBoundary) {
918 /*
919 * We're dealing with only fully contained views, so
920 * it has to be closer to the boundary to beat our
921 * candidate
922 */
923 focusCandidate = view;
924 }
925 } else {
926 if (viewIsFullyContained) {
927 /* Any fully contained view beats a partially contained view */
928 focusCandidate = view;
929 foundFullyContainedFocusable = true;
930 } else if (viewIsCloserToBoundary) {
931 /*
932 * Partially contained view beats another partially
933 * contained view if it's closer
934 */
935 focusCandidate = view;
936 }
937 }
938 }
939 }
940 }
941
942 return focusCandidate;
943 }
944
945 /**
946 * <p>Handles scrolling in response to a "page up/down" shortcut press. This
947 * method will scroll the view by one page left or right and give the focus
948 * to the leftmost/rightmost component in the new visible area. If no
949 * component is a good candidate for focus, this scrollview reclaims the
950 * focus.</p>
951 *
952 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
953 * to go one page left or {@link android.view.View#FOCUS_RIGHT}
954 * to go one page right
955 * @return true if the key event is consumed by this method, false otherwise
956 */
957 public boolean pageScroll(int direction) {
958 boolean right = direction == View.FOCUS_RIGHT;
959 int width = getWidth();
960
961 if (right) {
962 mTempRect.left = getScrollX() + width;
963 int count = getChildCount();
964 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -0700965 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800966 if (mTempRect.left + width > view.getRight()) {
967 mTempRect.left = view.getRight() - width;
968 }
969 }
970 } else {
971 mTempRect.left = getScrollX() - width;
972 if (mTempRect.left < 0) {
973 mTempRect.left = 0;
974 }
975 }
976 mTempRect.right = mTempRect.left + width;
977
978 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
979 }
980
981 /**
982 * <p>Handles scrolling in response to a "home/end" shortcut press. This
983 * method will scroll the view to the left or right and give the focus
984 * to the leftmost/rightmost component in the new visible area. If no
985 * component is a good candidate for focus, this scrollview reclaims the
986 * focus.</p>
987 *
988 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
989 * to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
990 * to go the right
991 * @return true if the key event is consumed by this method, false otherwise
992 */
993 public boolean fullScroll(int direction) {
994 boolean right = direction == View.FOCUS_RIGHT;
995 int width = getWidth();
996
997 mTempRect.left = 0;
998 mTempRect.right = width;
999
1000 if (right) {
1001 int count = getChildCount();
1002 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001003 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001004 mTempRect.right = view.getRight();
1005 mTempRect.left = mTempRect.right - width;
1006 }
1007 }
1008
1009 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
1010 }
1011
1012 /**
1013 * <p>Scrolls the view to make the area defined by <code>left</code> and
1014 * <code>right</code> visible. This method attempts to give the focus
1015 * to a component visible in this area. If no component can be focused in
1016 * the new visible area, the focus is reclaimed by this scrollview.</p>
1017 *
1018 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1019 * to go left {@link android.view.View#FOCUS_RIGHT} to right
1020 * @param left the left offset of the new area to be made visible
1021 * @param right the right offset of the new area to be made visible
1022 * @return true if the key event is consumed by this method, false otherwise
1023 */
1024 private boolean scrollAndFocus(int direction, int left, int right) {
1025 boolean handled = true;
1026
1027 int width = getWidth();
1028 int containerLeft = getScrollX();
1029 int containerRight = containerLeft + width;
1030 boolean goLeft = direction == View.FOCUS_LEFT;
1031
1032 View newFocused = findFocusableViewInBounds(goLeft, left, right);
1033 if (newFocused == null) {
1034 newFocused = this;
1035 }
1036
1037 if (left >= containerLeft && right <= containerRight) {
1038 handled = false;
1039 } else {
1040 int delta = goLeft ? (left - containerLeft) : (right - containerRight);
1041 doScrollX(delta);
1042 }
1043
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001044 if (newFocused != findFocus()) newFocused.requestFocus(direction);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001045
1046 return handled;
1047 }
1048
1049 /**
1050 * Handle scrolling in response to a left or right arrow click.
1051 *
1052 * @param direction The direction corresponding to the arrow key that was
1053 * pressed
1054 * @return True if we consumed the event, false otherwise
1055 */
1056 public boolean arrowScroll(int direction) {
1057
1058 View currentFocused = findFocus();
1059 if (currentFocused == this) currentFocused = null;
1060
1061 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1062
1063 final int maxJump = getMaxScrollAmount();
1064
1065 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
1066 nextFocused.getDrawingRect(mTempRect);
1067 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1068 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1069 doScrollX(scrollDelta);
1070 nextFocused.requestFocus(direction);
1071 } else {
1072 // no new focus
1073 int scrollDelta = maxJump;
1074
1075 if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
1076 scrollDelta = getScrollX();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001077 } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001078
Romain Guyef0e9ae2009-07-10 14:11:26 -07001079 int daRight = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001080
1081 int screenRight = getScrollX() + getWidth();
1082
1083 if (daRight - screenRight < maxJump) {
1084 scrollDelta = daRight - screenRight;
1085 }
1086 }
1087 if (scrollDelta == 0) {
1088 return false;
1089 }
1090 doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
1091 }
1092
1093 if (currentFocused != null && currentFocused.isFocused()
1094 && isOffScreen(currentFocused)) {
1095 // previously focused item still has focus and is off screen, give
1096 // it up (take it back to ourselves)
1097 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1098 // sure to
1099 // get it)
1100 final int descendantFocusability = getDescendantFocusability(); // save
1101 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1102 requestFocus();
1103 setDescendantFocusability(descendantFocusability); // restore
1104 }
1105 return true;
1106 }
1107
1108 /**
1109 * @return whether the descendant of this scroll view is scrolled off
1110 * screen.
1111 */
1112 private boolean isOffScreen(View descendant) {
1113 return !isWithinDeltaOfScreen(descendant, 0);
1114 }
1115
1116 /**
1117 * @return whether the descendant of this scroll view is within delta
1118 * pixels of being on the screen.
1119 */
1120 private boolean isWithinDeltaOfScreen(View descendant, int delta) {
1121 descendant.getDrawingRect(mTempRect);
1122 offsetDescendantRectToMyCoords(descendant, mTempRect);
1123
1124 return (mTempRect.right + delta) >= getScrollX()
1125 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
1126 }
1127
1128 /**
1129 * Smooth scroll by a X delta
1130 *
1131 * @param delta the number of pixels to scroll by on the X axis
1132 */
1133 private void doScrollX(int delta) {
1134 if (delta != 0) {
1135 if (mSmoothScrollingEnabled) {
1136 smoothScrollBy(delta, 0);
1137 } else {
1138 scrollBy(delta, 0);
1139 }
1140 }
1141 }
1142
1143 /**
1144 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1145 *
1146 * @param dx the number of pixels to scroll by on the X axis
1147 * @param dy the number of pixels to scroll by on the Y axis
1148 */
1149 public final void smoothScrollBy(int dx, int dy) {
Adam Powell3fc37372010-01-28 13:52:24 -08001150 if (getChildCount() == 0) {
1151 // Nothing to do.
1152 return;
1153 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001154 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1155 if (duration > ANIMATED_SCROLL_GAP) {
Adam Powellf5446052010-01-28 17:24:56 -08001156 final int width = getWidth() - mPaddingRight - mPaddingLeft;
1157 final int right = getChildAt(0).getWidth();
1158 final int maxX = Math.max(0, right - width);
1159 final int scrollX = mScrollX;
1160 dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
1161
1162 mScroller.startScroll(scrollX, mScrollY, dx, 0);
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001163 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001164 } else {
1165 if (!mScroller.isFinished()) {
1166 mScroller.abortAnimation();
1167 }
1168 scrollBy(dx, dy);
1169 }
1170 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1171 }
1172
1173 /**
1174 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1175 *
1176 * @param x the position where to scroll on the X axis
1177 * @param y the position where to scroll on the Y axis
1178 */
1179 public final void smoothScrollTo(int x, int y) {
1180 smoothScrollBy(x - mScrollX, y - mScrollY);
1181 }
1182
1183 /**
1184 * <p>The scroll range of a scroll view is the overall width of all of its
1185 * children.</p>
1186 */
1187 @Override
1188 protected int computeHorizontalScrollRange() {
Adam Powella2f91012010-02-16 12:26:33 -08001189 final int count = getChildCount();
1190 final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
Adam Powell0b8bb422010-02-08 14:30:45 -08001191 if (count == 0) {
Adam Powella2f91012010-02-16 12:26:33 -08001192 return contentWidth;
Adam Powell0b8bb422010-02-08 14:30:45 -08001193 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001194
Adam Powell637d3372010-08-25 14:37:03 -07001195 int scrollRange = getChildAt(0).getRight();
1196 final int scrollX = mScrollX;
1197 final int overscrollRight = Math.max(0, scrollRange - contentWidth);
1198 if (scrollX < 0) {
1199 scrollRange -= scrollX;
1200 } else if (scrollX > overscrollRight) {
1201 scrollRange += scrollX - overscrollRight;
1202 }
1203
1204 return scrollRange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001205 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001206
Adam Powell0b8bb422010-02-08 14:30:45 -08001207 @Override
1208 protected int computeHorizontalScrollOffset() {
1209 return Math.max(0, super.computeHorizontalScrollOffset());
1210 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001211
1212 @Override
1213 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
1214 ViewGroup.LayoutParams lp = child.getLayoutParams();
1215
1216 int childWidthMeasureSpec;
1217 int childHeightMeasureSpec;
1218
1219 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
1220 + mPaddingBottom, lp.height);
1221
1222 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1223
1224 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1225 }
1226
1227 @Override
1228 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1229 int parentHeightMeasureSpec, int heightUsed) {
1230 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1231
1232 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1233 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
1234 + heightUsed, lp.height);
1235 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1236 lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
1237
1238 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1239 }
1240
1241 @Override
1242 public void computeScroll() {
1243 if (mScroller.computeScrollOffset()) {
1244 // This is called at drawing time by ViewGroup. We don't want to
1245 // re-show the scrollbars at this point, which scrollTo will do,
1246 // so we replicate most of scrollTo here.
1247 //
1248 // It's a little odd to call onScrollChanged from inside the drawing.
1249 //
1250 // It is, except when you remember that computeScroll() is used to
1251 // animate scrolling. So unless we want to defer the onScrollChanged()
1252 // until the end of the animated scrolling, we don't really have a
1253 // choice here.
1254 //
1255 // I agree. The alternative, which I think would be worse, is to post
1256 // something and tell the subclasses later. This is bad because there
1257 // will be a window where mScrollX/Y is different from what the app
1258 // thinks it is.
1259 //
1260 int oldX = mScrollX;
1261 int oldY = mScrollY;
1262 int x = mScroller.getCurrX();
1263 int y = mScroller.getCurrY();
Adam Powell17dfce12010-01-25 18:38:22 -08001264
Adam Powell637d3372010-08-25 14:37:03 -07001265 if (oldX != x || oldY != y) {
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001266 final int range = getScrollRange();
1267 final int overscrollMode = getOverScrollMode();
1268 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
1269 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
1270
1271 overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001272 mOverflingDistance, 0, false);
1273 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1274
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001275 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -07001276 if (x < 0 && oldX >= 0) {
1277 mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
1278 } else if (x > range && oldX <= range) {
1279 mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
1280 }
Adam Powell9d32d242010-03-29 16:02:07 -07001281 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001282 }
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001283
Romain Guye979e622012-03-20 13:50:27 -07001284 if (!awakenScrollBars()) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001285 postInvalidateOnAnimation();
Romain Guye979e622012-03-20 13:50:27 -07001286 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001287 }
1288 }
1289
1290 /**
1291 * Scrolls the view to the given child.
1292 *
1293 * @param child the View to scroll to
1294 */
1295 private void scrollToChild(View child) {
1296 child.getDrawingRect(mTempRect);
1297
1298 /* Offset from child's local coordinates to ScrollView coordinates */
1299 offsetDescendantRectToMyCoords(child, mTempRect);
1300
1301 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1302
1303 if (scrollDelta != 0) {
1304 scrollBy(scrollDelta, 0);
1305 }
1306 }
1307
1308 /**
1309 * If rect is off screen, scroll just enough to get it (or at least the
1310 * first screen size chunk of it) on screen.
1311 *
1312 * @param rect The rectangle.
1313 * @param immediate True to scroll immediately without animation
1314 * @return true if scrolling was performed
1315 */
1316 private boolean scrollToChildRect(Rect rect, boolean immediate) {
1317 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1318 final boolean scroll = delta != 0;
1319 if (scroll) {
1320 if (immediate) {
1321 scrollBy(delta, 0);
1322 } else {
1323 smoothScrollBy(delta, 0);
1324 }
1325 }
1326 return scroll;
1327 }
1328
1329 /**
1330 * Compute the amount to scroll in the X direction in order to get
1331 * a rectangle completely on the screen (or, if taller than the screen,
1332 * at least the first screen size chunk of it).
1333 *
1334 * @param rect The rect.
1335 * @return The scroll delta.
1336 */
1337 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001338 if (getChildCount() == 0) return 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001339
1340 int width = getWidth();
1341 int screenLeft = getScrollX();
1342 int screenRight = screenLeft + width;
1343
1344 int fadingEdge = getHorizontalFadingEdgeLength();
1345
1346 // leave room for left fading edge as long as rect isn't at very left
1347 if (rect.left > 0) {
1348 screenLeft += fadingEdge;
1349 }
1350
1351 // leave room for right fading edge as long as rect isn't at very right
1352 if (rect.right < getChildAt(0).getWidth()) {
1353 screenRight -= fadingEdge;
1354 }
1355
1356 int scrollXDelta = 0;
1357
1358 if (rect.right > screenRight && rect.left > screenLeft) {
1359 // need to move right to get it in view: move right just enough so
1360 // that the entire rectangle is in view (or at least the first
1361 // screen size chunk).
1362
1363 if (rect.width() > width) {
1364 // just enough to get screen size chunk on
1365 scrollXDelta += (rect.left - screenLeft);
1366 } else {
1367 // get entire rect at right of screen
1368 scrollXDelta += (rect.right - screenRight);
1369 }
1370
1371 // make sure we aren't scrolling beyond the end of our content
Romain Guyef0e9ae2009-07-10 14:11:26 -07001372 int right = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001373 int distanceToRight = right - screenRight;
1374 scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1375
1376 } else if (rect.left < screenLeft && rect.right < screenRight) {
1377 // need to move right to get it in view: move right just enough so that
1378 // entire rectangle is in view (or at least the first screen
1379 // size chunk of it).
1380
1381 if (rect.width() > width) {
1382 // screen size chunk
1383 scrollXDelta -= (screenRight - rect.right);
1384 } else {
1385 // entire rect at left
1386 scrollXDelta -= (screenLeft - rect.left);
1387 }
1388
1389 // make sure we aren't scrolling any further than the left our content
1390 scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1391 }
1392 return scrollXDelta;
1393 }
1394
1395 @Override
1396 public void requestChildFocus(View child, View focused) {
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001397 if (!mIsLayoutDirty) {
1398 scrollToChild(focused);
1399 } else {
1400 // The child may not be laid out yet, we can't compute the scroll yet
1401 mChildToScrollTo = focused;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001402 }
1403 super.requestChildFocus(child, focused);
1404 }
1405
1406
1407 /**
1408 * When looking for focus in children of a scroll view, need to be a little
1409 * more careful not to give focus to something that is scrolled off screen.
1410 *
1411 * This is more expensive than the default {@link android.view.ViewGroup}
1412 * implementation, otherwise this behavior might have been made the default.
1413 */
1414 @Override
1415 protected boolean onRequestFocusInDescendants(int direction,
1416 Rect previouslyFocusedRect) {
1417
1418 // convert from forward / backward notation to up / down / left / right
1419 // (ugh).
1420 if (direction == View.FOCUS_FORWARD) {
1421 direction = View.FOCUS_RIGHT;
1422 } else if (direction == View.FOCUS_BACKWARD) {
1423 direction = View.FOCUS_LEFT;
1424 }
1425
1426 final View nextFocus = previouslyFocusedRect == null ?
1427 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1428 FocusFinder.getInstance().findNextFocusFromRect(this,
1429 previouslyFocusedRect, direction);
1430
1431 if (nextFocus == null) {
1432 return false;
1433 }
1434
1435 if (isOffScreen(nextFocus)) {
1436 return false;
1437 }
1438
1439 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1440 }
1441
1442 @Override
1443 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1444 boolean immediate) {
1445 // offset into coordinate space of this scroll view
1446 rectangle.offset(child.getLeft() - child.getScrollX(),
1447 child.getTop() - child.getScrollY());
1448
1449 return scrollToChildRect(rectangle, immediate);
1450 }
1451
1452 @Override
1453 public void requestLayout() {
1454 mIsLayoutDirty = true;
1455 super.requestLayout();
1456 }
1457
1458 @Override
1459 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001460 int childWidth = 0;
1461 int childMargins = 0;
1462
1463 if (getChildCount() > 0) {
1464 childWidth = getChildAt(0).getMeasuredWidth();
1465 LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams();
1466 childMargins = childParams.leftMargin + childParams.rightMargin;
1467 }
1468
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001469 final int available = r - l - getPaddingLeftWithForeground() -
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001470 getPaddingRightWithForeground() - childMargins;
1471
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001472 final boolean forceLeftGravity = (childWidth > available);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001473
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001474 layoutChildren(l, t, r, b, forceLeftGravity);
Fabrice Di Megliof2fb76c2013-06-18 14:42:58 -07001475
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001476 mIsLayoutDirty = false;
1477 // Give a child focus if it needs it
1478 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
Adam Powell90f339a2013-06-13 17:44:04 -07001479 scrollToChild(mChildToScrollTo);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001480 }
1481 mChildToScrollTo = null;
1482
Chet Haase7a46dde2013-07-17 10:22:53 -07001483 if (!isLaidOut()) {
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001484 final int scrollRange = Math.max(0,
1485 childWidth - (r - l - mPaddingLeft - mPaddingRight));
1486 if (mSavedState != null) {
1487 if (isLayoutRtl() == mSavedState.isLayoutRtl) {
1488 mScrollX = mSavedState.scrollPosition;
1489 } else {
1490 mScrollX = scrollRange - mSavedState.scrollPosition;
1491 }
Fabrice Di Megliofafe88c2013-06-12 18:18:08 -07001492 mSavedState = null;
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001493 } else {
1494 if (isLayoutRtl()) {
1495 mScrollX = scrollRange - mScrollX;
1496 } // mScrollX default value is "0" for LTR
1497 }
1498 // Don't forget to clamp
1499 if (mScrollX > scrollRange) {
1500 mScrollX = scrollRange;
1501 } else if (mScrollX < 0) {
1502 mScrollX = 0;
1503 }
1504 }
1505
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001506 // Calling this with the present values causes it to re-claim them
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001507 scrollTo(mScrollX, mScrollY);
1508 }
1509
1510 @Override
1511 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1512 super.onSizeChanged(w, h, oldw, oldh);
1513
1514 View currentFocused = findFocus();
1515 if (null == currentFocused || this == currentFocused)
1516 return;
1517
1518 final int maxJump = mRight - mLeft;
1519
1520 if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
1521 currentFocused.getDrawingRect(mTempRect);
1522 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1523 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1524 doScrollX(scrollDelta);
1525 }
1526 }
1527
1528 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001529 * Return true if child is a descendant of parent, (or equal to the parent).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001530 */
Romain Guye979e622012-03-20 13:50:27 -07001531 private static boolean isViewDescendantOf(View child, View parent) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001532 if (child == parent) {
1533 return true;
1534 }
1535
1536 final ViewParent theParent = child.getParent();
1537 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1538 }
1539
1540 /**
1541 * Fling the scroll view
1542 *
1543 * @param velocityX The initial velocity in the X direction. Positive
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001544 * numbers mean that the finger/cursor is moving down the screen,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001545 * which means we want to scroll towards the left.
1546 */
1547 public void fling(int velocityX) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001548 if (getChildCount() > 0) {
1549 int width = getWidth() - mPaddingRight - mPaddingLeft;
1550 int right = getChildAt(0).getWidth();
Mindy Pereira4e30d892010-11-24 15:32:39 -08001551
1552 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001553 Math.max(0, right - width), 0, 0, width/2, 0);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001554
Romain Guyef0e9ae2009-07-10 14:11:26 -07001555 final boolean movingRight = velocityX > 0;
Mindy Pereira4e30d892010-11-24 15:32:39 -08001556
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001557 View currentFocused = findFocus();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001558 View newFocused = findFocusableViewInMyBounds(movingRight,
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001559 mScroller.getFinalX(), currentFocused);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001560
Romain Guyef0e9ae2009-07-10 14:11:26 -07001561 if (newFocused == null) {
1562 newFocused = this;
1563 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001564
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001565 if (newFocused != currentFocused) {
1566 newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
Romain Guyef0e9ae2009-07-10 14:11:26 -07001567 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001568
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001569 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001570 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001571 }
1572
1573 /**
1574 * {@inheritDoc}
1575 *
1576 * <p>This version also clamps the scrolling to the bounds of our child.
1577 */
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001578 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001579 public void scrollTo(int x, int y) {
1580 // we rely on the fact the View.scrollBy calls scrollTo.
1581 if (getChildCount() > 0) {
1582 View child = getChildAt(0);
1583 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1584 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1585 if (x != mScrollX || y != mScrollY) {
1586 super.scrollTo(x, y);
1587 }
1588 }
1589 }
1590
Adam Powell637d3372010-08-25 14:37:03 -07001591 @Override
1592 public void setOverScrollMode(int mode) {
1593 if (mode != OVER_SCROLL_NEVER) {
1594 if (mEdgeGlowLeft == null) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001595 Context context = getContext();
Adam Powell89935e42011-08-31 14:26:12 -07001596 mEdgeGlowLeft = new EdgeEffect(context);
1597 mEdgeGlowRight = new EdgeEffect(context);
Adam Powell637d3372010-08-25 14:37:03 -07001598 }
1599 } else {
1600 mEdgeGlowLeft = null;
1601 mEdgeGlowRight = null;
1602 }
1603 super.setOverScrollMode(mode);
1604 }
1605
Romain Guy2243e552011-03-08 11:46:28 -08001606 @SuppressWarnings({"SuspiciousNameCombination"})
Adam Powell637d3372010-08-25 14:37:03 -07001607 @Override
1608 public void draw(Canvas canvas) {
1609 super.draw(canvas);
1610 if (mEdgeGlowLeft != null) {
1611 final int scrollX = mScrollX;
1612 if (!mEdgeGlowLeft.isFinished()) {
1613 final int restoreCount = canvas.save();
Adam Powell7d863782011-02-15 15:05:03 -08001614 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001615
1616 canvas.rotate(270);
Adam Powell7d863782011-02-15 15:05:03 -08001617 canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
1618 mEdgeGlowLeft.setSize(height, getWidth());
Adam Powell637d3372010-08-25 14:37:03 -07001619 if (mEdgeGlowLeft.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001620 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001621 }
1622 canvas.restoreToCount(restoreCount);
1623 }
1624 if (!mEdgeGlowRight.isFinished()) {
1625 final int restoreCount = canvas.save();
1626 final int width = getWidth();
Adam Powell7d863782011-02-15 15:05:03 -08001627 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001628
1629 canvas.rotate(90);
Adam Powell7d863782011-02-15 15:05:03 -08001630 canvas.translate(-mPaddingTop,
Mindy Pereirab1297f72010-12-07 15:06:47 -08001631 -(Math.max(getScrollRange(), scrollX) + width));
1632 mEdgeGlowRight.setSize(height, width);
Adam Powell637d3372010-08-25 14:37:03 -07001633 if (mEdgeGlowRight.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001634 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001635 }
1636 canvas.restoreToCount(restoreCount);
1637 }
1638 }
1639 }
1640
Romain Guye979e622012-03-20 13:50:27 -07001641 private static int clamp(int n, int my, int child) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001642 if (my >= child || n < 0) {
1643 return 0;
1644 }
1645 if ((my + n) > child) {
1646 return child - my;
1647 }
1648 return n;
1649 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001650
1651 @Override
1652 protected void onRestoreInstanceState(Parcelable state) {
Adam Powell90f339a2013-06-13 17:44:04 -07001653 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1654 // Some old apps reused IDs in ways they shouldn't have.
1655 // Don't break them, but they don't get scroll state restoration.
1656 super.onRestoreInstanceState(state);
1657 return;
1658 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001659 SavedState ss = (SavedState) state;
1660 super.onRestoreInstanceState(ss.getSuperState());
1661 mSavedState = ss;
1662 requestLayout();
1663 }
1664
1665 @Override
1666 protected Parcelable onSaveInstanceState() {
Adam Powell90f339a2013-06-13 17:44:04 -07001667 if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
1668 // Some old apps reused IDs in ways they shouldn't have.
1669 // Don't break them, but they don't get scroll state restoration.
1670 return super.onSaveInstanceState();
1671 }
Fabrice Di Meglioc4d71222013-06-11 19:13:54 -07001672 Parcelable superState = super.onSaveInstanceState();
1673 SavedState ss = new SavedState(superState);
1674 ss.scrollPosition = mScrollX;
1675 ss.isLayoutRtl = isLayoutRtl();
1676 return ss;
1677 }
1678
1679 static class SavedState extends BaseSavedState {
1680 public int scrollPosition;
1681 public boolean isLayoutRtl;
1682
1683 SavedState(Parcelable superState) {
1684 super(superState);
1685 }
1686
1687 public SavedState(Parcel source) {
1688 super(source);
1689 scrollPosition = source.readInt();
1690 isLayoutRtl = (source.readInt() == 0) ? true : false;
1691 }
1692
1693 @Override
1694 public void writeToParcel(Parcel dest, int flags) {
1695 super.writeToParcel(dest, flags);
1696 dest.writeInt(scrollPosition);
1697 dest.writeInt(isLayoutRtl ? 1 : 0);
1698 }
1699
1700 @Override
1701 public String toString() {
1702 return "HorizontalScrollView.SavedState{"
1703 + Integer.toHexString(System.identityHashCode(this))
1704 + " scrollPosition=" + scrollPosition
1705 + " isLayoutRtl=" + isLayoutRtl + "}";
1706 }
1707
1708 public static final Parcelable.Creator<SavedState> CREATOR
1709 = new Parcelable.Creator<SavedState>() {
1710 public SavedState createFromParcel(Parcel in) {
1711 return new SavedState(in);
1712 }
1713
1714 public SavedState[] newArray(int size) {
1715 return new SavedState[size];
1716 }
1717 };
1718 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001719}