PagedView.java revision 321e9ee68848d9e782fd557f69cc070308ffbc9c
1/*
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}
789