blob: 25dd438c4693a0143fbe0bb60dd4849f3af539ca [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 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 Powell17dfce12010-01-25 18:38:22 -080019import com.android.internal.R;
20
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.content.Context;
22import android.content.res.TypedArray;
Adam Powell637d3372010-08-25 14:37:03 -070023import android.graphics.Canvas;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024import android.graphics.Rect;
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -080025import android.os.StrictMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.util.AttributeSet;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.view.FocusFinder;
Jeff Brown33bbfd22011-02-24 20:55:35 -080028import android.view.InputDevice;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.view.KeyEvent;
30import android.view.MotionEvent;
31import android.view.VelocityTracker;
32import android.view.View;
33import android.view.ViewConfiguration;
Gilles Debunne2ed2eac2011-02-24 16:29:48 -080034import android.view.ViewDebug;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035import android.view.ViewGroup;
36import android.view.ViewParent;
Svetoslav Ganova0156172011-06-26 17:55:44 -070037import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039import android.view.animation.AnimationUtils;
40
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041import java.util.List;
42
43/**
44 * Layout container for a view hierarchy that can be scrolled by the user,
45 * allowing it to be larger than the physical display. A ScrollView
46 * is a {@link FrameLayout}, meaning you should place one child in it
47 * containing the entire contents to scroll; this child may itself be a layout
48 * manager with a complex hierarchy of objects. A child that is often used
49 * is a {@link LinearLayout} in a vertical orientation, presenting a vertical
50 * array of top-level items that the user can scroll through.
51 *
52 * <p>The {@link TextView} class also
53 * takes care of its own scrolling, so does not require a ScrollView, but
54 * using the two together is possible to achieve the effect of a text view
55 * within a larger container.
Mindy Pereira4e30d892010-11-24 15:32:39 -080056 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080057 * <p>ScrollView only supports vertical scrolling.
Romain Guyfdbf4842010-08-16 10:55:49 -070058 *
59 * @attr ref android.R.styleable#ScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060 */
61public class ScrollView extends FrameLayout {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062 static final int ANIMATED_SCROLL_GAP = 250;
63
64 static final float MAX_SCROLL_FACTOR = 0.5f;
65
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066 private long mLastScroll;
67
68 private final Rect mTempRect = new Rect();
Adam Powell637d3372010-08-25 14:37:03 -070069 private OverScroller mScroller;
Adam Powell89935e42011-08-31 14:26:12 -070070 private EdgeEffect mEdgeGlowTop;
71 private EdgeEffect mEdgeGlowBottom;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080072
73 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080074 * Position of the last motion event.
75 */
76 private float mLastMotionY;
77
78 /**
79 * True when the layout has changed but the traversal has not come through yet.
80 * Ideally the view hierarchy would keep track of this for us.
81 */
82 private boolean mIsLayoutDirty = true;
83
84 /**
85 * The child to give focus to in the event that a child has requested focus while the
86 * layout is dirty. This prevents the scroll from being wrong if the child has not been
87 * laid out before requesting focus.
88 */
89 private View mChildToScrollTo = null;
90
91 /**
92 * True if the user is currently dragging this ScrollView around. This is
93 * not the same as 'is being flinged', which can be checked by
94 * mScroller.isFinished() (flinging begins when the user lifts his finger).
95 */
96 private boolean mIsBeingDragged = false;
97
98 /**
99 * Determines speed during touch scrolling
100 */
101 private VelocityTracker mVelocityTracker;
102
103 /**
104 * When set to true, the scroll view measure its child to make it fill the currently
105 * visible area.
106 */
Romain Guya174d7a2011-01-07 13:27:39 -0800107 @ViewDebug.ExportedProperty(category = "layout")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800108 private boolean mFillViewport;
109
110 /**
111 * Whether arrow scrolling is animated.
112 */
113 private boolean mSmoothScrollingEnabled = true;
114
115 private int mTouchSlop;
Romain Guy4296fc42009-07-06 11:48:52 -0700116 private int mMinimumVelocity;
117 private int mMaximumVelocity;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800118
Adam Powell637d3372010-08-25 14:37:03 -0700119 private int mOverscrollDistance;
120 private int mOverflingDistance;
121
Adam Powellbc4e7532010-02-23 14:49:01 -0800122 /**
123 * ID of the active pointer. This is used to retain consistency during
124 * drags/flings if multiple pointers are used.
125 */
126 private int mActivePointerId = INVALID_POINTER;
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -0800127
128 /**
129 * The StrictMode "critical time span" objects to catch animation
130 * stutters. Non-null when a time-sensitive animation is
131 * in-flight. Must call finish() on them when done animating.
132 * These are no-ops on user builds.
133 */
134 private StrictMode.Span mScrollStrictSpan = null; // aka "drag"
135 private StrictMode.Span mFlingStrictSpan = null;
136
Adam Powellbc4e7532010-02-23 14:49:01 -0800137 /**
138 * Sentinel value for no current active pointer.
139 * Used by {@link #mActivePointerId}.
140 */
141 private static final int INVALID_POINTER = -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800142
143 public ScrollView(Context context) {
144 this(context, null);
145 }
146
147 public ScrollView(Context context, AttributeSet attrs) {
148 this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
149 }
150
151 public ScrollView(Context context, AttributeSet attrs, int defStyle) {
152 super(context, attrs, defStyle);
153 initScrollView();
154
155 TypedArray a =
156 context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ScrollView, defStyle, 0);
157
158 setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
159
160 a.recycle();
161 }
162
163 @Override
Patrick Dubroye0a799a2011-05-04 16:19:22 -0700164 public boolean shouldDelayChildPressedState() {
165 return true;
166 }
167
168 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800169 protected float getTopFadingEdgeStrength() {
170 if (getChildCount() == 0) {
171 return 0.0f;
172 }
173
174 final int length = getVerticalFadingEdgeLength();
175 if (mScrollY < length) {
176 return mScrollY / (float) length;
177 }
178
179 return 1.0f;
180 }
181
182 @Override
183 protected float getBottomFadingEdgeStrength() {
184 if (getChildCount() == 0) {
185 return 0.0f;
186 }
187
188 final int length = getVerticalFadingEdgeLength();
189 final int bottomEdge = getHeight() - mPaddingBottom;
190 final int span = getChildAt(0).getBottom() - mScrollY - bottomEdge;
191 if (span < length) {
192 return span / (float) length;
193 }
194
195 return 1.0f;
196 }
197
198 /**
199 * @return The maximum amount this scroll view will scroll in response to
200 * an arrow event.
201 */
202 public int getMaxScrollAmount() {
203 return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
204 }
205
206
207 private void initScrollView() {
Adam Powell637d3372010-08-25 14:37:03 -0700208 mScroller = new OverScroller(getContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800209 setFocusable(true);
210 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
211 setWillNotDraw(false);
Romain Guy4296fc42009-07-06 11:48:52 -0700212 final ViewConfiguration configuration = ViewConfiguration.get(mContext);
213 mTouchSlop = configuration.getScaledTouchSlop();
214 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
215 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
Adam Powell637d3372010-08-25 14:37:03 -0700216 mOverscrollDistance = configuration.getScaledOverscrollDistance();
217 mOverflingDistance = configuration.getScaledOverflingDistance();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800218 }
219
220 @Override
221 public void addView(View child) {
222 if (getChildCount() > 0) {
223 throw new IllegalStateException("ScrollView can host only one direct child");
224 }
225
226 super.addView(child);
227 }
228
229 @Override
230 public void addView(View child, int index) {
231 if (getChildCount() > 0) {
232 throw new IllegalStateException("ScrollView can host only one direct child");
233 }
234
235 super.addView(child, index);
236 }
237
238 @Override
239 public void addView(View child, ViewGroup.LayoutParams params) {
240 if (getChildCount() > 0) {
241 throw new IllegalStateException("ScrollView can host only one direct child");
242 }
243
244 super.addView(child, params);
245 }
246
247 @Override
248 public void addView(View child, int index, ViewGroup.LayoutParams params) {
249 if (getChildCount() > 0) {
250 throw new IllegalStateException("ScrollView can host only one direct child");
251 }
252
253 super.addView(child, index, params);
254 }
255
256 /**
257 * @return Returns true this ScrollView can be scrolled
258 */
259 private boolean canScroll() {
260 View child = getChildAt(0);
261 if (child != null) {
262 int childHeight = child.getHeight();
263 return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
264 }
265 return false;
266 }
267
268 /**
269 * Indicates whether this ScrollView's content is stretched to fill the viewport.
270 *
271 * @return True if the content fills the viewport, false otherwise.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800272 *
Romain Guyfdbf4842010-08-16 10:55:49 -0700273 * @attr ref android.R.styleable#ScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800274 */
275 public boolean isFillViewport() {
276 return mFillViewport;
277 }
278
279 /**
280 * Indicates this ScrollView whether it should stretch its content height to fill
281 * the viewport or not.
282 *
283 * @param fillViewport True to stretch the content's height to the viewport's
284 * boundaries, false otherwise.
Mindy Pereira4e30d892010-11-24 15:32:39 -0800285 *
Romain Guyfdbf4842010-08-16 10:55:49 -0700286 * @attr ref android.R.styleable#ScrollView_fillViewport
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 */
288 public void setFillViewport(boolean fillViewport) {
289 if (fillViewport != mFillViewport) {
290 mFillViewport = fillViewport;
291 requestLayout();
292 }
293 }
294
295 /**
296 * @return Whether arrow scrolling will animate its transition.
297 */
298 public boolean isSmoothScrollingEnabled() {
299 return mSmoothScrollingEnabled;
300 }
301
302 /**
303 * Set whether arrow scrolling will animate its transition.
304 * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
305 */
306 public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
307 mSmoothScrollingEnabled = smoothScrollingEnabled;
308 }
309
310 @Override
311 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
312 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
313
314 if (!mFillViewport) {
315 return;
316 }
317
318 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
319 if (heightMode == MeasureSpec.UNSPECIFIED) {
320 return;
321 }
322
Romain Guyef0e9ae2009-07-10 14:11:26 -0700323 if (getChildCount() > 0) {
324 final View child = getChildAt(0);
325 int height = getMeasuredHeight();
326 if (child.getMeasuredHeight() < height) {
327 final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
Mindy Pereira4e30d892010-11-24 15:32:39 -0800328
Romain Guy9c957372011-01-04 17:39:43 -0800329 int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
330 mPaddingLeft + mPaddingRight, lp.width);
Romain Guyef0e9ae2009-07-10 14:11:26 -0700331 height -= mPaddingTop;
332 height -= mPaddingBottom;
333 int childHeightMeasureSpec =
334 MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
Mindy Pereira4e30d892010-11-24 15:32:39 -0800335
Romain Guyef0e9ae2009-07-10 14:11:26 -0700336 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
337 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800338 }
339 }
340
341 @Override
342 public boolean dispatchKeyEvent(KeyEvent event) {
343 // Let the focused view and/or our descendants get the key first
Romain Guy8e618e52010-03-08 12:18:20 -0800344 return super.dispatchKeyEvent(event) || executeKeyEvent(event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800345 }
346
347 /**
348 * You can call this function yourself to have the scroll view perform
349 * scrolling from a key event, just as if the event had been dispatched to
350 * it by the view hierarchy.
351 *
352 * @param event The key event to execute.
353 * @return Return true if the event was handled, else false.
354 */
355 public boolean executeKeyEvent(KeyEvent event) {
356 mTempRect.setEmpty();
357
358 if (!canScroll()) {
Romain Guy2d4cff62010-04-09 15:39:00 -0700359 if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800360 View currentFocused = findFocus();
361 if (currentFocused == this) currentFocused = null;
362 View nextFocused = FocusFinder.getInstance().findNextFocus(this,
363 currentFocused, View.FOCUS_DOWN);
364 return nextFocused != null
365 && nextFocused != this
366 && nextFocused.requestFocus(View.FOCUS_DOWN);
367 }
368 return false;
369 }
370
371 boolean handled = false;
372 if (event.getAction() == KeyEvent.ACTION_DOWN) {
373 switch (event.getKeyCode()) {
374 case KeyEvent.KEYCODE_DPAD_UP:
375 if (!event.isAltPressed()) {
376 handled = arrowScroll(View.FOCUS_UP);
377 } else {
378 handled = fullScroll(View.FOCUS_UP);
379 }
380 break;
381 case KeyEvent.KEYCODE_DPAD_DOWN:
382 if (!event.isAltPressed()) {
383 handled = arrowScroll(View.FOCUS_DOWN);
384 } else {
385 handled = fullScroll(View.FOCUS_DOWN);
386 }
387 break;
388 case KeyEvent.KEYCODE_SPACE:
389 pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
390 break;
391 }
392 }
393
394 return handled;
395 }
396
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800397 private boolean inChild(int x, int y) {
398 if (getChildCount() > 0) {
Adam Powell352b9782010-03-24 14:23:43 -0700399 final int scrollY = mScrollY;
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800400 final View child = getChildAt(0);
Adam Powell352b9782010-03-24 14:23:43 -0700401 return !(y < child.getTop() - scrollY
402 || y >= child.getBottom() - scrollY
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800403 || x < child.getLeft()
404 || x >= child.getRight());
405 }
406 return false;
407 }
408
Michael Jurka13451a42011-08-22 15:54:21 -0700409 private void initOrResetVelocityTracker() {
410 if (mVelocityTracker == null) {
411 mVelocityTracker = VelocityTracker.obtain();
412 } else {
413 mVelocityTracker.clear();
414 }
415 }
416
417 private void initVelocityTrackerIfNotExists() {
418 if (mVelocityTracker == null) {
419 mVelocityTracker = VelocityTracker.obtain();
420 }
421 }
422
423 private void recycleVelocityTracker() {
424 if (mVelocityTracker != null) {
425 mVelocityTracker.recycle();
426 mVelocityTracker = null;
427 }
428 }
429
430 @Override
431 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
432 if (disallowIntercept) {
433 recycleVelocityTracker();
434 }
435 super.requestDisallowInterceptTouchEvent(disallowIntercept);
436 }
437
438
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800439 @Override
440 public boolean onInterceptTouchEvent(MotionEvent ev) {
441 /*
442 * This method JUST determines whether we want to intercept the motion.
443 * If we return true, onMotionEvent will be called and we do the actual
444 * scrolling there.
445 */
446
447 /*
448 * Shortcut the most recurring case: the user is in the dragging
449 * state and he is moving his finger. We want to intercept this
450 * motion.
451 */
452 final int action = ev.getAction();
453 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
454 return true;
455 }
456
Adam Powellbc4e7532010-02-23 14:49:01 -0800457 switch (action & MotionEvent.ACTION_MASK) {
458 case MotionEvent.ACTION_MOVE: {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800459 /*
460 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
461 * whether the user has moved far enough from his original down touch.
462 */
463
464 /*
465 * Locally do absolute value. mLastMotionY is set to the y value
466 * of the down event.
467 */
Adam Powell9d0335b2010-03-24 13:42:51 -0700468 final int activePointerId = mActivePointerId;
469 if (activePointerId == INVALID_POINTER) {
470 // If we don't have a valid id, the touch down wasn't on content.
471 break;
472 }
473
474 final int pointerIndex = ev.findPointerIndex(activePointerId);
Adam Powellbc4e7532010-02-23 14:49:01 -0800475 final float y = ev.getY(pointerIndex);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800476 final int yDiff = (int) Math.abs(y - mLastMotionY);
477 if (yDiff > mTouchSlop) {
478 mIsBeingDragged = true;
Romain Guyf7b4acc2009-12-01 16:24:45 -0800479 mLastMotionY = y;
Michael Jurka13451a42011-08-22 15:54:21 -0700480 initVelocityTrackerIfNotExists();
481 mVelocityTracker.addMovement(ev);
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -0800482 if (mScrollStrictSpan == null) {
483 mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
484 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800485 }
486 break;
Adam Powellbc4e7532010-02-23 14:49:01 -0800487 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800488
Adam Powellbc4e7532010-02-23 14:49:01 -0800489 case MotionEvent.ACTION_DOWN: {
490 final float y = ev.getY();
Adam Powell4cd47702010-02-25 11:21:14 -0800491 if (!inChild((int) ev.getX(), (int) y)) {
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800492 mIsBeingDragged = false;
Michael Jurka13451a42011-08-22 15:54:21 -0700493 recycleVelocityTracker();
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800494 break;
495 }
496
Adam Powellbc4e7532010-02-23 14:49:01 -0800497 /*
498 * Remember location of down touch.
499 * ACTION_DOWN always refers to pointer index 0.
500 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800501 mLastMotionY = y;
Adam Powellbc4e7532010-02-23 14:49:01 -0800502 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503
Michael Jurka13451a42011-08-22 15:54:21 -0700504 initOrResetVelocityTracker();
505 mVelocityTracker.addMovement(ev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800506 /*
507 * If being flinged and user touches the screen, initiate drag;
508 * otherwise don't. mScroller.isFinished should be false when
509 * being flinged.
510 */
511 mIsBeingDragged = !mScroller.isFinished();
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -0800512 if (mIsBeingDragged && mScrollStrictSpan == null) {
513 mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
514 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800515 break;
Adam Powellbc4e7532010-02-23 14:49:01 -0800516 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800517
518 case MotionEvent.ACTION_CANCEL:
519 case MotionEvent.ACTION_UP:
520 /* Release the drag */
521 mIsBeingDragged = false;
Adam Powellbc4e7532010-02-23 14:49:01 -0800522 mActivePointerId = INVALID_POINTER;
Michael Jurka13451a42011-08-22 15:54:21 -0700523 recycleVelocityTracker();
Adam Powell637d3372010-08-25 14:37:03 -0700524 if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
525 invalidate();
526 }
Adam Powellbc4e7532010-02-23 14:49:01 -0800527 break;
528 case MotionEvent.ACTION_POINTER_UP:
529 onSecondaryPointerUp(ev);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800530 break;
531 }
532
533 /*
534 * The only time we want to intercept motion events is if we are in the
535 * drag mode.
536 */
537 return mIsBeingDragged;
538 }
539
540 @Override
541 public boolean onTouchEvent(MotionEvent ev) {
Michael Jurka13451a42011-08-22 15:54:21 -0700542 initVelocityTrackerIfNotExists();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800543 mVelocityTracker.addMovement(ev);
544
545 final int action = ev.getAction();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800546
Adam Powellbc4e7532010-02-23 14:49:01 -0800547 switch (action & MotionEvent.ACTION_MASK) {
548 case MotionEvent.ACTION_DOWN: {
Jeff Brownfb757382011-01-18 18:42:33 -0800549 mIsBeingDragged = getChildCount() != 0;
550 if (!mIsBeingDragged) {
551 return false;
552 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800553
Adam Powell352b9782010-03-24 14:23:43 -0700554 /*
555 * If being flinged and user touches, stop the fling. isFinished
556 * will be false if being flinged.
557 */
558 if (!mScroller.isFinished()) {
559 mScroller.abortAnimation();
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -0800560 if (mFlingStrictSpan != null) {
561 mFlingStrictSpan.finish();
562 mFlingStrictSpan = null;
563 }
Adam Powell352b9782010-03-24 14:23:43 -0700564 }
565
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800566 // Remember where the motion event started
Jeff Brownfb757382011-01-18 18:42:33 -0800567 mLastMotionY = ev.getY();
Adam Powellbc4e7532010-02-23 14:49:01 -0800568 mActivePointerId = ev.getPointerId(0);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800569 break;
Adam Powellbc4e7532010-02-23 14:49:01 -0800570 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800571 case MotionEvent.ACTION_MOVE:
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800572 if (mIsBeingDragged) {
573 // Scroll to follow the motion event
Adam Powellbc4e7532010-02-23 14:49:01 -0800574 final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
575 final float y = ev.getY(activePointerIndex);
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800576 final int deltaY = (int) (mLastMotionY - y);
577 mLastMotionY = y;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800578
Adam Powell637d3372010-08-25 14:37:03 -0700579 final int oldX = mScrollX;
580 final int oldY = mScrollY;
581 final int range = getScrollRange();
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700582 final int overscrollMode = getOverScrollMode();
583 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
584 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
585
Adam Powellf6a6c972011-09-28 23:30:20 -0700586 if (overScrollBy(0, deltaY, 0, mScrollY,
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700587 0, range, 0, mOverscrollDistance, true)) {
Adam Powell637d3372010-08-25 14:37:03 -0700588 // Break our velocity if we hit a scroll barrier.
589 mVelocityTracker.clear();
590 }
591 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
592
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -0700593 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -0700594 final int pulledToY = oldY + deltaY;
595 if (pulledToY < 0) {
596 mEdgeGlowTop.onPull((float) deltaY / getHeight());
597 if (!mEdgeGlowBottom.isFinished()) {
598 mEdgeGlowBottom.onRelease();
599 }
600 } else if (pulledToY > range) {
601 mEdgeGlowBottom.onPull((float) deltaY / getHeight());
602 if (!mEdgeGlowTop.isFinished()) {
603 mEdgeGlowTop.onRelease();
604 }
605 }
606 if (mEdgeGlowTop != null
607 && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
608 invalidate();
609 }
610 }
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800611 }
Adam Powellbc4e7532010-02-23 14:49:01 -0800612 break;
Mindy Pereira4e30d892010-11-24 15:32:39 -0800613 case MotionEvent.ACTION_UP:
Joe Onorato9f0e8ee2010-02-23 19:19:22 -0800614 if (mIsBeingDragged) {
615 final VelocityTracker velocityTracker = mVelocityTracker;
616 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
Adam Powellbc4e7532010-02-23 14:49:01 -0800617 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800618
Adam Powellf6a6c972011-09-28 23:30:20 -0700619 if (getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700620 if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
621 fling(-initialVelocity);
622 } else {
Adam Powellf6a6c972011-09-28 23:30:20 -0700623 if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
624 getScrollRange())) {
Adam Powell637d3372010-08-25 14:37:03 -0700625 invalidate();
626 }
627 }
Adam Powell17dfce12010-01-25 18:38:22 -0800628 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800629
Adam Powellbc4e7532010-02-23 14:49:01 -0800630 mActivePointerId = INVALID_POINTER;
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -0800631 endDrag();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800632 }
Adam Powellbc4e7532010-02-23 14:49:01 -0800633 break;
Adam Powell352b9782010-03-24 14:23:43 -0700634 case MotionEvent.ACTION_CANCEL:
635 if (mIsBeingDragged && getChildCount() > 0) {
Adam Powell637d3372010-08-25 14:37:03 -0700636 if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
637 invalidate();
638 }
Adam Powell352b9782010-03-24 14:23:43 -0700639 mActivePointerId = INVALID_POINTER;
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -0800640 endDrag();
Adam Powell352b9782010-03-24 14:23:43 -0700641 }
642 break;
Adam Powell9bc30d32011-02-28 10:27:49 -0800643 case MotionEvent.ACTION_POINTER_DOWN: {
644 final int index = ev.getActionIndex();
645 final float y = ev.getY(index);
646 mLastMotionY = y;
647 mActivePointerId = ev.getPointerId(index);
648 break;
649 }
Adam Powellbc4e7532010-02-23 14:49:01 -0800650 case MotionEvent.ACTION_POINTER_UP:
651 onSecondaryPointerUp(ev);
Adam Powell9bc30d32011-02-28 10:27:49 -0800652 mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId));
Adam Powellbc4e7532010-02-23 14:49:01 -0800653 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800654 }
655 return true;
656 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800657
Adam Powellbc4e7532010-02-23 14:49:01 -0800658 private void onSecondaryPointerUp(MotionEvent ev) {
659 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
660 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
661 final int pointerId = ev.getPointerId(pointerIndex);
662 if (pointerId == mActivePointerId) {
663 // This was our active pointer going up. Choose a new
664 // active pointer and adjust accordingly.
665 // TODO: Make this decision more intelligent.
666 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
667 mLastMotionY = ev.getY(newPointerIndex);
668 mActivePointerId = ev.getPointerId(newPointerIndex);
669 if (mVelocityTracker != null) {
670 mVelocityTracker.clear();
671 }
672 }
673 }
Mindy Pereira4e30d892010-11-24 15:32:39 -0800674
Adam Powell637d3372010-08-25 14:37:03 -0700675 @Override
Jeff Brown33bbfd22011-02-24 20:55:35 -0800676 public boolean onGenericMotionEvent(MotionEvent event) {
677 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
678 switch (event.getAction()) {
679 case MotionEvent.ACTION_SCROLL: {
680 if (!mIsBeingDragged) {
681 final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
682 if (vscroll != 0) {
683 final int delta = (int) (vscroll * getVerticalScrollFactor());
684 final int range = getScrollRange();
685 int oldScrollY = mScrollY;
686 int newScrollY = oldScrollY - delta;
687 if (newScrollY < 0) {
688 newScrollY = 0;
689 } else if (newScrollY > range) {
690 newScrollY = range;
691 }
692 if (newScrollY != oldScrollY) {
693 super.scrollTo(mScrollX, newScrollY);
694 return true;
695 }
696 }
697 }
698 }
699 }
700 }
701 return super.onGenericMotionEvent(event);
702 }
703
704 @Override
Adam Powell637d3372010-08-25 14:37:03 -0700705 protected void onOverScrolled(int scrollX, int scrollY,
706 boolean clampedX, boolean clampedY) {
707 // Treat animating scrolls differently; see #computeScroll() for why.
708 if (!mScroller.isFinished()) {
709 mScrollX = scrollX;
710 mScrollY = scrollY;
Romain Guy0fd89bf2011-01-26 15:41:30 -0800711 invalidateParentIfNeeded();
Adam Powell637d3372010-08-25 14:37:03 -0700712 if (clampedY) {
713 mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
714 }
715 } else {
716 super.scrollTo(scrollX, scrollY);
717 }
718 awakenScrollBars();
719 }
720
Svetoslav Ganova0156172011-06-26 17:55:44 -0700721 @Override
722 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
723 super.onInitializeAccessibilityNodeInfo(info);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800724 info.setClassName(ScrollView.class.getName());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700725 info.setScrollable(getScrollRange() > 0);
Svetoslav Ganova0156172011-06-26 17:55:44 -0700726 }
727
728 @Override
729 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
730 super.onInitializeAccessibilityEvent(event);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800731 event.setClassName(ScrollView.class.getName());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700732 final boolean scrollable = getScrollRange() > 0;
733 event.setScrollable(scrollable);
734 event.setScrollX(mScrollX);
735 event.setScrollY(mScrollY);
736 event.setMaxScrollX(mScrollX);
737 event.setMaxScrollY(getScrollRange());
Svetoslav Ganova0156172011-06-26 17:55:44 -0700738 }
739
Adam Powell637d3372010-08-25 14:37:03 -0700740 private int getScrollRange() {
741 int scrollRange = 0;
742 if (getChildCount() > 0) {
743 View child = getChildAt(0);
744 scrollRange = Math.max(0,
745 child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
746 }
747 return scrollRange;
748 }
749
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800750 /**
751 * <p>
752 * Finds the next focusable component that fits in this View's bounds
753 * (excluding fading edges) pretending that this View's top is located at
754 * the parameter top.
755 * </p>
756 *
Gilles Debunne2ed2eac2011-02-24 16:29:48 -0800757 * @param topFocus look for a candidate at the top of the bounds if topFocus is true,
758 * or at the bottom of the bounds if topFocus is false
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800759 * @param top the top offset of the bounds in which a focusable must be
760 * found (the fading edge is assumed to start at this position)
761 * @param preferredFocusable the View that has highest priority and will be
762 * returned if it is within my bounds (null is valid)
Gilles Debunne2ed2eac2011-02-24 16:29:48 -0800763 * @return the next focusable component in the bounds or null if none can be found
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800764 */
765 private View findFocusableViewInMyBounds(final boolean topFocus,
766 final int top, View preferredFocusable) {
767 /*
768 * The fading edge's transparent side should be considered for focus
769 * since it's mostly visible, so we divide the actual fading edge length
770 * by 2.
771 */
772 final int fadingEdgeLength = getVerticalFadingEdgeLength() / 2;
773 final int topWithoutFadingEdge = top + fadingEdgeLength;
774 final int bottomWithoutFadingEdge = top + getHeight() - fadingEdgeLength;
775
776 if ((preferredFocusable != null)
777 && (preferredFocusable.getTop() < bottomWithoutFadingEdge)
778 && (preferredFocusable.getBottom() > topWithoutFadingEdge)) {
779 return preferredFocusable;
780 }
781
782 return findFocusableViewInBounds(topFocus, topWithoutFadingEdge,
783 bottomWithoutFadingEdge);
784 }
785
786 /**
787 * <p>
788 * Finds the next focusable component that fits in the specified bounds.
789 * </p>
790 *
791 * @param topFocus look for a candidate is the one at the top of the bounds
792 * if topFocus is true, or at the bottom of the bounds if topFocus is
793 * false
794 * @param top the top offset of the bounds in which a focusable must be
795 * found
796 * @param bottom the bottom offset of the bounds in which a focusable must
797 * be found
798 * @return the next focusable component in the bounds or null if none can
799 * be found
800 */
801 private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) {
802
803 List<View> focusables = getFocusables(View.FOCUS_FORWARD);
804 View focusCandidate = null;
805
806 /*
807 * A fully contained focusable is one where its top is below the bound's
808 * top, and its bottom is above the bound's bottom. A partially
809 * contained focusable is one where some part of it is within the
810 * bounds, but it also has some part that is not within bounds. A fully contained
811 * focusable is preferred to a partially contained focusable.
812 */
813 boolean foundFullyContainedFocusable = false;
814
815 int count = focusables.size();
816 for (int i = 0; i < count; i++) {
817 View view = focusables.get(i);
818 int viewTop = view.getTop();
819 int viewBottom = view.getBottom();
820
821 if (top < viewBottom && viewTop < bottom) {
822 /*
823 * the focusable is in the target area, it is a candidate for
824 * focusing
825 */
826
827 final boolean viewIsFullyContained = (top < viewTop) &&
828 (viewBottom < bottom);
829
830 if (focusCandidate == null) {
831 /* No candidate, take this one */
832 focusCandidate = view;
833 foundFullyContainedFocusable = viewIsFullyContained;
834 } else {
835 final boolean viewIsCloserToBoundary =
836 (topFocus && viewTop < focusCandidate.getTop()) ||
837 (!topFocus && viewBottom > focusCandidate
838 .getBottom());
839
840 if (foundFullyContainedFocusable) {
841 if (viewIsFullyContained && viewIsCloserToBoundary) {
842 /*
843 * We're dealing with only fully contained views, so
844 * it has to be closer to the boundary to beat our
845 * candidate
846 */
847 focusCandidate = view;
848 }
849 } else {
850 if (viewIsFullyContained) {
851 /* Any fully contained view beats a partially contained view */
852 focusCandidate = view;
853 foundFullyContainedFocusable = true;
854 } else if (viewIsCloserToBoundary) {
855 /*
856 * Partially contained view beats another partially
857 * contained view if it's closer
858 */
859 focusCandidate = view;
860 }
861 }
862 }
863 }
864 }
865
866 return focusCandidate;
867 }
868
869 /**
870 * <p>Handles scrolling in response to a "page up/down" shortcut press. This
871 * method will scroll the view by one page up or down and give the focus
872 * to the topmost/bottommost component in the new visible area. If no
873 * component is a good candidate for focus, this scrollview reclaims the
874 * focus.</p>
875 *
876 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
877 * to go one page up or
878 * {@link android.view.View#FOCUS_DOWN} to go one page down
879 * @return true if the key event is consumed by this method, false otherwise
880 */
881 public boolean pageScroll(int direction) {
882 boolean down = direction == View.FOCUS_DOWN;
883 int height = getHeight();
884
885 if (down) {
886 mTempRect.top = getScrollY() + height;
887 int count = getChildCount();
888 if (count > 0) {
889 View view = getChildAt(count - 1);
890 if (mTempRect.top + height > view.getBottom()) {
891 mTempRect.top = view.getBottom() - height;
892 }
893 }
894 } else {
895 mTempRect.top = getScrollY() - height;
896 if (mTempRect.top < 0) {
897 mTempRect.top = 0;
898 }
899 }
900 mTempRect.bottom = mTempRect.top + height;
901
902 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
903 }
904
905 /**
906 * <p>Handles scrolling in response to a "home/end" shortcut press. This
907 * method will scroll the view to the top or bottom and give the focus
908 * to the topmost/bottommost component in the new visible area. If no
909 * component is a good candidate for focus, this scrollview reclaims the
910 * focus.</p>
911 *
912 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
913 * to go the top of the view or
914 * {@link android.view.View#FOCUS_DOWN} to go the bottom
915 * @return true if the key event is consumed by this method, false otherwise
916 */
917 public boolean fullScroll(int direction) {
918 boolean down = direction == View.FOCUS_DOWN;
919 int height = getHeight();
920
921 mTempRect.top = 0;
922 mTempRect.bottom = height;
923
924 if (down) {
925 int count = getChildCount();
926 if (count > 0) {
927 View view = getChildAt(count - 1);
Mattias Petersson5435a062011-04-07 15:46:56 +0200928 mTempRect.bottom = view.getBottom() + mPaddingBottom;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800929 mTempRect.top = mTempRect.bottom - height;
930 }
931 }
932
933 return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
934 }
935
936 /**
937 * <p>Scrolls the view to make the area defined by <code>top</code> and
938 * <code>bottom</code> visible. This method attempts to give the focus
939 * to a component visible in this area. If no component can be focused in
Gilles Debunne2ed2eac2011-02-24 16:29:48 -0800940 * the new visible area, the focus is reclaimed by this ScrollView.</p>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800941 *
942 * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
Gilles Debunne2ed2eac2011-02-24 16:29:48 -0800943 * to go upward, {@link android.view.View#FOCUS_DOWN} to downward
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800944 * @param top the top offset of the new area to be made visible
945 * @param bottom the bottom offset of the new area to be made visible
946 * @return true if the key event is consumed by this method, false otherwise
947 */
948 private boolean scrollAndFocus(int direction, int top, int bottom) {
949 boolean handled = true;
950
951 int height = getHeight();
952 int containerTop = getScrollY();
953 int containerBottom = containerTop + height;
954 boolean up = direction == View.FOCUS_UP;
955
956 View newFocused = findFocusableViewInBounds(up, top, bottom);
957 if (newFocused == null) {
958 newFocused = this;
959 }
960
961 if (top >= containerTop && bottom <= containerBottom) {
962 handled = false;
963 } else {
964 int delta = up ? (top - containerTop) : (bottom - containerBottom);
965 doScrollY(delta);
966 }
967
Gilles Debunne2ed2eac2011-02-24 16:29:48 -0800968 if (newFocused != findFocus()) newFocused.requestFocus(direction);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800969
970 return handled;
971 }
972
973 /**
974 * Handle scrolling in response to an up or down arrow click.
975 *
976 * @param direction The direction corresponding to the arrow key that was
977 * pressed
978 * @return True if we consumed the event, false otherwise
979 */
980 public boolean arrowScroll(int direction) {
981
982 View currentFocused = findFocus();
983 if (currentFocused == this) currentFocused = null;
984
985 View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
986
987 final int maxJump = getMaxScrollAmount();
988
Jack Veenstra7d4200d2009-09-21 19:45:14 -0700989 if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800990 nextFocused.getDrawingRect(mTempRect);
991 offsetDescendantRectToMyCoords(nextFocused, mTempRect);
992 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
993 doScrollY(scrollDelta);
994 nextFocused.requestFocus(direction);
995 } else {
996 // no new focus
997 int scrollDelta = maxJump;
998
999 if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
1000 scrollDelta = getScrollY();
1001 } else if (direction == View.FOCUS_DOWN) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001002 if (getChildCount() > 0) {
1003 int daBottom = getChildAt(0).getBottom();
Mattias Petersson5435a062011-04-07 15:46:56 +02001004 int screenBottom = getScrollY() + getHeight() - mPaddingBottom;
Romain Guyef0e9ae2009-07-10 14:11:26 -07001005 if (daBottom - screenBottom < maxJump) {
1006 scrollDelta = daBottom - screenBottom;
1007 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001008 }
1009 }
1010 if (scrollDelta == 0) {
1011 return false;
1012 }
1013 doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
1014 }
1015
1016 if (currentFocused != null && currentFocused.isFocused()
1017 && isOffScreen(currentFocused)) {
1018 // previously focused item still has focus and is off screen, give
1019 // it up (take it back to ourselves)
1020 // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
1021 // sure to
1022 // get it)
1023 final int descendantFocusability = getDescendantFocusability(); // save
1024 setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
1025 requestFocus();
1026 setDescendantFocusability(descendantFocusability); // restore
1027 }
1028 return true;
1029 }
1030
1031 /**
1032 * @return whether the descendant of this scroll view is scrolled off
1033 * screen.
1034 */
1035 private boolean isOffScreen(View descendant) {
Jack Veenstra7d4200d2009-09-21 19:45:14 -07001036 return !isWithinDeltaOfScreen(descendant, 0, getHeight());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001037 }
1038
1039 /**
1040 * @return whether the descendant of this scroll view is within delta
1041 * pixels of being on the screen.
1042 */
Jack Veenstra7d4200d2009-09-21 19:45:14 -07001043 private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001044 descendant.getDrawingRect(mTempRect);
1045 offsetDescendantRectToMyCoords(descendant, mTempRect);
1046
1047 return (mTempRect.bottom + delta) >= getScrollY()
Jack Veenstra7d4200d2009-09-21 19:45:14 -07001048 && (mTempRect.top - delta) <= (getScrollY() + height);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001049 }
1050
1051 /**
1052 * Smooth scroll by a Y delta
1053 *
1054 * @param delta the number of pixels to scroll by on the Y axis
1055 */
1056 private void doScrollY(int delta) {
1057 if (delta != 0) {
1058 if (mSmoothScrollingEnabled) {
1059 smoothScrollBy(0, delta);
1060 } else {
1061 scrollBy(0, delta);
1062 }
1063 }
1064 }
1065
1066 /**
1067 * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
1068 *
1069 * @param dx the number of pixels to scroll by on the X axis
1070 * @param dy the number of pixels to scroll by on the Y axis
1071 */
1072 public final void smoothScrollBy(int dx, int dy) {
Adam Powell3fc37372010-01-28 13:52:24 -08001073 if (getChildCount() == 0) {
1074 // Nothing to do.
1075 return;
1076 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001077 long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
1078 if (duration > ANIMATED_SCROLL_GAP) {
Adam Powellf5446052010-01-28 17:24:56 -08001079 final int height = getHeight() - mPaddingBottom - mPaddingTop;
1080 final int bottom = getChildAt(0).getHeight();
1081 final int maxY = Math.max(0, bottom - height);
1082 final int scrollY = mScrollY;
1083 dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
1084
1085 mScroller.startScroll(mScrollX, scrollY, 0, dy);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001086 invalidate();
1087 } else {
1088 if (!mScroller.isFinished()) {
1089 mScroller.abortAnimation();
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -08001090 if (mFlingStrictSpan != null) {
1091 mFlingStrictSpan.finish();
1092 mFlingStrictSpan = null;
1093 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001094 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001095 scrollBy(dx, dy);
1096 }
1097 mLastScroll = AnimationUtils.currentAnimationTimeMillis();
1098 }
1099
1100 /**
1101 * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
1102 *
1103 * @param x the position where to scroll on the X axis
1104 * @param y the position where to scroll on the Y axis
1105 */
1106 public final void smoothScrollTo(int x, int y) {
1107 smoothScrollBy(x - mScrollX, y - mScrollY);
1108 }
1109
1110 /**
1111 * <p>The scroll range of a scroll view is the overall height of all of its
1112 * children.</p>
1113 */
1114 @Override
1115 protected int computeVerticalScrollRange() {
Adam Powella2f91012010-02-16 12:26:33 -08001116 final int count = getChildCount();
1117 final int contentHeight = getHeight() - mPaddingBottom - mPaddingTop;
Adam Powell0b8bb422010-02-08 14:30:45 -08001118 if (count == 0) {
Adam Powella2f91012010-02-16 12:26:33 -08001119 return contentHeight;
Adam Powell0b8bb422010-02-08 14:30:45 -08001120 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001121
Adam Powell637d3372010-08-25 14:37:03 -07001122 int scrollRange = getChildAt(0).getBottom();
1123 final int scrollY = mScrollY;
1124 final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
1125 if (scrollY < 0) {
1126 scrollRange -= scrollY;
1127 } else if (scrollY > overscrollBottom) {
1128 scrollRange += scrollY - overscrollBottom;
1129 }
1130
1131 return scrollRange;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001132 }
1133
Adam Powell0b8bb422010-02-08 14:30:45 -08001134 @Override
1135 protected int computeVerticalScrollOffset() {
1136 return Math.max(0, super.computeVerticalScrollOffset());
1137 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001138
1139 @Override
1140 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
1141 ViewGroup.LayoutParams lp = child.getLayoutParams();
1142
1143 int childWidthMeasureSpec;
1144 int childHeightMeasureSpec;
1145
1146 childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
1147 + mPaddingRight, lp.width);
1148
1149 childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1150
1151 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1152 }
1153
1154 @Override
1155 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
1156 int parentHeightMeasureSpec, int heightUsed) {
1157 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
1158
1159 final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
1160 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
1161 + widthUsed, lp.width);
1162 final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
1163 lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
1164
1165 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
1166 }
1167
1168 @Override
1169 public void computeScroll() {
1170 if (mScroller.computeScrollOffset()) {
1171 // This is called at drawing time by ViewGroup. We don't want to
1172 // re-show the scrollbars at this point, which scrollTo will do,
1173 // so we replicate most of scrollTo here.
1174 //
1175 // It's a little odd to call onScrollChanged from inside the drawing.
1176 //
1177 // It is, except when you remember that computeScroll() is used to
1178 // animate scrolling. So unless we want to defer the onScrollChanged()
1179 // until the end of the animated scrolling, we don't really have a
1180 // choice here.
1181 //
1182 // I agree. The alternative, which I think would be worse, is to post
1183 // something and tell the subclasses later. This is bad because there
1184 // will be a window where mScrollX/Y is different from what the app
1185 // thinks it is.
1186 //
1187 int oldX = mScrollX;
1188 int oldY = mScrollY;
1189 int x = mScroller.getCurrX();
1190 int y = mScroller.getCurrY();
Adam Powell17dfce12010-01-25 18:38:22 -08001191
Adam Powell637d3372010-08-25 14:37:03 -07001192 if (oldX != x || oldY != y) {
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001193 final int range = getScrollRange();
1194 final int overscrollMode = getOverScrollMode();
1195 final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
1196 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
1197
1198 overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
Adam Powell637d3372010-08-25 14:37:03 -07001199 0, mOverflingDistance, false);
1200 onScrollChanged(mScrollX, mScrollY, oldX, oldY);
1201
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001202 if (canOverscroll) {
Adam Powell637d3372010-08-25 14:37:03 -07001203 if (y < 0 && oldY >= 0) {
1204 mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
1205 } else if (y > range && oldY <= range) {
1206 mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
1207 }
Adam Powell9d32d242010-03-29 16:02:07 -07001208 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001209 }
Fabrice Di Meglioe9dbef82011-09-12 16:36:46 -07001210
Adam Powella0d645cb2010-03-31 17:27:49 -07001211 awakenScrollBars();
Adam Powell9d32d242010-03-29 16:02:07 -07001212
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001213 // Keep on drawing until the animation has finished.
1214 postInvalidate();
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -08001215 } else {
1216 if (mFlingStrictSpan != null) {
1217 mFlingStrictSpan.finish();
1218 mFlingStrictSpan = null;
1219 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001220 }
1221 }
1222
1223 /**
1224 * Scrolls the view to the given child.
1225 *
1226 * @param child the View to scroll to
1227 */
1228 private void scrollToChild(View child) {
1229 child.getDrawingRect(mTempRect);
1230
1231 /* Offset from child's local coordinates to ScrollView coordinates */
1232 offsetDescendantRectToMyCoords(child, mTempRect);
1233
1234 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1235
1236 if (scrollDelta != 0) {
1237 scrollBy(0, scrollDelta);
1238 }
1239 }
1240
1241 /**
1242 * If rect is off screen, scroll just enough to get it (or at least the
1243 * first screen size chunk of it) on screen.
1244 *
1245 * @param rect The rectangle.
1246 * @param immediate True to scroll immediately without animation
1247 * @return true if scrolling was performed
1248 */
1249 private boolean scrollToChildRect(Rect rect, boolean immediate) {
1250 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
1251 final boolean scroll = delta != 0;
1252 if (scroll) {
1253 if (immediate) {
1254 scrollBy(0, delta);
1255 } else {
1256 smoothScrollBy(0, delta);
1257 }
1258 }
1259 return scroll;
1260 }
1261
1262 /**
1263 * Compute the amount to scroll in the Y direction in order to get
1264 * a rectangle completely on the screen (or, if taller than the screen,
1265 * at least the first screen size chunk of it).
1266 *
1267 * @param rect The rect.
1268 * @return The scroll delta.
1269 */
1270 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001271 if (getChildCount() == 0) return 0;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001272
1273 int height = getHeight();
1274 int screenTop = getScrollY();
1275 int screenBottom = screenTop + height;
1276
1277 int fadingEdge = getVerticalFadingEdgeLength();
1278
1279 // leave room for top fading edge as long as rect isn't at very top
1280 if (rect.top > 0) {
1281 screenTop += fadingEdge;
1282 }
1283
1284 // leave room for bottom fading edge as long as rect isn't at very bottom
1285 if (rect.bottom < getChildAt(0).getHeight()) {
1286 screenBottom -= fadingEdge;
1287 }
1288
1289 int scrollYDelta = 0;
1290
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001291 if (rect.bottom > screenBottom && rect.top > screenTop) {
1292 // need to move down to get it in view: move down just enough so
1293 // that the entire rectangle is in view (or at least the first
1294 // screen size chunk).
1295
1296 if (rect.height() > height) {
1297 // just enough to get screen size chunk on
1298 scrollYDelta += (rect.top - screenTop);
1299 } else {
1300 // get entire rect at bottom of screen
1301 scrollYDelta += (rect.bottom - screenBottom);
1302 }
1303
1304 // make sure we aren't scrolling beyond the end of our content
Romain Guyef0e9ae2009-07-10 14:11:26 -07001305 int bottom = getChildAt(0).getBottom();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001306 int distanceToBottom = bottom - screenBottom;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001307 scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
1308
1309 } else if (rect.top < screenTop && rect.bottom < screenBottom) {
1310 // need to move up to get it in view: move up just enough so that
1311 // entire rectangle is in view (or at least the first screen
1312 // size chunk of it).
1313
1314 if (rect.height() > height) {
1315 // screen size chunk
1316 scrollYDelta -= (screenBottom - rect.bottom);
1317 } else {
1318 // entire rect at top
1319 scrollYDelta -= (screenTop - rect.top);
1320 }
1321
1322 // make sure we aren't scrolling any further than the top our content
1323 scrollYDelta = Math.max(scrollYDelta, -getScrollY());
1324 }
1325 return scrollYDelta;
1326 }
1327
1328 @Override
1329 public void requestChildFocus(View child, View focused) {
Gilles Debunne2ed2eac2011-02-24 16:29:48 -08001330 if (!mIsLayoutDirty) {
1331 scrollToChild(focused);
1332 } else {
1333 // The child may not be laid out yet, we can't compute the scroll yet
1334 mChildToScrollTo = focused;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001335 }
1336 super.requestChildFocus(child, focused);
1337 }
1338
1339
1340 /**
1341 * When looking for focus in children of a scroll view, need to be a little
1342 * more careful not to give focus to something that is scrolled off screen.
1343 *
1344 * This is more expensive than the default {@link android.view.ViewGroup}
1345 * implementation, otherwise this behavior might have been made the default.
1346 */
1347 @Override
1348 protected boolean onRequestFocusInDescendants(int direction,
1349 Rect previouslyFocusedRect) {
1350
1351 // convert from forward / backward notation to up / down / left / right
1352 // (ugh).
1353 if (direction == View.FOCUS_FORWARD) {
1354 direction = View.FOCUS_DOWN;
1355 } else if (direction == View.FOCUS_BACKWARD) {
1356 direction = View.FOCUS_UP;
1357 }
1358
1359 final View nextFocus = previouslyFocusedRect == null ?
1360 FocusFinder.getInstance().findNextFocus(this, null, direction) :
1361 FocusFinder.getInstance().findNextFocusFromRect(this,
1362 previouslyFocusedRect, direction);
1363
1364 if (nextFocus == null) {
1365 return false;
1366 }
1367
1368 if (isOffScreen(nextFocus)) {
1369 return false;
1370 }
1371
1372 return nextFocus.requestFocus(direction, previouslyFocusedRect);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001373 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001374
1375 @Override
1376 public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
1377 boolean immediate) {
1378 // offset into coordinate space of this scroll view
1379 rectangle.offset(child.getLeft() - child.getScrollX(),
1380 child.getTop() - child.getScrollY());
1381
1382 return scrollToChildRect(rectangle, immediate);
1383 }
1384
1385 @Override
1386 public void requestLayout() {
1387 mIsLayoutDirty = true;
1388 super.requestLayout();
1389 }
1390
1391 @Override
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -08001392 protected void onDetachedFromWindow() {
1393 super.onDetachedFromWindow();
1394
1395 if (mScrollStrictSpan != null) {
1396 mScrollStrictSpan.finish();
1397 mScrollStrictSpan = null;
1398 }
1399 if (mFlingStrictSpan != null) {
1400 mFlingStrictSpan.finish();
1401 mFlingStrictSpan = null;
1402 }
1403 }
1404
1405 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001406 protected void onLayout(boolean changed, int l, int t, int r, int b) {
1407 super.onLayout(changed, l, t, r, b);
1408 mIsLayoutDirty = false;
Mindy Pereira4e30d892010-11-24 15:32:39 -08001409 // Give a child focus if it needs it
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001410 if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
Romain Guy9c957372011-01-04 17:39:43 -08001411 scrollToChild(mChildToScrollTo);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001412 }
1413 mChildToScrollTo = null;
1414
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001415 // Calling this with the present values causes it to re-claim them
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001416 scrollTo(mScrollX, mScrollY);
1417 }
1418
1419 @Override
1420 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1421 super.onSizeChanged(w, h, oldw, oldh);
1422
1423 View currentFocused = findFocus();
1424 if (null == currentFocused || this == currentFocused)
1425 return;
1426
Jack Veenstra7d4200d2009-09-21 19:45:14 -07001427 // If the currently-focused view was visible on the screen when the
1428 // screen was at the old height, then scroll the screen to make that
1429 // view visible with the new screen height.
1430 if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001431 currentFocused.getDrawingRect(mTempRect);
1432 offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1433 int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1434 doScrollY(scrollDelta);
1435 }
Mindy Pereira4e30d892010-11-24 15:32:39 -08001436 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001437
1438 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +09001439 * Return true if child is a descendant of parent, (or equal to the parent).
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001440 */
1441 private boolean isViewDescendantOf(View child, View parent) {
1442 if (child == parent) {
1443 return true;
1444 }
1445
1446 final ViewParent theParent = child.getParent();
1447 return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001448 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001449
1450 /**
1451 * Fling the scroll view
1452 *
1453 * @param velocityY The initial velocity in the Y direction. Positive
Gilles Debunne52964242010-02-24 11:05:19 -08001454 * numbers mean that the finger/cursor is moving down the screen,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001455 * which means we want to scroll towards the top.
1456 */
1457 public void fling(int velocityY) {
Romain Guyef0e9ae2009-07-10 14:11:26 -07001458 if (getChildCount() > 0) {
1459 int height = getHeight() - mPaddingBottom - mPaddingTop;
1460 int bottom = getChildAt(0).getHeight();
Mindy Pereira4e30d892010-11-24 15:32:39 -08001461
1462 mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
Adam Powell637d3372010-08-25 14:37:03 -07001463 Math.max(0, bottom - height), 0, height/2);
Mindy Pereira4e30d892010-11-24 15:32:39 -08001464
Romain Guyef0e9ae2009-07-10 14:11:26 -07001465 final boolean movingDown = velocityY > 0;
Mindy Pereira4e30d892010-11-24 15:32:39 -08001466
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -08001467 if (mFlingStrictSpan == null) {
1468 mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
1469 }
1470
Romain Guyef0e9ae2009-07-10 14:11:26 -07001471 invalidate();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001472 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001473 }
1474
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -08001475 private void endDrag() {
1476 mIsBeingDragged = false;
1477
Michael Jurka13451a42011-08-22 15:54:21 -07001478 recycleVelocityTracker();
Brad Fitzpatrickce81f3a2010-11-21 18:46:08 -08001479
1480 if (mEdgeGlowTop != null) {
1481 mEdgeGlowTop.onRelease();
1482 mEdgeGlowBottom.onRelease();
1483 }
1484
1485 if (mScrollStrictSpan != null) {
1486 mScrollStrictSpan.finish();
1487 mScrollStrictSpan = null;
1488 }
1489 }
1490
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001491 /**
1492 * {@inheritDoc}
1493 *
1494 * <p>This version also clamps the scrolling to the bounds of our child.
1495 */
Gilles Debunne52964242010-02-24 11:05:19 -08001496 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001497 public void scrollTo(int x, int y) {
1498 // we rely on the fact the View.scrollBy calls scrollTo.
1499 if (getChildCount() > 0) {
1500 View child = getChildAt(0);
1501 x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
1502 y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
1503 if (x != mScrollX || y != mScrollY) {
1504 super.scrollTo(x, y);
1505 }
1506 }
1507 }
1508
Adam Powell637d3372010-08-25 14:37:03 -07001509 @Override
1510 public void setOverScrollMode(int mode) {
1511 if (mode != OVER_SCROLL_NEVER) {
1512 if (mEdgeGlowTop == null) {
Mindy Pereira4e30d892010-11-24 15:32:39 -08001513 Context context = getContext();
Adam Powell89935e42011-08-31 14:26:12 -07001514 mEdgeGlowTop = new EdgeEffect(context);
1515 mEdgeGlowBottom = new EdgeEffect(context);
Adam Powell637d3372010-08-25 14:37:03 -07001516 }
1517 } else {
1518 mEdgeGlowTop = null;
1519 mEdgeGlowBottom = null;
1520 }
1521 super.setOverScrollMode(mode);
1522 }
1523
1524 @Override
1525 public void draw(Canvas canvas) {
1526 super.draw(canvas);
1527 if (mEdgeGlowTop != null) {
1528 final int scrollY = mScrollY;
1529 if (!mEdgeGlowTop.isFinished()) {
1530 final int restoreCount = canvas.save();
Adam Powell7d863782011-02-15 15:05:03 -08001531 final int width = getWidth() - mPaddingLeft - mPaddingRight;
Adam Powell637d3372010-08-25 14:37:03 -07001532
Adam Powell7d863782011-02-15 15:05:03 -08001533 canvas.translate(mPaddingLeft, Math.min(0, scrollY));
Mindy Pereirab1297f72010-12-07 15:06:47 -08001534 mEdgeGlowTop.setSize(width, getHeight());
Adam Powell637d3372010-08-25 14:37:03 -07001535 if (mEdgeGlowTop.draw(canvas)) {
1536 invalidate();
1537 }
1538 canvas.restoreToCount(restoreCount);
1539 }
1540 if (!mEdgeGlowBottom.isFinished()) {
1541 final int restoreCount = canvas.save();
Adam Powell7d863782011-02-15 15:05:03 -08001542 final int width = getWidth() - mPaddingLeft - mPaddingRight;
Adam Powell637d3372010-08-25 14:37:03 -07001543 final int height = getHeight();
1544
Adam Powell7d863782011-02-15 15:05:03 -08001545 canvas.translate(-width + mPaddingLeft,
1546 Math.max(getScrollRange(), scrollY) + height);
Adam Powell637d3372010-08-25 14:37:03 -07001547 canvas.rotate(180, width, 0);
Mindy Pereirab1297f72010-12-07 15:06:47 -08001548 mEdgeGlowBottom.setSize(width, height);
Adam Powell637d3372010-08-25 14:37:03 -07001549 if (mEdgeGlowBottom.draw(canvas)) {
1550 invalidate();
1551 }
1552 canvas.restoreToCount(restoreCount);
1553 }
1554 }
1555 }
1556
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001557 private int clamp(int n, int my, int child) {
1558 if (my >= child || n < 0) {
1559 /* my >= child is this case:
1560 * |--------------- me ---------------|
1561 * |------ child ------|
1562 * or
1563 * |--------------- me ---------------|
1564 * |------ child ------|
1565 * or
1566 * |--------------- me ---------------|
1567 * |------ child ------|
1568 *
1569 * n < 0 is this case:
1570 * |------ me ------|
1571 * |-------- child --------|
1572 * |-- mScrollX --|
1573 */
1574 return 0;
1575 }
1576 if ((my+n) > child) {
1577 /* this case:
1578 * |------ me ------|
1579 * |------ child ------|
1580 * |-- mScrollX --|
1581 */
1582 return child-my;
1583 }
1584 return n;
1585 }
1586}