blob: 26805e0b51d453d7bac4d35f07a15623bd91b4fa [file] [log] [blame]
Winson Chung321e9ee2010-08-09 13:37:56 -07001/*
2 * Copyright (C) 2010 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 com.android.launcher2;
18
19import java.util.ArrayList;
20
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.view.MotionEvent;
30import android.view.VelocityTracker;
31import android.view.View;
32import android.view.ViewConfiguration;
33import android.view.ViewGroup;
34import android.view.ViewParent;
35import android.widget.Scroller;
36
37/**
38 * An abstraction of the original Workspace which supports browsing through a
39 * sequential list of "pages" (or PagedViewCellLayouts).
40 */
41public abstract class PagedView extends ViewGroup {
42 private static final String TAG = "PagedView";
43 private static final int INVALID_SCREEN = -1;
44
45 // the velocity at which a fling gesture will cause us to snap to the next screen
46 private static final int SNAP_VELOCITY = 500;
47
48 // the min drag distance for a fling to register, to prevent random screen shifts
49 private static final int MIN_LENGTH_FOR_FLING = 50;
50
51 private boolean mFirstLayout = true;
52
53 private int mCurrentScreen;
54 private int mNextScreen = INVALID_SCREEN;
55 private Scroller mScroller;
56 private VelocityTracker mVelocityTracker;
57
58 private float mDownMotionX;
59 private float mLastMotionX;
60 private float mLastMotionY;
61
62 private final static int TOUCH_STATE_REST = 0;
63 private final static int TOUCH_STATE_SCROLLING = 1;
64 private final static int TOUCH_STATE_PREV_PAGE = 2;
65 private final static int TOUCH_STATE_NEXT_PAGE = 3;
66
67 private int mTouchState = TOUCH_STATE_REST;
68
69 private OnLongClickListener mLongClickListener;
70
71 private boolean mAllowLongPress = true;
72
73 private int mTouchSlop;
74 private int mPagingTouchSlop;
75 private int mMaximumVelocity;
76
77 private static final int INVALID_POINTER = -1;
78
79 private int mActivePointerId = INVALID_POINTER;
80
81 private ScreenSwitchListener mScreenSwitchListener;
82
83 private boolean mDimmedPagesDirty;
84
85 public interface ScreenSwitchListener {
86 void onScreenSwitch(View newScreen, int newScreenIndex);
87 }
88
89 /**
90 * Constructor
91 *
92 * @param context The application's context.
93 */
94 public PagedView(Context context) {
95 this(context, null);
96 }
97
98 public PagedView(Context context, AttributeSet attrs) {
99 this(context, attrs, 0);
100 }
101
102 public PagedView(Context context, AttributeSet attrs, int defStyle) {
103 super(context, attrs, defStyle);
104
105 setHapticFeedbackEnabled(false);
106 initWorkspace();
107 }
108
109 /**
110 * Initializes various states for this workspace.
111 */
112 private void initWorkspace() {
113 mScroller = new Scroller(getContext());
114 mCurrentScreen = 0;
115
116 final ViewConfiguration configuration = ViewConfiguration.get(getContext());
117 mTouchSlop = configuration.getScaledTouchSlop();
118 mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
119 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
120 }
121
122 public void setScreenSwitchListener(ScreenSwitchListener screenSwitchListener) {
123 mScreenSwitchListener = screenSwitchListener;
124 if (mScreenSwitchListener != null) {
125 mScreenSwitchListener.onScreenSwitch(getScreenAt(mCurrentScreen), mCurrentScreen);
126 }
127 }
128
129 /**
130 * Returns the index of the currently displayed screen.
131 *
132 * @return The index of the currently displayed screen.
133 */
134 int getCurrentScreen() {
135 return mCurrentScreen;
136 }
137
138 int getScreenCount() {
139 return getChildCount();
140 }
141
142 View getScreenAt(int index) {
143 return getChildAt(index);
144 }
145
146 int getScrollWidth() {
147 return getWidth();
148 }
149
150 /**
151 * Sets the current screen.
152 *
153 * @param currentScreen
154 */
155 void setCurrentScreen(int currentScreen) {
156 if (!mScroller.isFinished()) mScroller.abortAnimation();
157 if (getChildCount() == 0) return;
158
159 mCurrentScreen = Math.max(0, Math.min(currentScreen, getScreenCount() - 1));
160 scrollTo(getChildOffset(mCurrentScreen) - getRelativeChildOffset(mCurrentScreen), 0);
161 invalidate();
162 notifyScreenSwitchListener();
163 }
164
165 private void notifyScreenSwitchListener() {
166 if (mScreenSwitchListener != null) {
167 mScreenSwitchListener.onScreenSwitch(getScreenAt(mCurrentScreen), mCurrentScreen);
168 }
169 }
170
171 /**
172 * Registers the specified listener on each screen contained in this workspace.
173 *
174 * @param l The listener used to respond to long clicks.
175 */
176 @Override
177 public void setOnLongClickListener(OnLongClickListener l) {
178 mLongClickListener = l;
179 final int count = getScreenCount();
180 for (int i = 0; i < count; i++) {
181 getScreenAt(i).setOnLongClickListener(l);
182 }
183 }
184
185 @Override
186 public void computeScroll() {
187 if (mScroller.computeScrollOffset()) {
188 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
189 postInvalidate();
190 } else if (mNextScreen != INVALID_SCREEN) {
191 mCurrentScreen = Math.max(0, Math.min(mNextScreen, getScreenCount() - 1));
192 notifyScreenSwitchListener();
193 mNextScreen = INVALID_SCREEN;
194 }
195 }
196
197 @Override
198 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
199 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
200 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
201 if (widthMode != MeasureSpec.EXACTLY) {
202 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
203 }
204
205 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
206 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
207 if (heightMode != MeasureSpec.EXACTLY) {
208 throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
209 }
210
211 // The children are given the same width and height as the workspace
212 final int childCount = getChildCount();
213 for (int i = 0; i < childCount; i++) {
214 getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
215 }
216
217 setMeasuredDimension(widthSize, heightSize);
218
219 if (mFirstLayout) {
220 setHorizontalScrollBarEnabled(false);
221 scrollTo(mCurrentScreen * widthSize, 0);
222 setHorizontalScrollBarEnabled(true);
223 mFirstLayout = false;
224 }
225 }
226
227 @Override
228 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
229 final int childCount = getChildCount();
230 int childLeft = 0;
231 if (childCount > 0) {
232 childLeft = (getMeasuredWidth() - getChildAt(0).getMeasuredWidth()) / 2;
233 }
234
235 for (int i = 0; i < childCount; i++) {
236 final View child = getChildAt(i);
237 if (child.getVisibility() != View.GONE) {
238 final int childWidth = child.getMeasuredWidth();
239 child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
240 childLeft += childWidth;
241 }
242 }
243 }
244
245 protected void invalidateDimmedPages() {
246 mDimmedPagesDirty = true;
247 }
248
249 @Override
250 protected void dispatchDraw(Canvas canvas) {
251 if (mDimmedPagesDirty || (mTouchState == TOUCH_STATE_SCROLLING) ||
252 !mScroller.isFinished()) {
253 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
254 final int childCount = getChildCount();
255 for (int i = 0; i < childCount; ++i) {
256 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
257 int childWidth = layout.getMeasuredWidth();
258 int halfChildWidth = (childWidth / 2);
259 int childCenter = getChildOffset(i) + halfChildWidth;
260 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
261 float dimAlpha = 0.0f;
262 if (distanceFromScreenCenter < halfChildWidth) {
263 dimAlpha = 0.0f;
264 } else if (distanceFromScreenCenter > childWidth) {
265 dimAlpha = 1.0f;
266 } else {
267 dimAlpha = (float) (distanceFromScreenCenter - halfChildWidth) / halfChildWidth;
268 dimAlpha = (dimAlpha * dimAlpha);
269 }
270 layout.setDimmedBitmapAlpha(Math.max(0.0f, Math.min(1.0f, dimAlpha)));
271 }
272 }
273 super.dispatchDraw(canvas);
274 }
275
276 @Override
277 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
278 int screen = indexOfChild(child);
279 if (screen != mCurrentScreen || !mScroller.isFinished()) {
280 snapToScreen(screen);
281 return true;
282 }
283 return false;
284 }
285
286 @Override
287 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
288 int focusableScreen;
289 if (mNextScreen != INVALID_SCREEN) {
290 focusableScreen = mNextScreen;
291 } else {
292 focusableScreen = mCurrentScreen;
293 }
294 View v = getScreenAt(focusableScreen);
295 if (v != null) {
296 v.requestFocus(direction, previouslyFocusedRect);
297 }
298 return false;
299 }
300
301 @Override
302 public boolean dispatchUnhandledMove(View focused, int direction) {
303 if (direction == View.FOCUS_LEFT) {
304 if (getCurrentScreen() > 0) {
305 snapToScreen(getCurrentScreen() - 1);
306 return true;
307 }
308 } else if (direction == View.FOCUS_RIGHT) {
309 if (getCurrentScreen() < getScreenCount() - 1) {
310 snapToScreen(getCurrentScreen() + 1);
311 return true;
312 }
313 }
314 return super.dispatchUnhandledMove(focused, direction);
315 }
316
317 @Override
318 public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
319 if (mCurrentScreen >= 0 && mCurrentScreen < getScreenCount()) {
320 getScreenAt(mCurrentScreen).addFocusables(views, direction);
321 }
322 if (direction == View.FOCUS_LEFT) {
323 if (mCurrentScreen > 0) {
324 getScreenAt(mCurrentScreen - 1).addFocusables(views, direction);
325 }
326 } else if (direction == View.FOCUS_RIGHT){
327 if (mCurrentScreen < getScreenCount() - 1) {
328 getScreenAt(mCurrentScreen + 1).addFocusables(views, direction);
329 }
330 }
331 }
332
333 /**
334 * If one of our descendant views decides that it could be focused now, only
335 * pass that along if it's on the current screen.
336 *
337 * This happens when live folders requery, and if they're off screen, they
338 * end up calling requestFocus, which pulls it on screen.
339 */
340 @Override
341 public void focusableViewAvailable(View focused) {
342 View current = getScreenAt(mCurrentScreen);
343 View v = focused;
344 while (true) {
345 if (v == current) {
346 super.focusableViewAvailable(focused);
347 return;
348 }
349 if (v == this) {
350 return;
351 }
352 ViewParent parent = v.getParent();
353 if (parent instanceof View) {
354 v = (View)v.getParent();
355 } else {
356 return;
357 }
358 }
359 }
360
361 /**
362 * {@inheritDoc}
363 */
364 @Override
365 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
366 if (disallowIntercept) {
367 // We need to make sure to cancel our long press if
368 // a scrollable widget takes over touch events
369 final View currentScreen = getChildAt(mCurrentScreen);
370 currentScreen.cancelLongPress();
371 }
372 super.requestDisallowInterceptTouchEvent(disallowIntercept);
373 }
374
375 @Override
376 public boolean onInterceptTouchEvent(MotionEvent ev) {
377 /*
378 * This method JUST determines whether we want to intercept the motion.
379 * If we return true, onTouchEvent will be called and we do the actual
380 * scrolling there.
381 */
382
383 /*
384 * Shortcut the most recurring case: the user is in the dragging
385 * state and he is moving his finger. We want to intercept this
386 * motion.
387 */
388 final int action = ev.getAction();
389 if ((action == MotionEvent.ACTION_MOVE) &&
390 (mTouchState == TOUCH_STATE_SCROLLING)) {
391 return true;
392 }
393
394
395 switch (action & MotionEvent.ACTION_MASK) {
396 case MotionEvent.ACTION_MOVE: {
397 /*
398 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
399 * whether the user has moved far enough from his original down touch.
400 */
401 determineScrollingStart(ev);
402 break;
403 }
404
405 case MotionEvent.ACTION_DOWN: {
406 final float x = ev.getX();
407 final float y = ev.getY();
408 // Remember location of down touch
409 mDownMotionX = x;
410 mLastMotionX = x;
411 mLastMotionY = y;
412 mActivePointerId = ev.getPointerId(0);
413 mAllowLongPress = true;
414
415 /*
416 * If being flinged and user touches the screen, initiate drag;
417 * otherwise don't. mScroller.isFinished should be false when
418 * being flinged.
419 */
420 mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
421
422 // check if this can be the beginning of a tap on the side of the screens
423 // to scroll the current page
424 if ((mTouchState != TOUCH_STATE_PREV_PAGE) &&
425 (mTouchState != TOUCH_STATE_NEXT_PAGE)) {
426 if (getChildCount() > 0) {
427 int relativeChildLeft = getChildOffset(0);
428 int relativeChildRight = relativeChildLeft + getChildAt(0).getMeasuredWidth();
429 if (x < relativeChildLeft) {
430 mTouchState = TOUCH_STATE_PREV_PAGE;
431 } else if (x > relativeChildRight) {
432 mTouchState = TOUCH_STATE_NEXT_PAGE;
433 }
434 }
435 }
436 break;
437 }
438
439 case MotionEvent.ACTION_CANCEL:
440 case MotionEvent.ACTION_UP:
441 // Release the drag
442 mTouchState = TOUCH_STATE_REST;
443 mAllowLongPress = false;
444 mActivePointerId = INVALID_POINTER;
445
446 break;
447
448 case MotionEvent.ACTION_POINTER_UP:
449 onSecondaryPointerUp(ev);
450 break;
451 }
452
453 /*
454 * The only time we want to intercept motion events is if we are in the
455 * drag mode.
456 */
457 return mTouchState != TOUCH_STATE_REST;
458 }
459
460 /*
461 * Determines if we should change the touch state to start scrolling after the
462 * user moves their touch point too far.
463 */
464 private void determineScrollingStart(MotionEvent ev) {
465 /*
466 * Locally do absolute value. mLastMotionX is set to the y value
467 * of the down event.
468 */
469 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
470 final float x = ev.getX(pointerIndex);
471 final float y = ev.getY(pointerIndex);
472 final int xDiff = (int) Math.abs(x - mLastMotionX);
473 final int yDiff = (int) Math.abs(y - mLastMotionY);
474
475 final int touchSlop = mTouchSlop;
476 boolean xPaged = xDiff > mPagingTouchSlop;
477 boolean xMoved = xDiff > touchSlop;
478 boolean yMoved = yDiff > touchSlop;
479
480 if (xMoved || yMoved) {
481 if (xPaged) {
482 // Scroll if the user moved far enough along the X axis
483 mTouchState = TOUCH_STATE_SCROLLING;
484 mLastMotionX = x;
485 }
486 // Either way, cancel any pending longpress
487 if (mAllowLongPress) {
488 mAllowLongPress = false;
489 // Try canceling the long press. It could also have been scheduled
490 // by a distant descendant, so use the mAllowLongPress flag to block
491 // everything
492 final View currentScreen = getScreenAt(mCurrentScreen);
493 currentScreen.cancelLongPress();
494 }
495 }
496 }
497
498 @Override
499 public boolean onTouchEvent(MotionEvent ev) {
500 if (mVelocityTracker == null) {
501 mVelocityTracker = VelocityTracker.obtain();
502 }
503 mVelocityTracker.addMovement(ev);
504
505 final int action = ev.getAction();
506
507 switch (action & MotionEvent.ACTION_MASK) {
508 case MotionEvent.ACTION_DOWN:
509 /*
510 * If being flinged and user touches, stop the fling. isFinished
511 * will be false if being flinged.
512 */
513 if (!mScroller.isFinished()) {
514 mScroller.abortAnimation();
515 }
516
517 // Remember where the motion event started
518 mDownMotionX = mLastMotionX = ev.getX();
519 mActivePointerId = ev.getPointerId(0);
520 break;
521
522 case MotionEvent.ACTION_MOVE:
523 if (mTouchState == TOUCH_STATE_SCROLLING) {
524 // Scroll to follow the motion event
525 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
526 final float x = ev.getX(pointerIndex);
527 final int deltaX = (int) (mLastMotionX - x);
528 mLastMotionX = x;
529
530 int sx = getScrollX();
531 if (deltaX < 0) {
532 if (sx > 0) {
533 scrollBy(Math.max(-sx, deltaX), 0);
534 }
535 } else if (deltaX > 0) {
536 final int lastChildIndex = getChildCount() - 1;
537 final int availableToScroll = getChildOffset(lastChildIndex) -
538 getRelativeChildOffset(lastChildIndex) - sx;
539 if (availableToScroll > 0) {
540 scrollBy(Math.min(availableToScroll, deltaX), 0);
541 }
542 } else {
543 awakenScrollBars();
544 }
545 } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) ||
546 (mTouchState == TOUCH_STATE_NEXT_PAGE)) {
547 determineScrollingStart(ev);
548 }
549 break;
550
551 case MotionEvent.ACTION_UP:
552 if (mTouchState == TOUCH_STATE_SCROLLING) {
553 final int activePointerId = mActivePointerId;
554 final int pointerIndex = ev.findPointerIndex(activePointerId);
555 final float x = ev.getX(pointerIndex);
556 final VelocityTracker velocityTracker = mVelocityTracker;
557 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
558 int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
559 boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING;
560
561 if (isfling && velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
562 snapToScreen(mCurrentScreen - 1);
563 } else if (isfling && velocityX < -SNAP_VELOCITY &&
564 mCurrentScreen < getChildCount() - 1) {
565 snapToScreen(mCurrentScreen + 1);
566 } else {
567 snapToDestination();
568 }
569
570 if (mVelocityTracker != null) {
571 mVelocityTracker.recycle();
572 mVelocityTracker = null;
573 }
574 } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
575 // at this point we have not moved beyond the touch slop
576 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
577 // we can just page
578 int nextScreen = Math.max(0, mCurrentScreen - 1);
579 if (nextScreen != mCurrentScreen) {
580 snapToScreen(nextScreen);
581 } else {
582 snapToDestination();
583 }
584 } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
585 // at this point we have not moved beyond the touch slop
586 // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
587 // we can just page
588 int nextScreen = Math.min(getChildCount() - 1, mCurrentScreen + 1);
589 if (nextScreen != mCurrentScreen) {
590 snapToScreen(nextScreen);
591 } else {
592 snapToDestination();
593 }
594 }
595 mTouchState = TOUCH_STATE_REST;
596 mActivePointerId = INVALID_POINTER;
597 break;
598
599 case MotionEvent.ACTION_CANCEL:
600 mTouchState = TOUCH_STATE_REST;
601 mActivePointerId = INVALID_POINTER;
602 break;
603
604 case MotionEvent.ACTION_POINTER_UP:
605 onSecondaryPointerUp(ev);
606 break;
607 }
608
609 return true;
610 }
611
612 private void onSecondaryPointerUp(MotionEvent ev) {
613 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
614 MotionEvent.ACTION_POINTER_INDEX_SHIFT;
615 final int pointerId = ev.getPointerId(pointerIndex);
616 if (pointerId == mActivePointerId) {
617 // This was our active pointer going up. Choose a new
618 // active pointer and adjust accordingly.
619 // TODO: Make this decision more intelligent.
620 final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
621 mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
622 mLastMotionY = ev.getY(newPointerIndex);
623 mActivePointerId = ev.getPointerId(newPointerIndex);
624 if (mVelocityTracker != null) {
625 mVelocityTracker.clear();
626 }
627 }
628 }
629
630 @Override
631 public void requestChildFocus(View child, View focused) {
632 super.requestChildFocus(child, focused);
633 int screen = indexOfChild(child);
634 if (screen >= 0 && !isInTouchMode()) {
635 snapToScreen(screen);
636 }
637 }
638
639 protected int getRelativeChildOffset(int index) {
640 return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2;
641 }
642
643 protected int getChildOffset(int index) {
644 if (getChildCount() == 0)
645 return 0;
646
647 int offset = getRelativeChildOffset(0);
648 for (int i = 0; i < index; ++i) {
649 offset += getChildAt(i).getMeasuredWidth();
650 }
651 return offset;
652 }
653
654 protected void snapToDestination() {
655 int minDistanceFromScreenCenter = getMeasuredWidth();
656 int minDistanceFromScreenCenterIndex = -1;
657 int screenCenter = mScrollX + (getMeasuredWidth() / 2);
658 final int childCount = getChildCount();
659 for (int i = 0; i < childCount; ++i) {
660 PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
661 int childWidth = layout.getMeasuredWidth();
662 int halfChildWidth = (childWidth / 2);
663 int childCenter = getChildOffset(i) + halfChildWidth;
664 int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
665 if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
666 minDistanceFromScreenCenter = distanceFromScreenCenter;
667 minDistanceFromScreenCenterIndex = i;
668 }
669 }
670 snapToScreen(minDistanceFromScreenCenterIndex, 1000);
671 }
672
673 void snapToScreen(int whichScreen) {
674 snapToScreen(whichScreen, 1000);
675 }
676
677 void snapToScreen(int whichScreen, int duration) {
678 whichScreen = Math.max(0, Math.min(whichScreen, getScreenCount() - 1));
679
680 mNextScreen = whichScreen;
681
682 int newX = getChildOffset(whichScreen) - getRelativeChildOffset(whichScreen);
683 final int sX = getScrollX();
684 final int delta = newX - sX;
685 awakenScrollBars(duration);
686 if (duration == 0) {
687 duration = Math.abs(delta);
688 }
689
690 if (!mScroller.isFinished()) mScroller.abortAnimation();
691 mScroller.startScroll(sX, 0, delta, 0, duration);
692 invalidate();
693 }
694
695 @Override
696 protected Parcelable onSaveInstanceState() {
697 final SavedState state = new SavedState(super.onSaveInstanceState());
698 state.currentScreen = mCurrentScreen;
699 return state;
700 }
701
702 @Override
703 protected void onRestoreInstanceState(Parcelable state) {
704 SavedState savedState = (SavedState) state;
705 super.onRestoreInstanceState(savedState.getSuperState());
706 if (savedState.currentScreen != -1) {
707 mCurrentScreen = savedState.currentScreen;
708 }
709 }
710
711 public void scrollLeft() {
712 if (mScroller.isFinished()) {
713 if (mCurrentScreen > 0) snapToScreen(mCurrentScreen - 1);
714 } else {
715 if (mNextScreen > 0) snapToScreen(mNextScreen - 1);
716 }
717 }
718
719 public void scrollRight() {
720 if (mScroller.isFinished()) {
721 if (mCurrentScreen < getChildCount() -1) snapToScreen(mCurrentScreen + 1);
722 } else {
723 if (mNextScreen < getChildCount() -1) snapToScreen(mNextScreen + 1);
724 }
725 }
726
727 public int getScreenForView(View v) {
728 int result = -1;
729 if (v != null) {
730 ViewParent vp = v.getParent();
731 int count = getChildCount();
732 for (int i = 0; i < count; i++) {
733 if (vp == getChildAt(i)) {
734 return i;
735 }
736 }
737 }
738 return result;
739 }
740
741 /**
742 * @return True is long presses are still allowed for the current touch
743 */
744 public boolean allowLongPress() {
745 return mAllowLongPress;
746 }
747
748 public static class SavedState extends BaseSavedState {
749 int currentScreen = -1;
750
751 SavedState(Parcelable superState) {
752 super(superState);
753 }
754
755 private SavedState(Parcel in) {
756 super(in);
757 currentScreen = in.readInt();
758 }
759
760 @Override
761 public void writeToParcel(Parcel out, int flags) {
762 super.writeToParcel(out, flags);
763 out.writeInt(currentScreen);
764 }
765
766 public static final Parcelable.Creator<SavedState> CREATOR =
767 new Parcelable.Creator<SavedState>() {
768 public SavedState createFromParcel(Parcel in) {
769 return new SavedState(in);
770 }
771
772 public SavedState[] newArray(int size) {
773 return new SavedState[size];
774 }
775 };
776 }
777
778 public abstract void syncPages();
779 public abstract void syncPageItems(int page);
780 public void invalidatePageData() {
781 syncPages();
782 for (int i = 0; i < getChildCount(); ++i) {
783 syncPageItems(i);
784 }
785 invalidateDimmedPages();
786 requestLayout();
787 }
788}