blob: ff0579cd78f9f15e890a256fee34c458c6bb9017 [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;
Svetoslav Ganova1dc7612012-05-10 04:14:53 -070023import android.os.Bundle;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080024import android.util.AttributeSet;
Johan Rosengren0dc291e2011-02-21 09:49:45 +010025import android.util.Log;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080026import android.view.FocusFinder;
Jeff Brown33bbfd22011-02-24 20:55:35 -080027import android.view.InputDevice;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080028import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.VelocityTracker;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.view.ViewDebug;
34import android.view.ViewGroup;
35import android.view.ViewParent;
Svetoslav Ganova0156172011-06-26 17:55:44 -070036import android.view.accessibility.AccessibilityEvent;
37import android.view.accessibility.AccessibilityNodeInfo;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080038import android.view.animation.AnimationUtils;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039
40import java.util.List;
41
42/**
43 * Layout container for a view hierarchy that can be scrolled by the user,
44 * allowing it to be larger than the physical display. A HorizontalScrollView
45 * is a {@link FrameLayout}, meaning you should place one child in it
46 * containing the entire contents to scroll; this child may itself be a layout
47 * manager with a complex hierarchy of objects. A child that is often used
48 * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
49 * array of top-level items that the user can scroll through.
50 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051 * <p>The {@link TextView} class also
Scott Main15279cf2012-07-02 21:49:47 -070052 * takes care of its own scrolling, so does not require a HorizontalScrollView, but
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053 * using the two together is possible to achieve the effect of a text view
54 * within a larger container.
55 *
Scott Main15279cf2012-07-02 21:49:47 -070056 * <p>HorizontalScrollView only supports horizontal scrolling. For vertical scrolling,
57 * use either {@link ScrollView} or {@link ListView}.
Mindy Pereira4e30d892010-11-24 15:32:39 -080058 *
59 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060 */
61public class HorizontalScrollView extends FrameLayout {
62 private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
63
64 private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
65
Johan Rosengren0dc291e2011-02-21 09:49:45 +010066 private static final String TAG = "HorizontalScrollView";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080067
68 private long mLastScroll;
69
70 private final Rect mTempRect = new Rect();
Adam Powell637d3372010-08-25 14:37:03 -070071 private OverScroller mScroller;
Adam Powell89935e42011-08-31 14:26:12 -070072 private EdgeEffect mEdgeGlowLeft;
73 private EdgeEffect mEdgeGlowRight;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080074
75 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080076 * Position of the last motion event.
77 */
Adam Powelldf3ae4f2012-04-10 18:55:22 -070078 private int mLastMotionX;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080079
80 /**
81 * True when the layout has changed but the traversal has not come through yet.
82 * Ideally the view hierarchy would keep track of this for us.
83 */
84 private boolean mIsLayoutDirty = true;
85
86 /**
87 * The child to give focus to in the event that a child has requested focus while the
88 * layout is dirty. This prevents the scroll from being wrong if the child has not been
89 * laid out before requesting focus.
90 */
91 private View mChildToScrollTo = null;
92
93 /**
94 * True if the user is currently dragging this ScrollView around. This is
95 * not the same as 'is being flinged', which can be checked by
96 * mScroller.isFinished() (flinging begins when the user lifts his finger).
97 */
98 private boolean mIsBeingDragged = false;
99
100 /**
101 * Determines speed during touch scrolling
102 */
103 private VelocityTracker mVelocityTracker;
104
105 /**
106 * When set to true, the scroll view measure its child to make it fill the currently
107 * visible area.
108 */
Romain Guya174d7a2011-01-07 13:27:39 -0800109 @ViewDebug.ExportedProperty(category = "layout")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110 private boolean mFillViewport;
111
112 /**
113 * Whether arrow scrolling is animated.
114 */
115 private boolean mSmoothScrollingEnabled = true;
116
117 private int mTouchSlop;
Romain Guy4296fc42009-07-06 11:48:52 -0700118 private int mMinimumVelocity;
119 private int mMaximumVelocity;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800120
Adam Powell637d3372010-08-25 14:37:03 -0700121 private int mOverscrollDistance;
122 private int mOverflingDistance;
123
Adam Powell4cd47702010-02-25 11:21:14 -0800124 /**
125 * ID of the active pointer. This is used to retain consistency during
126 * drags/flings if multiple pointers are used.
127 */
128 private int mActivePointerId = INVALID_POINTER;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800129
Adam Powell4cd47702010-02-25 11:21:14 -0800130 /**
131 * Sentinel value for no current active pointer.
132 * Used by {@link #mActivePointerId}.
133 */
134 private static final int INVALID_POINTER = -1;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800135
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800136 public HorizontalScrollView(Context context) {
137 this(context, null);
138 }
139
140 public HorizontalScrollView(Context context, AttributeSet attrs) {
141 this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
142 }
143
144 public HorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
145 super(context, attrs, defStyle);
146 initScrollView();
147
148 TypedArray a = context.obtainStyledAttributes(attrs,
149 android.R.styleable.HorizontalScrollView, defStyle, 0);
150
151 setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
152
153 a.recycle();
154 }
155
156 @Override
157 protected float getLeftFadingEdgeStrength() {
158 if (getChildCount() == 0) {
159 return 0.0f;
160 }
161
162 final int length = getHorizontalFadingEdgeLength();
163 if (mScrollX < length) {
164 return mScrollX / (float) length;
165 }
166
167 return 1.0f;
168 }
169
170 @Override
171 protected float getRightFadingEdgeStrength() {
172 if (getChildCount() == 0) {
173 return 0.0f;
174 }
175
176 final int length = getHorizontalFadingEdgeLength();
177 final int rightEdge = getWidth() - mPaddingRight;
178 final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
179 if (span < length) {
180 return span / (float) length;
181 }
182
183 return 1.0f;
184 }
185
186 /**
187 * @return The maximum amount this scroll view will scroll in response to
188 * an arrow event.
189 */
190 public int getMaxScrollAmount() {
191 return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
192 }
193
194
195 private void initScrollView() {
Adam Powell637d3372010-08-25 14:37:03 -0700196 mScroller = new OverScroller(getContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800197 setFocusable(true);
198 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
199 setWillNotDraw(false);
Romain Guy4296fc42009-07-06 11:48:52 -0700200 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
201 mTouchSlop = configuration.getScaledTouchSlop();
202 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
203 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Adam Powell637d3372010-08-25 14:37:03 -0700204 mOverscrollDistance = configuration.getScaledOverscrollDistance();
205 mOverflingDistance = configuration.getScaledOverflingDistance();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800206 }
207
208 @Override
209 public void addView(View child) {
210 if (getChildCount() > 0) {
211 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
212 }
213
214 super.addView(child);
215 }
216
217 @Override
218 public void addView(View child, int index) {
219 if (getChildCount() > 0) {
220 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
221 }
222
223 super.addView(child, index);
224 }
225
226 @Override
227 public void addView(View child, ViewGroup.LayoutParams params) {
228 if (getChildCount() > 0) {
229 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
230 }
231
232 super.addView(child, params);
233 }
234
235 @Override
236 public void addView(View child, int index, ViewGroup.LayoutParams params) {
237 if (getChildCount() > 0) {
238 throw new IllegalStateException("HorizontalScrollView can host only one direct child");
239 }
240
241 super.addView(child, index, params);
242 }
243
244 /**
245 * @return Returns true this HorizontalScrollView can be scrolled
246 */
247 private boolean canScroll() {
248 View child = getChildAt(0);
249 if (child != null) {
250 int childWidth = child.getWidth();
251 return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
252 }
253 return false;
254 }
255
256 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700257 * Indicates whether this HorizontalScrollView's content is stretched to
258 * fill the viewport.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800259 *
260 * @return True if the content fills the viewport, false otherwise.
Romain Guyfdbf4842010-08-16 10:55:49 -0700261 *
262 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800263 */
264 public boolean isFillViewport() {
265 return mFillViewport;
266 }
267
268 /**
Romain Guyfdbf4842010-08-16 10:55:49 -0700269 * Indicates this HorizontalScrollView whether it should stretch its content width
270 * to fill the viewport or not.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800271 *
272 * @param fillViewport True to stretch the content's width to the viewport's
273 * boundaries, false otherwise.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800274 *
Romain Guyfdbf4842010-08-16 10:55:49 -0700275 * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800276 */
277 public void setFillViewport(boolean fillViewport) {
278 if (fillViewport != mFillViewport) {
279 mFillViewport = fillViewport;
280 requestLayout();
281 }
282 }
283
284 /**
285 * @return Whether arrow scrolling will animate its transition.
286 */
287 public boolean isSmoothScrollingEnabled() {
288 return mSmoothScrollingEnabled;
289 }
290
291 /**
292 * Set whether arrow scrolling will animate its transition.
293 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
294 */
295 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
296 mSmoothScrollingEnabled = smoothScrollingEnabled;
297 }
298
299 @Override
300 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
301 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
302
303 if (!mFillViewport) {
304 return;
305 }
306
307 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
308 if (widthMode == MeasureSpec.UNSPECIFIED) {
309 return;
310 }
311
Romain Guyef0e9ae2009-07-10 14:11:26 -0700312 if (getChildCount() > 0) {
313 final View child = getChildAt(0);
314 int width = getMeasuredWidth();
315 if (child.getMeasuredWidth() < width) {
316 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
Mindy Pereira4e30d892010-11-24 15:32:39 -0800317
Romain Guyef0e9ae2009-07-10 14:11:26 -0700318 int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, mPaddingTop
319 + mPaddingBottom, lp.height);
320 width -= mPaddingLeft;
321 width -= mPaddingRight;
322 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
Mindy Pereira4e30d892010-11-24 15:32:39 -0800323
Romain Guyef0e9ae2009-07-10 14:11:26 -0700324 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
325 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800326 }
327 }
328
329 @Override
330 public boolean dispatchKeyEvent(KeyEvent event) {
331 // Let the focused view and/or our descendants get the key first
Romain Guy8e618e52010-03-08 12:18:20 -0800332 return super.dispatchKeyEvent(event) || executeKeyEvent(event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800333 }
334
335 /**
336 * You can call this function yourself to have the scroll view perform
337 * scrolling from a key event, just as if the event had been dispatched to
338 * it by the view hierarchy.
339 *
340 * @param event The key event to execute.
341 * @return Return true if the event was handled, else false.
342 */
343 public boolean executeKeyEvent(KeyEvent event) {
344 mTempRect.setEmpty();
345
346 if (!canScroll()) {
347 if (isFocused()) {
348 View currentFocused = findFocus();
349 if (currentFocused == this) currentFocused = null;
350 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
351 currentFocused, View.FOCUS_RIGHT);
352 return nextFocused != null && nextFocused != this &&
353 nextFocused.requestFocus(View.FOCUS_RIGHT);
354 }
355 return false;
356 }
357
358 boolean handled = false;
359 if (event.getAction() == KeyEvent.ACTION_DOWN) {
360 switch (event.getKeyCode()) {
361 case KeyEvent.KEYCODE_DPAD_LEFT:
362 if (!event.isAltPressed()) {
363 handled = arrowScroll(View.FOCUS_LEFT);
364 } else {
365 handled = fullScroll(View.FOCUS_LEFT);
366 }
367 break;
368 case KeyEvent.KEYCODE_DPAD_RIGHT:
369 if (!event.isAltPressed()) {
370 handled = arrowScroll(View.FOCUS_RIGHT);
371 } else {
372 handled = fullScroll(View.FOCUS_RIGHT);
373 }
374 break;
375 case KeyEvent.KEYCODE_SPACE:
376 pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
377 break;
378 }
379 }
380
381 return handled;
382 }
383
Adam Powell4cd47702010-02-25 11:21:14 -0800384 private boolean inChild(int x, int y) {
385 if (getChildCount() > 0) {
Adam Powell352b9782010-03-24 14:23:43 -0700386 final int scrollX = mScrollX;
Adam Powell4cd47702010-02-25 11:21:14 -0800387 final View child = getChildAt(0);
388 return !(y < child.getTop()
389 || y >= child.getBottom()
Adam Powell352b9782010-03-24 14:23:43 -0700390 || x < child.getLeft() - scrollX
391 || x >= child.getRight() - scrollX);
Adam Powell4cd47702010-02-25 11:21:14 -0800392 }
393 return false;
394 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800395
Michael Jurka13451a42011-08-22 15:54:21 -0700396 private void initOrResetVelocityTracker() {
397 if (mVelocityTracker == null) {
398 mVelocityTracker = VelocityTracker.obtain();
399 } else {
400 mVelocityTracker.clear();
401 }
402 }
403
404 private void initVelocityTrackerIfNotExists() {
405 if (mVelocityTracker == null) {
406 mVelocityTracker = VelocityTracker.obtain();
407 }
408 }
409
410 private void recycleVelocityTracker() {
411 if (mVelocityTracker != null) {
412 mVelocityTracker.recycle();
413 mVelocityTracker = null;
414 }
415 }
416
417 @Override
418 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
419 if (disallowIntercept) {
420 recycleVelocityTracker();
421 }
422 super.requestDisallowInterceptTouchEvent(disallowIntercept);
423 }
424
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800425 @Override
426 public boolean onInterceptTouchEvent(MotionEvent ev) {
427 /*
428 * This method JUST determines whether we want to intercept the motion.
429 * If we return true, onMotionEvent will be called and we do the actual
430 * scrolling there.
431 */
432
433 /*
434 * Shortcut the most recurring case: the user is in the dragging
435 * state and he is moving his finger. We want to intercept this
436 * motion.
437 */
438 final int action = ev.getAction();
439 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
440 return true;
441 }
442
Adam Powell4cd47702010-02-25 11:21:14 -0800443 switch (action & MotionEvent.ACTION_MASK) {
444 case MotionEvent.ACTION_MOVE: {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800445 /*
446 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
447 * whether the user has moved far enough from his original down touch.
448 */
449
450 /*
451 * Locally do absolute value. mLastMotionX is set to the x value
452 * of the down event.
453 */
Adam Powell9d0335b2010-03-24 13:42:51 -0700454 final int activePointerId = mActivePointerId;
455 if (activePointerId == INVALID_POINTER) {
456 // If we don't have a valid id, the touch down wasn't on content.
457 break;
458 }
459
460 final int pointerIndex = ev.findPointerIndex(activePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100461 if (pointerIndex == -1) {
462 Log.e(TAG, "Invalid pointerId=" + activePointerId
463 + " in onInterceptTouchEvent");
464 break;
465 }
466
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700467 final int x = (int) ev.getX(pointerIndex);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800468 final int xDiff = (int) Math.abs(x - mLastMotionX);
469 if (xDiff > mTouchSlop) {
470 mIsBeingDragged = true;
Adam Powell4cd47702010-02-25 11:21:14 -0800471 mLastMotionX = x;
Michael Jurka13451a42011-08-22 15:54:21 -0700472 initVelocityTrackerIfNotExists();
473 mVelocityTracker.addMovement(ev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800474 if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
475 }
476 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800477 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800478
Adam Powell4cd47702010-02-25 11:21:14 -0800479 case MotionEvent.ACTION_DOWN: {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700480 final int x = (int) ev.getX();
Adam Powell4cd47702010-02-25 11:21:14 -0800481 if (!inChild((int) x, (int) ev.getY())) {
482 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700483 recycleVelocityTracker();
Adam Powell4cd47702010-02-25 11:21:14 -0800484 break;
485 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800486
Adam Powell4cd47702010-02-25 11:21:14 -0800487 /*
488 * Remember location of down touch.
489 * ACTION_DOWN always refers to pointer index 0.
490 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800491 mLastMotionX = x;
Adam Powell4cd47702010-02-25 11:21:14 -0800492 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800493
Michael Jurka13451a42011-08-22 15:54:21 -0700494 initOrResetVelocityTracker();
495 mVelocityTracker.addMovement(ev);
496
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800497 /*
498 * If being flinged and user touches the screen, initiate drag;
499 * otherwise don't. mScroller.isFinished should be false when
500 * being flinged.
501 */
502 mIsBeingDragged = !mScroller.isFinished();
503 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800504 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800505
506 case MotionEvent.ACTION_CANCEL:
507 case MotionEvent.ACTION_UP:
508 /* Release the drag */
509 mIsBeingDragged = false;
Adam Powell4cd47702010-02-25 11:21:14 -0800510 mActivePointerId = INVALID_POINTER;
Adam Powell637d3372010-08-25 14:37:03 -0700511 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700512 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700513 }
Adam Powell4cd47702010-02-25 11:21:14 -0800514 break;
Adam Powell9bc30d32011-02-28 10:27:49 -0800515 case MotionEvent.ACTION_POINTER_DOWN: {
516 final int index = ev.getActionIndex();
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700517 mLastMotionX = (int) ev.getX(index);
Adam Powell9bc30d32011-02-28 10:27:49 -0800518 mActivePointerId = ev.getPointerId(index);
519 break;
520 }
Adam Powell4cd47702010-02-25 11:21:14 -0800521 case MotionEvent.ACTION_POINTER_UP:
522 onSecondaryPointerUp(ev);
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700523 mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800524 break;
525 }
526
527 /*
528 * The only time we want to intercept motion events is if we are in the
529 * drag mode.
530 */
531 return mIsBeingDragged;
532 }
533
534 @Override
535 public boolean onTouchEvent(MotionEvent ev) {
Michael Jurka13451a42011-08-22 15:54:21 -0700536 initVelocityTrackerIfNotExists();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800537 mVelocityTracker.addMovement(ev);
538
539 final int action = ev.getAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800540
Adam Powell4cd47702010-02-25 11:21:14 -0800541 switch (action & MotionEvent.ACTION_MASK) {
542 case MotionEvent.ACTION_DOWN: {
Adam Powellb3e02c42012-05-02 22:05:46 -0700543 if (getChildCount() == 0) {
Jeff Brownfb757382011-01-18 18:42:33 -0800544 return false;
545 }
Adam Powellb3e02c42012-05-02 22:05:46 -0700546 if ((mIsBeingDragged = !mScroller.isFinished())) {
547 final ViewParent parent = getParent();
548 if (parent != null) {
549 parent.requestDisallowInterceptTouchEvent(true);
550 }
551 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800552
Adam Powell352b9782010-03-24 14:23:43 -0700553 /*
554 * If being flinged and user touches, stop the fling. isFinished
555 * will be false if being flinged.
556 */
557 if (!mScroller.isFinished()) {
558 mScroller.abortAnimation();
559 }
560
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800561 // Remember where the motion event started
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700562 mLastMotionX = (int) ev.getX();
Adam Powell352b9782010-03-24 14:23:43 -0700563 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800564 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800565 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800566 case MotionEvent.ACTION_MOVE:
Adam Powellb3e02c42012-05-02 22:05:46 -0700567 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
Johan Rosengren0dc291e2011-02-21 09:49:45 +0100568 if (activePointerIndex == -1) {
569 Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
570 break;
571 }
572
Adam Powellb3e02c42012-05-02 22:05:46 -0700573 final int x = (int) ev.getX(activePointerIndex);
574 int deltaX = mLastMotionX - x;
575 if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
576 final ViewParent parent = getParent();
577 if (parent != null) {
578 parent.requestDisallowInterceptTouchEvent(true);
579 }
580 mIsBeingDragged = true;
581 if (deltaX > 0) {
582 deltaX -= mTouchSlop;
583 } else {
584 deltaX += mTouchSlop;
585 }
586 }
Adam Powell4cd47702010-02-25 11:21:14 -0800587 if (mIsBeingDragged) {
588 // Scroll to follow the motion event
Adam Powell4cd47702010-02-25 11:21:14 -0800589 mLastMotionX = x;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800590
Adam Powell637d3372010-08-25 14:37:03 -0700591 final int oldX = mScrollX;
592 final int oldY = mScrollY;
593 final int range = getScrollRange();
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700594 final int overscrollMode = getOverScrollMode();
595 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
596 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
597
Adam Powellf6a6c972011-09-28 23:30:20 -0700598 if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -0700599 mOverscrollDistance, 0, true)) {
600 // Break our velocity if we hit a scroll barrier.
601 mVelocityTracker.clear();
602 }
603 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
604
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700605 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -0700606 final int pulledToX = oldX + deltaX;
607 if (pulledToX < 0) {
608 mEdgeGlowLeft.onPull((float) deltaX / getWidth());
609 if (!mEdgeGlowRight.isFinished()) {
610 mEdgeGlowRight.onRelease();
611 }
612 } else if (pulledToX > range) {
613 mEdgeGlowRight.onPull((float) deltaX / getWidth());
614 if (!mEdgeGlowLeft.isFinished()) {
615 mEdgeGlowLeft.onRelease();
616 }
617 }
618 if (mEdgeGlowLeft != null
619 && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700620 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700621 }
622 }
Adam Powell4cd47702010-02-25 11:21:14 -0800623 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800624 break;
625 case MotionEvent.ACTION_UP:
Adam Powell4cd47702010-02-25 11:21:14 -0800626 if (mIsBeingDragged) {
627 final VelocityTracker velocityTracker = mVelocityTracker;
628 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
629 int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800630
Adam Powellf6a6c972011-09-28 23:30:20 -0700631 if (getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700632 if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
633 fling(-initialVelocity);
634 } else {
Adam Powellf6a6c972011-09-28 23:30:20 -0700635 if (mScroller.springBack(mScrollX, mScrollY, 0,
636 getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700637 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700638 }
639 }
Adam Powell17dfce12010-01-25 18:38:22 -0800640 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800641
Adam Powell4cd47702010-02-25 11:21:14 -0800642 mActivePointerId = INVALID_POINTER;
643 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700644 recycleVelocityTracker();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800645
Adam Powell637d3372010-08-25 14:37:03 -0700646 if (mEdgeGlowLeft != null) {
647 mEdgeGlowLeft.onRelease();
648 mEdgeGlowRight.onRelease();
649 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800650 }
Adam Powell4cd47702010-02-25 11:21:14 -0800651 break;
Adam Powell352b9782010-03-24 14:23:43 -0700652 case MotionEvent.ACTION_CANCEL:
653 if (mIsBeingDragged && getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700654 if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700655 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -0700656 }
Adam Powell352b9782010-03-24 14:23:43 -0700657 mActivePointerId = INVALID_POINTER;
658 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700659 recycleVelocityTracker();
660
Adam Powell637d3372010-08-25 14:37:03 -0700661 if (mEdgeGlowLeft != null) {
662 mEdgeGlowLeft.onRelease();
663 mEdgeGlowRight.onRelease();
664 }
Adam Powell352b9782010-03-24 14:23:43 -0700665 }
666 break;
Adam Powell4cd47702010-02-25 11:21:14 -0800667 case MotionEvent.ACTION_POINTER_UP:
668 onSecondaryPointerUp(ev);
669 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800670 }
671 return true;
672 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800673
Adam Powell4cd47702010-02-25 11:21:14 -0800674 private void onSecondaryPointerUp(MotionEvent ev) {
675 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
676 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
677 final int pointerId = ev.getPointerId(pointerIndex);
678 if (pointerId == mActivePointerId) {
679 // This was our active pointer going up. Choose a new
680 // active pointer and adjust accordingly.
681 // TODO: Make this decision more intelligent.
682 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
Adam Powelldf3ae4f2012-04-10 18:55:22 -0700683 mLastMotionX = (int) ev.getX(newPointerIndex);
Adam Powell4cd47702010-02-25 11:21:14 -0800684 mActivePointerId = ev.getPointerId(newPointerIndex);
685 if (mVelocityTracker != null) {
686 mVelocityTracker.clear();
687 }
688 }
689 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800690
Adam Powell637d3372010-08-25 14:37:03 -0700691 @Override
Jeff Brown33bbfd22011-02-24 20:55:35 -0800692 public boolean onGenericMotionEvent(MotionEvent event) {
693 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
694 switch (event.getAction()) {
695 case MotionEvent.ACTION_SCROLL: {
696 if (!mIsBeingDragged) {
697 final float hscroll;
698 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
699 hscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
700 } else {
701 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
702 }
703 if (hscroll != 0) {
704 final int delta = (int) (hscroll * getHorizontalScrollFactor());
705 final int range = getScrollRange();
706 int oldScrollX = mScrollX;
707 int newScrollX = oldScrollX + delta;
708 if (newScrollX < 0) {
709 newScrollX = 0;
710 } else if (newScrollX > range) {
711 newScrollX = range;
712 }
713 if (newScrollX != oldScrollX) {
714 super.scrollTo(newScrollX, mScrollY);
715 return true;
716 }
717 }
718 }
719 }
720 }
721 }
722 return super.onGenericMotionEvent(event);
723 }
724
725 @Override
Michael Jurka9edd58e2011-10-28 16:39:18 -0700726 public boolean shouldDelayChildPressedState() {
727 return true;
728 }
729
730 @Override
Adam Powell637d3372010-08-25 14:37:03 -0700731 protected void onOverScrolled(int scrollX, int scrollY,
732 boolean clampedX, boolean clampedY) {
733 // Treat animating scrolls differently; see #computeScroll() for why.
734 if (!mScroller.isFinished()) {
735 mScrollX = scrollX;
736 mScrollY = scrollY;
Romain Guy0fd89bf2011-01-26 15:41:30 -0800737 invalidateParentIfNeeded();
Adam Powell637d3372010-08-25 14:37:03 -0700738 if (clampedX) {
739 mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
740 }
741 } else {
742 super.scrollTo(scrollX, scrollY);
743 }
Romain Guye979e622012-03-20 13:50:27 -0700744
Romain Guye72cf732012-03-20 14:23:09 -0700745 awakenScrollBars();
Adam Powell637d3372010-08-25 14:37:03 -0700746 }
747
Svetoslav Ganova0156172011-06-26 17:55:44 -0700748 @Override
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700749 public boolean performAccessibilityAction(int action, Bundle arguments) {
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700750 if (super.performAccessibilityAction(action, arguments)) {
751 return true;
752 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700753 switch (action) {
754 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700755 if (!isEnabled()) {
756 return false;
757 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700758 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
759 final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
760 if (targetScrollX != mScrollX) {
761 smoothScrollTo(targetScrollX, 0);
762 return true;
763 }
764 } return false;
765 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700766 if (!isEnabled()) {
767 return false;
768 }
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700769 final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
770 final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
771 if (targetScrollX != mScrollX) {
772 smoothScrollTo(targetScrollX, 0);
773 return true;
774 }
775 } return false;
776 }
Svetoslav Ganov48d15862012-05-15 10:10:00 -0700777 return false;
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700778 }
779
780 @Override
Svetoslav Ganova0156172011-06-26 17:55:44 -0700781 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
782 super.onInitializeAccessibilityNodeInfo(info);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800783 info.setClassName(HorizontalScrollView.class.getName());
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700784 final int scrollRange = getScrollRange();
785 if (scrollRange > 0) {
786 info.setScrollable(true);
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700787 if (isEnabled() && mScrollX > 0) {
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700788 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
789 }
Svetoslav Ganovfb1e80a2012-05-16 17:33:19 -0700790 if (isEnabled() && mScrollX < scrollRange) {
Svetoslav Ganova1dc7612012-05-10 04:14:53 -0700791 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
792 }
793 }
Svetoslav Ganova0156172011-06-26 17:55:44 -0700794 }
795
796 @Override
797 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
798 super.onInitializeAccessibilityEvent(event);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800799 event.setClassName(HorizontalScrollView.class.getName());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700800 event.setScrollable(getScrollRange() > 0);
801 event.setScrollX(mScrollX);
802 event.setScrollY(mScrollY);
803 event.setMaxScrollX(getScrollRange());
804 event.setMaxScrollY(mScrollY);
Svetoslav Ganova0156172011-06-26 17:55:44 -0700805 }
806
Adam Powell0b8bb422010-02-08 14:30:45 -0800807 private int getScrollRange() {
808 int scrollRange = 0;
809 if (getChildCount() > 0) {
810 View child = getChildAt(0);
811 scrollRange = Math.max(0,
Adam Powell637d3372010-08-25 14:37:03 -0700812 child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
Adam Powell0b8bb422010-02-08 14:30:45 -0800813 }
814 return scrollRange;
815 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800816
817 /**
818 * <p>
819 * Finds the next focusable component that fits in this View's bounds
820 * (excluding fading edges) pretending that this View's left is located at
821 * the parameter left.
822 * </p>
823 *
824 * @param leftFocus look for a candidate is the one at the left of the bounds
825 * if leftFocus is true, or at the right of the bounds if leftFocus
826 * is false
827 * @param left the left offset of the bounds in which a focusable must be
828 * found (the fading edge is assumed to start at this position)
829 * @param preferredFocusable the View that has highest priority and will be
830 * returned if it is within my bounds (null is valid)
831 * @return the next focusable component in the bounds or null if none can be found
832 */
833 private View findFocusableViewInMyBounds(final boolean leftFocus,
834 final int left, View preferredFocusable) {
835 /*
836 * The fading edge's transparent side should be considered for focus
837 * since it's mostly visible, so we divide the actual fading edge length
838 * by 2.
839 */
840 final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
841 final int leftWithoutFadingEdge = left + fadingEdgeLength;
842 final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
843
844 if ((preferredFocusable != null)
845 && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
846 && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
847 return preferredFocusable;
848 }
849
850 return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
851 rightWithoutFadingEdge);
852 }
853
854 /**
855 * <p>
856 * Finds the next focusable component that fits in the specified bounds.
857 * </p>
858 *
859 * @param leftFocus look for a candidate is the one at the left of the bounds
860 * if leftFocus is true, or at the right of the bounds if
861 * leftFocus is false
862 * @param left the left offset of the bounds in which a focusable must be
863 * found
864 * @param right the right offset of the bounds in which a focusable must
865 * be found
866 * @return the next focusable component in the bounds or null if none can
867 * be found
868 */
869 private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
870
871 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
872 View focusCandidate = null;
873
874 /*
875 * A fully contained focusable is one where its left is below the bound's
876 * left, and its right is above the bound's right. A partially
877 * contained focusable is one where some part of it is within the
878 * bounds, but it also has some part that is not within bounds. A fully contained
879 * focusable is preferred to a partially contained focusable.
880 */
881 boolean foundFullyContainedFocusable = false;
882
883 int count = focusables.size();
884 for (int i = 0; i < count; i++) {
885 View view = focusables.get(i);
886 int viewLeft = view.getLeft();
887 int viewRight = view.getRight();
888
889 if (left < viewRight && viewLeft < right) {
890 /*
891 * the focusable is in the target area, it is a candidate for
892 * focusing
893 */
894
895 final boolean viewIsFullyContained = (left < viewLeft) &&
896 (viewRight < right);
897
898 if (focusCandidate == null) {
899 /* No candidate, take this one */
900 focusCandidate = view;
901 foundFullyContainedFocusable = viewIsFullyContained;
902 } else {
903 final boolean viewIsCloserToBoundary =
904 (leftFocus && viewLeft < focusCandidate.getLeft()) ||
905 (!leftFocus && viewRight > focusCandidate.getRight());
906
907 if (foundFullyContainedFocusable) {
908 if (viewIsFullyContained && viewIsCloserToBoundary) {
909 /*
910 * We're dealing with only fully contained views, so
911 * it has to be closer to the boundary to beat our
912 * candidate
913 */
914 focusCandidate = view;
915 }
916 } else {
917 if (viewIsFullyContained) {
918 /* Any fully contained view beats a partially contained view */
919 focusCandidate = view;
920 foundFullyContainedFocusable = true;
921 } else if (viewIsCloserToBoundary) {
922 /*
923 * Partially contained view beats another partially
924 * contained view if it's closer
925 */
926 focusCandidate = view;
927 }
928 }
929 }
930 }
931 }
932
933 return focusCandidate;
934 }
935
936 /**
937 * <p>Handles scrolling in response to a "page up/down" shortcut press. This
938 * method will scroll the view by one page left or right and give the focus
939 * to the leftmost/rightmost component in the new visible area. If no
940 * component is a good candidate for focus, this scrollview reclaims the
941 * focus.</p>
942 *
943 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
944 * to go one page left or {@link android.view.View#FOCUS_RIGHT}
945 * to go one page right
946 * @return true if the key event is consumed by this method, false otherwise
947 */
948 public boolean pageScroll(int direction) {
949 boolean right = direction == View.FOCUS_RIGHT;
950 int width = getWidth();
951
952 if (right) {
953 mTempRect.left = getScrollX() + width;
954 int count = getChildCount();
955 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -0700956 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800957 if (mTempRect.left + width > view.getRight()) {
958 mTempRect.left = view.getRight() - width;
959 }
960 }
961 } else {
962 mTempRect.left = getScrollX() - width;
963 if (mTempRect.left < 0) {
964 mTempRect.left = 0;
965 }
966 }
967 mTempRect.right = mTempRect.left + width;
968
969 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
970 }
971
972 /**
973 * <p>Handles scrolling in response to a "home/end" shortcut press. This
974 * method will scroll the view to the left or right and give the focus
975 * to the leftmost/rightmost component in the new visible area. If no
976 * component is a good candidate for focus, this scrollview reclaims the
977 * focus.</p>
978 *
979 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
980 * to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
981 * to go the right
982 * @return true if the key event is consumed by this method, false otherwise
983 */
984 public boolean fullScroll(int direction) {
985 boolean right = direction == View.FOCUS_RIGHT;
986 int width = getWidth();
987
988 mTempRect.left = 0;
989 mTempRect.right = width;
990
991 if (right) {
992 int count = getChildCount();
993 if (count > 0) {
Romain Guyef0e9ae2009-07-10 14:11:26 -0700994 View view = getChildAt(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800995 mTempRect.right = view.getRight();
996 mTempRect.left = mTempRect.right - width;
997 }
998 }
999
1000 return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
1001 }
1002
1003 /**
1004 * <p>Scrolls the view to make the area defined by <code>left</code> and
1005 * <code>right</code> visible. This method attempts to give the focus
1006 * to a component visible in this area. If no component can be focused in
1007 * the new visible area, the focus is reclaimed by this scrollview.</p>
1008 *
1009 * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
1010 * to go left {@link android.view.View#FOCUS_RIGHT} to right
1011 * @param left the left offset of the new area to be made visible
1012 * @param right the right offset of the new area to be made visible
1013 * @return true if the key event is consumed by this method, false otherwise
1014 */
1015 private boolean scrollAndFocus(int direction, int left, int right) {
1016 boolean handled = true;
1017
1018 int width = getWidth();
1019 int containerLeft = getScrollX();
1020 int containerRight = containerLeft + width;
1021 boolean goLeft = direction == View.FOCUS_LEFT;
1022
1023 View newFocused = findFocusableViewInBounds(goLeft, left, right);
1024 if (newFocused == null) {
1025 newFocused = this;
1026 }
1027
1028 if (left >= containerLeft && right <= containerRight) {
1029 handled = false;
1030 } else {
1031 int delta = goLeft ? (left - containerLeft) : (right - containerRight);
1032 doScrollX(delta);
1033 }
1034
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001035 if (newFocused != findFocus()) newFocused.requestFocus(direction);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001036
1037 return handled;
1038 }
1039
1040 /**
1041 * Handle scrolling in response to a left or right arrow click.
1042 *
1043 * @param direction The direction corresponding to the arrow key that was
1044 * pressed
1045 * @return True if we consumed the event, false otherwise
1046 */
1047 public boolean arrowScroll(int direction) {
1048
1049 View currentFocused = findFocus();
1050 if (currentFocused == this) currentFocused = null;
1051
1052 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
1053
1054 final int maxJump = getMaxScrollAmount();
1055
1056 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
1057 nextFocused.getDrawingRect(mTempRect);
1058 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
1059 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1060 doScrollX(scrollDelta);
1061 nextFocused.requestFocus(direction);
1062 } else {
1063 // no new focus
1064 int scrollDelta = maxJump;
1065
1066 if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
1067 scrollDelta = getScrollX();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001068 } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001069
Romain Guyef0e9ae2009-07-10 14:11:26 -07001070 int daRight = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001071
1072 int screenRight = getScrollX() + getWidth();
1073
1074 if (daRight - screenRight < maxJump) {
1075 scrollDelta = daRight - screenRight;
1076 }
1077 }
1078 if (scrollDelta == 0) {
1079 return false;
1080 }
1081 doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
1082 }
1083
1084 if (currentFocused != null && currentFocused.isFocused()
1085 && isOffScreen(currentFocused)) {
1086 // previously focused item still has focus and is off screen, give
1087 // it up (take it back to ourselves)
1088 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1089 // sure to
1090 // get it)
1091 final int descendantFocusability = getDescendantFocusability(); // save
1092 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1093 requestFocus();
1094 setDescendantFocusability(descendantFocusability); // restore
1095 }
1096 return true;
1097 }
1098
1099 /**
1100 * @return whether the descendant of this scroll view is scrolled off
1101 * screen.
1102 */
1103 private boolean isOffScreen(View descendant) {
1104 return !isWithinDeltaOfScreen(descendant, 0);
1105 }
1106
1107 /**
1108 * @return whether the descendant of this scroll view is within delta
1109 * pixels of being on the screen.
1110 */
1111 private boolean isWithinDeltaOfScreen(View descendant, int delta) {
1112 descendant.getDrawingRect(mTempRect);
1113 offsetDescendantRectToMyCoords(descendant, mTempRect);
1114
1115 return (mTempRect.right + delta) >= getScrollX()
1116 && (mTempRect.left - delta) <= (getScrollX() + getWidth());
1117 }
1118
1119 /**
1120 * Smooth scroll by a X delta
1121 *
1122 * @param delta the number of pixels to scroll by on the X axis
1123 */
1124 private void doScrollX(int delta) {
1125 if (delta != 0) {
1126 if (mSmoothScrollingEnabled) {
1127 smoothScrollBy(delta, 0);
1128 } else {
1129 scrollBy(delta, 0);
1130 }
1131 }
1132 }
1133
1134 /**
1135 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1136 *
1137 * @param dx the number of pixels to scroll by on the X axis
1138 * @param dy the number of pixels to scroll by on the Y axis
1139 */
1140 public final void smoothScrollBy(int dx, int dy) {
Adam Powell3fc37372010-01-28 13:52:24 -08001141 if (getChildCount() == 0) {
1142 // Nothing to do.
1143 return;
1144 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001145 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1146 if (duration > ANIMATED_SCROLL_GAP) {
Adam Powellf5446052010-01-28 17:24:56 -08001147 final int width = getWidth() - mPaddingRight - mPaddingLeft;
1148 final int right = getChildAt(0).getWidth();
1149 final int maxX = Math.max(0, right - width);
1150 final int scrollX = mScrollX;
1151 dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
1152
1153 mScroller.startScroll(scrollX, mScrollY, dx, 0);
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001154 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001155 } else {
1156 if (!mScroller.isFinished()) {
1157 mScroller.abortAnimation();
1158 }
1159 scrollBy(dx, dy);
1160 }
1161 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1162 }
1163
1164 /**
1165 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1166 *
1167 * @param x the position where to scroll on the X axis
1168 * @param y the position where to scroll on the Y axis
1169 */
1170 public final void smoothScrollTo(int x, int y) {
1171 smoothScrollBy(x - mScrollX, y - mScrollY);
1172 }
1173
1174 /**
1175 * <p>The scroll range of a scroll view is the overall width of all of its
1176 * children.</p>
1177 */
1178 @Override
1179 protected int computeHorizontalScrollRange() {
Adam Powella2f91012010-02-16 12:26:33 -08001180 final int count = getChildCount();
1181 final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
Adam Powell0b8bb422010-02-08 14:30:45 -08001182 if (count == 0) {
Adam Powella2f91012010-02-16 12:26:33 -08001183 return contentWidth;
Adam Powell0b8bb422010-02-08 14:30:45 -08001184 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001185
Adam Powell637d3372010-08-25 14:37:03 -07001186 int scrollRange = getChildAt(0).getRight();
1187 final int scrollX = mScrollX;
1188 final int overscrollRight = Math.max(0, scrollRange - contentWidth);
1189 if (scrollX < 0) {
1190 scrollRange -= scrollX;
1191 } else if (scrollX > overscrollRight) {
1192 scrollRange += scrollX - overscrollRight;
1193 }
1194
1195 return scrollRange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001196 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001197
Adam Powell0b8bb422010-02-08 14:30:45 -08001198 @Override
1199 protected int computeHorizontalScrollOffset() {
1200 return Math.max(0, super.computeHorizontalScrollOffset());
1201 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001202
1203 @Override
1204 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
1205 ViewGroup.LayoutParams lp = child.getLayoutParams();
1206
1207 int childWidthMeasureSpec;
1208 int childHeightMeasureSpec;
1209
1210 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop
1211 + mPaddingBottom, lp.height);
1212
1213 childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1214
1215 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1216 }
1217
1218 @Override
1219 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1220 int parentHeightMeasureSpec, int heightUsed) {
1221 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1222
1223 final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
1224 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
1225 + heightUsed, lp.height);
1226 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
1227 lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
1228
1229 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1230 }
1231
1232 @Override
1233 public void computeScroll() {
1234 if (mScroller.computeScrollOffset()) {
1235 // This is called at drawing time by ViewGroup. We don't want to
1236 // re-show the scrollbars at this point, which scrollTo will do,
1237 // so we replicate most of scrollTo here.
1238 //
1239 // It's a little odd to call onScrollChanged from inside the drawing.
1240 //
1241 // It is, except when you remember that computeScroll() is used to
1242 // animate scrolling. So unless we want to defer the onScrollChanged()
1243 // until the end of the animated scrolling, we don't really have a
1244 // choice here.
1245 //
1246 // I agree. The alternative, which I think would be worse, is to post
1247 // something and tell the subclasses later. This is bad because there
1248 // will be a window where mScrollX/Y is different from what the app
1249 // thinks it is.
1250 //
1251 int oldX = mScrollX;
1252 int oldY = mScrollY;
1253 int x = mScroller.getCurrX();
1254 int y = mScroller.getCurrY();
Adam Powell17dfce12010-01-25 18:38:22 -08001255
Adam Powell637d3372010-08-25 14:37:03 -07001256 if (oldX != x || oldY != y) {
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001257 final int range = getScrollRange();
1258 final int overscrollMode = getOverScrollMode();
1259 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
1260 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
1261
1262 overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001263 mOverflingDistance, 0, false);
1264 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1265
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001266 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -07001267 if (x < 0 && oldX >= 0) {
1268 mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
1269 } else if (x > range && oldX <= range) {
1270 mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
1271 }
Adam Powell9d32d242010-03-29 16:02:07 -07001272 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001273 }
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001274
Romain Guye979e622012-03-20 13:50:27 -07001275 if (!awakenScrollBars()) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001276 postInvalidateOnAnimation();
Romain Guye979e622012-03-20 13:50:27 -07001277 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001278 }
1279 }
1280
1281 /**
1282 * Scrolls the view to the given child.
1283 *
1284 * @param child the View to scroll to
1285 */
1286 private void scrollToChild(View child) {
1287 child.getDrawingRect(mTempRect);
1288
1289 /* Offset from child's local coordinates to ScrollView coordinates */
1290 offsetDescendantRectToMyCoords(child, mTempRect);
1291
1292 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1293
1294 if (scrollDelta != 0) {
1295 scrollBy(scrollDelta, 0);
1296 }
1297 }
1298
1299 /**
1300 * If rect is off screen, scroll just enough to get it (or at least the
1301 * first screen size chunk of it) on screen.
1302 *
1303 * @param rect The rectangle.
1304 * @param immediate True to scroll immediately without animation
1305 * @return true if scrolling was performed
1306 */
1307 private boolean scrollToChildRect(Rect rect, boolean immediate) {
1308 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1309 final boolean scroll = delta != 0;
1310 if (scroll) {
1311 if (immediate) {
1312 scrollBy(delta, 0);
1313 } else {
1314 smoothScrollBy(delta, 0);
1315 }
1316 }
1317 return scroll;
1318 }
1319
1320 /**
1321 * Compute the amount to scroll in the X direction in order to get
1322 * a rectangle completely on the screen (or, if taller than the screen,
1323 * at least the first screen size chunk of it).
1324 *
1325 * @param rect The rect.
1326 * @return The scroll delta.
1327 */
1328 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001329 if (getChildCount() == 0) return 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001330
1331 int width = getWidth();
1332 int screenLeft = getScrollX();
1333 int screenRight = screenLeft + width;
1334
1335 int fadingEdge = getHorizontalFadingEdgeLength();
1336
1337 // leave room for left fading edge as long as rect isn't at very left
1338 if (rect.left > 0) {
1339 screenLeft += fadingEdge;
1340 }
1341
1342 // leave room for right fading edge as long as rect isn't at very right
1343 if (rect.right < getChildAt(0).getWidth()) {
1344 screenRight -= fadingEdge;
1345 }
1346
1347 int scrollXDelta = 0;
1348
1349 if (rect.right > screenRight && rect.left > screenLeft) {
1350 // need to move right to get it in view: move right just enough so
1351 // that the entire rectangle is in view (or at least the first
1352 // screen size chunk).
1353
1354 if (rect.width() > width) {
1355 // just enough to get screen size chunk on
1356 scrollXDelta += (rect.left - screenLeft);
1357 } else {
1358 // get entire rect at right of screen
1359 scrollXDelta += (rect.right - screenRight);
1360 }
1361
1362 // make sure we aren't scrolling beyond the end of our content
Romain Guyef0e9ae2009-07-10 14:11:26 -07001363 int right = getChildAt(0).getRight();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001364 int distanceToRight = right - screenRight;
1365 scrollXDelta = Math.min(scrollXDelta, distanceToRight);
1366
1367 } else if (rect.left < screenLeft && rect.right < screenRight) {
1368 // need to move right to get it in view: move right just enough so that
1369 // entire rectangle is in view (or at least the first screen
1370 // size chunk of it).
1371
1372 if (rect.width() > width) {
1373 // screen size chunk
1374 scrollXDelta -= (screenRight - rect.right);
1375 } else {
1376 // entire rect at left
1377 scrollXDelta -= (screenLeft - rect.left);
1378 }
1379
1380 // make sure we aren't scrolling any further than the left our content
1381 scrollXDelta = Math.max(scrollXDelta, -getScrollX());
1382 }
1383 return scrollXDelta;
1384 }
1385
1386 @Override
1387 public void requestChildFocus(View child, View focused) {
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001388 if (!mIsLayoutDirty) {
1389 scrollToChild(focused);
1390 } else {
1391 // The child may not be laid out yet, we can't compute the scroll yet
1392 mChildToScrollTo = focused;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001393 }
1394 super.requestChildFocus(child, focused);
1395 }
1396
1397
1398 /**
1399 * When looking for focus in children of a scroll view, need to be a little
1400 * more careful not to give focus to something that is scrolled off screen.
1401 *
1402 * This is more expensive than the default {@link android.view.ViewGroup}
1403 * implementation, otherwise this behavior might have been made the default.
1404 */
1405 @Override
1406 protected boolean onRequestFocusInDescendants(int direction,
1407 Rect previouslyFocusedRect) {
1408
1409 // convert from forward / backward notation to up / down / left / right
1410 // (ugh).
1411 if (direction == View.FOCUS_FORWARD) {
1412 direction = View.FOCUS_RIGHT;
1413 } else if (direction == View.FOCUS_BACKWARD) {
1414 direction = View.FOCUS_LEFT;
1415 }
1416
1417 final View nextFocus = previouslyFocusedRect == null ?
1418 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1419 FocusFinder.getInstance().findNextFocusFromRect(this,
1420 previouslyFocusedRect, direction);
1421
1422 if (nextFocus == null) {
1423 return false;
1424 }
1425
1426 if (isOffScreen(nextFocus)) {
1427 return false;
1428 }
1429
1430 return nextFocus.requestFocus(direction, previouslyFocusedRect);
1431 }
1432
1433 @Override
1434 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1435 boolean immediate) {
1436 // offset into coordinate space of this scroll view
1437 rectangle.offset(child.getLeft() - child.getScrollX(),
1438 child.getTop() - child.getScrollY());
1439
1440 return scrollToChildRect(rectangle, immediate);
1441 }
1442
1443 @Override
1444 public void requestLayout() {
1445 mIsLayoutDirty = true;
1446 super.requestLayout();
1447 }
1448
1449 @Override
1450 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1451 super.onLayout(changed, l, t, r, b);
1452 mIsLayoutDirty = false;
1453 // Give a child focus if it needs it
1454 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
1455 scrollToChild(mChildToScrollTo);
1456 }
1457 mChildToScrollTo = null;
1458
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001459 // Calling this with the present values causes it to re-claim them
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001460 scrollTo(mScrollX, mScrollY);
1461 }
1462
1463 @Override
1464 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1465 super.onSizeChanged(w, h, oldw, oldh);
1466
1467 View currentFocused = findFocus();
1468 if (null == currentFocused || this == currentFocused)
1469 return;
1470
1471 final int maxJump = mRight - mLeft;
1472
1473 if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
1474 currentFocused.getDrawingRect(mTempRect);
1475 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1476 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1477 doScrollX(scrollDelta);
1478 }
1479 }
1480
1481 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001482 * Return true if child is a descendant of parent, (or equal to the parent).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001483 */
Romain Guye979e622012-03-20 13:50:27 -07001484 private static boolean isViewDescendantOf(View child, View parent) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001485 if (child == parent) {
1486 return true;
1487 }
1488
1489 final ViewParent theParent = child.getParent();
1490 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1491 }
1492
1493 /**
1494 * Fling the scroll view
1495 *
1496 * @param velocityX The initial velocity in the X direction. Positive
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001497 * numbers mean that the finger/cursor is moving down the screen,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001498 * which means we want to scroll towards the left.
1499 */
1500 public void fling(int velocityX) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001501 if (getChildCount() > 0) {
1502 int width = getWidth() - mPaddingRight - mPaddingLeft;
1503 int right = getChildAt(0).getWidth();
Mindy Pereira4e30d892010-11-24 15:32:39 -08001504
1505 mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001506 Math.max(0, right - width), 0, 0, width/2, 0);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001507
Romain Guyef0e9ae2009-07-10 14:11:26 -07001508 final boolean movingRight = velocityX > 0;
Mindy Pereira4e30d892010-11-24 15:32:39 -08001509
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001510 View currentFocused = findFocus();
Romain Guyef0e9ae2009-07-10 14:11:26 -07001511 View newFocused = findFocusableViewInMyBounds(movingRight,
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001512 mScroller.getFinalX(), currentFocused);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001513
Romain Guyef0e9ae2009-07-10 14:11:26 -07001514 if (newFocused == null) {
1515 newFocused = this;
1516 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001517
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001518 if (newFocused != currentFocused) {
1519 newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
Romain Guyef0e9ae2009-07-10 14:11:26 -07001520 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001521
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001522 postInvalidateOnAnimation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001523 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001524 }
1525
1526 /**
1527 * {@inheritDoc}
1528 *
1529 * <p>This version also clamps the scrolling to the bounds of our child.
1530 */
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001531 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001532 public void scrollTo(int x, int y) {
1533 // we rely on the fact the View.scrollBy calls scrollTo.
1534 if (getChildCount() > 0) {
1535 View child = getChildAt(0);
1536 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1537 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1538 if (x != mScrollX || y != mScrollY) {
1539 super.scrollTo(x, y);
1540 }
1541 }
1542 }
1543
Adam Powell637d3372010-08-25 14:37:03 -07001544 @Override
1545 public void setOverScrollMode(int mode) {
1546 if (mode != OVER_SCROLL_NEVER) {
1547 if (mEdgeGlowLeft == null) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001548 Context context = getContext();
Adam Powell89935e42011-08-31 14:26:12 -07001549 mEdgeGlowLeft = new EdgeEffect(context);
1550 mEdgeGlowRight = new EdgeEffect(context);
Adam Powell637d3372010-08-25 14:37:03 -07001551 }
1552 } else {
1553 mEdgeGlowLeft = null;
1554 mEdgeGlowRight = null;
1555 }
1556 super.setOverScrollMode(mode);
1557 }
1558
Romain Guy2243e552011-03-08 11:46:28 -08001559 @SuppressWarnings({"SuspiciousNameCombination"})
Adam Powell637d3372010-08-25 14:37:03 -07001560 @Override
1561 public void draw(Canvas canvas) {
1562 super.draw(canvas);
1563 if (mEdgeGlowLeft != null) {
1564 final int scrollX = mScrollX;
1565 if (!mEdgeGlowLeft.isFinished()) {
1566 final int restoreCount = canvas.save();
Adam Powell7d863782011-02-15 15:05:03 -08001567 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001568
1569 canvas.rotate(270);
Adam Powell7d863782011-02-15 15:05:03 -08001570 canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
1571 mEdgeGlowLeft.setSize(height, getWidth());
Adam Powell637d3372010-08-25 14:37:03 -07001572 if (mEdgeGlowLeft.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001573 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001574 }
1575 canvas.restoreToCount(restoreCount);
1576 }
1577 if (!mEdgeGlowRight.isFinished()) {
1578 final int restoreCount = canvas.save();
1579 final int width = getWidth();
Adam Powell7d863782011-02-15 15:05:03 -08001580 final int height = getHeight() - mPaddingTop - mPaddingBottom;
Adam Powell637d3372010-08-25 14:37:03 -07001581
1582 canvas.rotate(90);
Adam Powell7d863782011-02-15 15:05:03 -08001583 canvas.translate(-mPaddingTop,
Mindy Pereirab1297f72010-12-07 15:06:47 -08001584 -(Math.max(getScrollRange(), scrollX) + width));
1585 mEdgeGlowRight.setSize(height, width);
Adam Powell637d3372010-08-25 14:37:03 -07001586 if (mEdgeGlowRight.draw(canvas)) {
Adam Powelldf3ae4f2012-04-10 18:55:22 -07001587 postInvalidateOnAnimation();
Adam Powell637d3372010-08-25 14:37:03 -07001588 }
1589 canvas.restoreToCount(restoreCount);
1590 }
1591 }
1592 }
1593
Romain Guye979e622012-03-20 13:50:27 -07001594 private static int clamp(int n, int my, int child) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001595 if (my >= child || n < 0) {
1596 return 0;
1597 }
1598 if ((my + n) > child) {
1599 return child - my;
1600 }
1601 return n;
1602 }
1603}