PagedView.java revision 28750fba6a2d141eb9a1e566718c17236030b815
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;
20import java.util.HashMap;
21
22import android.content.Context;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Rect;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.ActionMode;
31import android.view.MotionEvent;
32import android.view.VelocityTracker;
33import android.view.View;
34import android.view.ViewConfiguration;
35import android.view.ViewGroup;
36import android.view.ViewParent;
37import android.view.animation.Animation;
38import android.view.animation.Animation.AnimationListener;
39import android.view.animation.AnimationUtils;
40import android.widget.Checkable;
41import android.widget.LinearLayout;
42import android.widget.Scroller;
43
44import com.android.launcher.R;
45
46/**
47 * An abstraction of the original Workspace which supports browsing through a
48 * sequential list of "pages"
49 */
50public abstract class PagedView extends ViewGroup {
51    private static final String TAG = "PagedView";
52    protected static final int INVALID_PAGE = -1;
53
54    // the min drag distance for a fling to register, to prevent random page shifts
55    private static final int MIN_LENGTH_FOR_FLING = 50;
56
57    private static final int PAGE_SNAP_ANIMATION_DURATION = 1000;
58    protected static final float NANOTIME_DIV = 1000000000.0f;
59
60    // the velocity at which a fling gesture will cause us to snap to the next page
61    protected int mSnapVelocity = 500;
62
63    protected float mSmoothingTime;
64    protected float mTouchX;
65
66    protected boolean mFirstLayout = true;
67
68    protected int mCurrentPage;
69    protected int mNextPage = INVALID_PAGE;
70    protected Scroller mScroller;
71    private VelocityTracker mVelocityTracker;
72
73    private float mDownMotionX;
74    private float mLastMotionX;
75    private float mLastMotionY;
76
77    protected final static int TOUCH_STATE_REST = 0;
78    protected final static int TOUCH_STATE_SCROLLING = 1;
79    protected final static int TOUCH_STATE_PREV_PAGE = 2;
80    protected final static int TOUCH_STATE_NEXT_PAGE = 3;
81
82    protected int mTouchState = TOUCH_STATE_REST;
83
84    protected OnLongClickListener mLongClickListener;
85
86    private boolean mAllowLongPress = true;
87
88    private int mTouchSlop;
89    private int mPagingTouchSlop;
90    private int mMaximumVelocity;
91
92    protected static final int INVALID_POINTER = -1;
93
94    protected int mActivePointerId = INVALID_POINTER;
95
96    private PageSwitchListener mPageSwitchListener;
97
98    private ArrayList<Boolean> mDirtyPageContent;
99    private boolean mDirtyPageAlpha;
100
101    // choice modes
102    protected static final int CHOICE_MODE_NONE = 0;
103    protected static final int CHOICE_MODE_SINGLE = 1;
104    // Multiple selection mode is not supported by all Launcher actions atm
105    protected static final int CHOICE_MODE_MULTIPLE = 2;
106
107    private int mChoiceMode;
108    private ActionMode mActionMode;
109
110    protected PagedViewIconCache mPageViewIconCache;
111
112    // If true, syncPages and syncPageItems will be called to refresh pages
113    protected boolean mContentIsRefreshable = true;
114
115    // If true, modify alpha of neighboring pages as user scrolls left/right
116    protected boolean mFadeInAdjacentScreens = true;
117
118    // It true, use a different slop parameter (pagingTouchSlop = 2 * touchSlop) for deciding
119    // to switch to a new page
120    protected boolean mUsePagingTouchSlop = true;
121
122    // If true, the subclass should directly update mScrollX itself in its computeScroll method
123    // (SmoothPagedView does this)
124    protected boolean mDeferScrollUpdate = false;
125
126    /**
127     * Simple cache mechanism for PagedViewIcon outlines.
128     */
129    class PagedViewIconCache {
130        private final HashMap<Object, Bitmap> iconOutlineCache = new HashMap<Object, Bitmap>();
131
132        public void clear() {
133            iconOutlineCache.clear();
134        }
135        public void addOutline(Object key, Bitmap b) {
136            iconOutlineCache.put(key, b);
137        }
138        public void removeOutline(Object key) {
139            if (iconOutlineCache.containsKey(key)) {
140                iconOutlineCache.remove(key);
141            }
142        }
143        public Bitmap getOutline(Object key) {
144            return iconOutlineCache.get(key);
145        }
146    }
147
148    public interface PageSwitchListener {
149        void onPageSwitch(View newPage, int newPageIndex);
150    }
151
152    public interface PageMovingListener {
153        void onPageBeginMoving();
154        void onPageEndMoving();
155    }
156
157    public PagedView(Context context) {
158        this(context, null);
159    }
160
161    public PagedView(Context context, AttributeSet attrs) {
162        this(context, attrs, 0);
163    }
164
165    public PagedView(Context context, AttributeSet attrs, int defStyle) {
166        super(context, attrs, defStyle);
167        mChoiceMode = CHOICE_MODE_NONE;
168
169        setHapticFeedbackEnabled(false);
170        init();
171    }
172
173    /**
174     * Initializes various states for this workspace.
175     */
176    protected void init() {
177        mDirtyPageContent = new ArrayList<Boolean>();
178        mDirtyPageContent.ensureCapacity(32);
179        mPageViewIconCache = new PagedViewIconCache();
180        mScroller = new Scroller(getContext());
181        mCurrentPage = 0;
182
183        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
184        mTouchSlop = configuration.getScaledTouchSlop();
185        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
186        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
187    }
188
189    public void setPageSwitchListener(PageSwitchListener pageSwitchListener) {
190        mPageSwitchListener = pageSwitchListener;
191        if (mPageSwitchListener != null) {
192            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
193        }
194    }
195
196    /**
197     * Returns the index of the currently displayed page.
198     *
199     * @return The index of the currently displayed page.
200     */
201    int getCurrentPage() {
202        return mCurrentPage;
203    }
204
205    int getPageCount() {
206        return getChildCount();
207    }
208
209    View getPageAt(int index) {
210        return getChildAt(index);
211    }
212
213    int getScrollWidth() {
214        return getWidth();
215    }
216
217    /**
218     * Sets the current page.
219     */
220    void setCurrentPage(int currentPage) {
221        if (!mScroller.isFinished()) mScroller.abortAnimation();
222        if (getChildCount() == 0) return;
223
224        mCurrentPage = Math.max(0, Math.min(currentPage, getPageCount() - 1));
225        scrollTo(getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage), 0);
226
227        invalidate();
228        notifyPageSwitchListener();
229    }
230
231    protected void notifyPageSwitchListener() {
232        if (mPageSwitchListener != null) {
233            mPageSwitchListener.onPageSwitch(getPageAt(mCurrentPage), mCurrentPage);
234        }
235    }
236
237    // a method that subclasses can override to add behavior
238    protected void pageBeginMoving() {
239    }
240
241    // a method that subclasses can override to add behavior
242    protected void pageEndMoving() {
243    }
244
245    /**
246     * Registers the specified listener on each page contained in this workspace.
247     *
248     * @param l The listener used to respond to long clicks.
249     */
250    @Override
251    public void setOnLongClickListener(OnLongClickListener l) {
252        mLongClickListener = l;
253        final int count = getPageCount();
254        for (int i = 0; i < count; i++) {
255            getPageAt(i).setOnLongClickListener(l);
256        }
257    }
258
259    @Override
260    public void scrollTo(int x, int y) {
261        super.scrollTo(x, y);
262        mTouchX = x;
263        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
264    }
265
266    // we moved this functionality to a helper function so SmoothPagedView can reuse it
267    protected boolean computeScrollHelper() {
268        if (mScroller.computeScrollOffset()) {
269            mDirtyPageAlpha = true;
270            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
271            invalidate();
272            return true;
273        } else if (mNextPage != INVALID_PAGE) {
274            mDirtyPageAlpha = true;
275            mCurrentPage = Math.max(0, Math.min(mNextPage, getPageCount() - 1));
276            mNextPage = INVALID_PAGE;
277            notifyPageSwitchListener();
278            pageEndMoving();
279            return true;
280        }
281        return false;
282    }
283
284    @Override
285    public void computeScroll() {
286        computeScrollHelper();
287    }
288
289    @Override
290    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
291        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
292        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
293        if (widthMode != MeasureSpec.EXACTLY) {
294            throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
295        }
296
297        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
298        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
299        if (heightMode != MeasureSpec.EXACTLY) {
300            throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
301        }
302
303        // The children are given the same width and height as the workspace
304        // unless they were set to WRAP_CONTENT
305        final int childCount = getChildCount();
306        for (int i = 0; i < childCount; i++) {
307            // disallowing padding in paged view (just pass 0)
308            final View child = getChildAt(i);
309            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
310
311            int childWidthMode;
312            if (lp.width == LayoutParams.WRAP_CONTENT) {
313                childWidthMode = MeasureSpec.AT_MOST;
314            } else {
315                childWidthMode = MeasureSpec.EXACTLY;
316            }
317
318            int childHeightMode;
319            if (lp.height == LayoutParams.WRAP_CONTENT) {
320                childHeightMode = MeasureSpec.AT_MOST;
321            } else {
322                childHeightMode = MeasureSpec.EXACTLY;
323            }
324
325            final int childWidthMeasureSpec =
326                MeasureSpec.makeMeasureSpec(widthSize, childWidthMode);
327            final int childHeightMeasureSpec =
328                MeasureSpec.makeMeasureSpec(heightSize, childHeightMode);
329
330            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
331        }
332
333        setMeasuredDimension(widthSize, heightSize);
334    }
335
336    @Override
337    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
338        if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) {
339            setHorizontalScrollBarEnabled(false);
340            int newX = getChildOffset(mCurrentPage) - getRelativeChildOffset(mCurrentPage);
341            scrollTo(newX, 0);
342            mScroller.setFinalX(newX);
343            setHorizontalScrollBarEnabled(true);
344            mFirstLayout = false;
345        }
346
347        final int childCount = getChildCount();
348        int childLeft = 0;
349        if (childCount > 0) {
350            childLeft = getRelativeChildOffset(0);
351        }
352
353        for (int i = 0; i < childCount; i++) {
354            final View child = getChildAt(i);
355            if (child.getVisibility() != View.GONE) {
356                final int childWidth = child.getMeasuredWidth();
357                final int childHeight = (getMeasuredHeight() - child.getMeasuredHeight()) / 2;
358                child.layout(childLeft, childHeight,
359                        childLeft + childWidth, childHeight + child.getMeasuredHeight());
360                childLeft += childWidth;
361            }
362        }
363    }
364
365    protected void updateAdjacentPagesAlpha() {
366        if (mFadeInAdjacentScreens) {
367            if (mDirtyPageAlpha || (mTouchState == TOUCH_STATE_SCROLLING) || !mScroller.isFinished()) {
368                int halfScreenSize = getMeasuredWidth() / 2;
369                int screenCenter = mScrollX + halfScreenSize;
370
371                final int childCount = getChildCount();
372                for (int i = 0; i < childCount; ++i) {
373                    View layout = (View) getChildAt(i);
374                    int childWidth = layout.getMeasuredWidth();
375                    int halfChildWidth = (childWidth / 2);
376                    int childCenter = getChildOffset(i) + halfChildWidth;
377
378                    int d = halfChildWidth;
379                    int distanceFromScreenCenter = childCenter - screenCenter;
380                    if (distanceFromScreenCenter > 0) {
381                        if (i > 0) {
382                            d += getChildAt(i - 1).getMeasuredWidth() / 2;
383                        }
384                    } else {
385                        if (i < childCount - 1) {
386                            d += getChildAt(i + 1).getMeasuredWidth() / 2;
387                        }
388                    }
389
390                    float dimAlpha = (float) (Math.abs(distanceFromScreenCenter)) / d;
391                    dimAlpha = Math.max(0.0f, Math.min(1.0f, (dimAlpha * dimAlpha)));
392                    float alpha = 1.0f - dimAlpha;
393
394                    if (Float.compare(alpha, layout.getAlpha()) != 0) {
395                        layout.setAlpha(alpha);
396                    }
397                }
398                mDirtyPageAlpha = false;
399            }
400        }
401    }
402
403    @Override
404    protected void dispatchDraw(Canvas canvas) {
405        updateAdjacentPagesAlpha();
406
407        // Find out which screens are visible; as an optimization we only call draw on them
408        // As an optimization, this code assumes that all pages have the same width as the 0th
409        // page.
410        final int pageCount = getChildCount();
411        if (pageCount > 0) {
412            final int pageWidth = getChildAt(0).getMeasuredWidth();
413            final int screenWidth = getMeasuredWidth();
414            int x = getRelativeChildOffset(0) + pageWidth;
415            int leftScreen = 0;
416            int rightScreen = 0;
417            while (x <= mScrollX) {
418                leftScreen++;
419                x += pageWidth;
420                // replace above line with this if you don't assume all pages have same width as 0th
421                // page:
422                // x += getChildAt(leftScreen).getMeasuredWidth();
423            }
424            rightScreen = leftScreen;
425            while (x < mScrollX + screenWidth) {
426                rightScreen++;
427                x += pageWidth;
428                // replace above line with this if you don't assume all pages have same width as 0th
429                // page:
430                //if (rightScreen < pageCount) {
431                //    x += getChildAt(rightScreen).getMeasuredWidth();
432                //}
433            }
434            rightScreen = Math.min(getChildCount() - 1, rightScreen);
435
436            final long drawingTime = getDrawingTime();
437            for (int i = leftScreen; i <= rightScreen; i++) {
438                drawChild(canvas, getChildAt(i), drawingTime);
439            }
440        }
441    }
442
443    @Override
444    public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
445        int page = indexOfChild(child);
446        if (page != mCurrentPage || !mScroller.isFinished()) {
447            snapToPage(page);
448            return true;
449        }
450        return false;
451    }
452
453    @Override
454    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
455        int focusablePage;
456        if (mNextPage != INVALID_PAGE) {
457            focusablePage = mNextPage;
458        } else {
459            focusablePage = mCurrentPage;
460        }
461        View v = getPageAt(focusablePage);
462        if (v != null) {
463            v.requestFocus(direction, previouslyFocusedRect);
464        }
465        return false;
466    }
467
468    @Override
469    public boolean dispatchUnhandledMove(View focused, int direction) {
470        if (direction == View.FOCUS_LEFT) {
471            if (getCurrentPage() > 0) {
472                snapToPage(getCurrentPage() - 1);
473                return true;
474            }
475        } else if (direction == View.FOCUS_RIGHT) {
476            if (getCurrentPage() < getPageCount() - 1) {
477                snapToPage(getCurrentPage() + 1);
478                return true;
479            }
480        }
481        return super.dispatchUnhandledMove(focused, direction);
482    }
483
484    @Override
485    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
486        if (mCurrentPage >= 0 && mCurrentPage < getPageCount()) {
487            getPageAt(mCurrentPage).addFocusables(views, direction);
488        }
489        if (direction == View.FOCUS_LEFT) {
490            if (mCurrentPage > 0) {
491                getPageAt(mCurrentPage - 1).addFocusables(views, direction);
492            }
493        } else if (direction == View.FOCUS_RIGHT){
494            if (mCurrentPage < getPageCount() - 1) {
495                getPageAt(mCurrentPage + 1).addFocusables(views, direction);
496            }
497        }
498    }
499
500    /**
501     * If one of our descendant views decides that it could be focused now, only
502     * pass that along if it's on the current page.
503     *
504     * This happens when live folders requery, and if they're off page, they
505     * end up calling requestFocus, which pulls it on page.
506     */
507    @Override
508    public void focusableViewAvailable(View focused) {
509        View current = getPageAt(mCurrentPage);
510        View v = focused;
511        while (true) {
512            if (v == current) {
513                super.focusableViewAvailable(focused);
514                return;
515            }
516            if (v == this) {
517                return;
518            }
519            ViewParent parent = v.getParent();
520            if (parent instanceof View) {
521                v = (View)v.getParent();
522            } else {
523                return;
524            }
525        }
526    }
527
528    /**
529     * {@inheritDoc}
530     */
531    @Override
532    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
533        if (disallowIntercept) {
534            // We need to make sure to cancel our long press if
535            // a scrollable widget takes over touch events
536            final View currentPage = getChildAt(mCurrentPage);
537            currentPage.cancelLongPress();
538        }
539        super.requestDisallowInterceptTouchEvent(disallowIntercept);
540    }
541
542    @Override
543    public boolean onInterceptTouchEvent(MotionEvent ev) {
544        /*
545         * This method JUST determines whether we want to intercept the motion.
546         * If we return true, onTouchEvent will be called and we do the actual
547         * scrolling there.
548         */
549
550        /*
551         * Shortcut the most recurring case: the user is in the dragging
552         * state and he is moving his finger.  We want to intercept this
553         * motion.
554         */
555        final int action = ev.getAction();
556        if ((action == MotionEvent.ACTION_MOVE) &&
557                (mTouchState == TOUCH_STATE_SCROLLING)) {
558            return true;
559        }
560
561        switch (action & MotionEvent.ACTION_MASK) {
562            case MotionEvent.ACTION_MOVE: {
563                /*
564                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
565                 * whether the user has moved far enough from his original down touch.
566                 */
567                if (mActivePointerId != INVALID_POINTER) {
568                    determineScrollingStart(ev);
569                    break;
570                }
571                // if mActivePointerId is INVALID_POINTER, then we must have missed an ACTION_DOWN
572                // event. in that case, treat the first occurence of a move event as a ACTION_DOWN
573                // i.e. fall through to the next case (don't break)
574                // (We sometimes miss ACTION_DOWN events in Workspace because it ignores all events
575                // while it's small- this was causing a crash before we checked for INVALID_POINTER)
576            }
577
578            case MotionEvent.ACTION_DOWN: {
579                final float x = ev.getX();
580                final float y = ev.getY();
581                // Remember location of down touch
582                mDownMotionX = x;
583                mLastMotionX = x;
584                mLastMotionY = y;
585                mActivePointerId = ev.getPointerId(0);
586                mAllowLongPress = true;
587
588                /*
589                 * If being flinged and user touches the screen, initiate drag;
590                 * otherwise don't.  mScroller.isFinished should be false when
591                 * being flinged.
592                 */
593                final int xDist = (mScroller.getFinalX() - mScroller.getCurrX());
594                final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop);
595                if (finishedScrolling) {
596                    mTouchState = TOUCH_STATE_REST;
597                    mScroller.abortAnimation();
598                } else {
599                    mTouchState = TOUCH_STATE_SCROLLING;
600                }
601
602                // check if this can be the beginning of a tap on the side of the pages
603                // to scroll the current page
604                if ((mTouchState != TOUCH_STATE_PREV_PAGE) &&
605                        (mTouchState != TOUCH_STATE_NEXT_PAGE)) {
606                    if (getChildCount() > 0) {
607                        int width = getMeasuredWidth();
608                        int offset = getRelativeChildOffset(mCurrentPage);
609                        if (x < offset) {
610                            mTouchState = TOUCH_STATE_PREV_PAGE;
611                        } else if (x > (width - offset)) {
612                            mTouchState = TOUCH_STATE_NEXT_PAGE;
613                        }
614                    }
615                }
616                break;
617            }
618
619            case MotionEvent.ACTION_CANCEL:
620            case MotionEvent.ACTION_UP:
621                // Release the drag
622                pageEndMoving();
623                mTouchState = TOUCH_STATE_REST;
624                mAllowLongPress = false;
625                mActivePointerId = INVALID_POINTER;
626
627                break;
628
629            case MotionEvent.ACTION_POINTER_UP:
630                onSecondaryPointerUp(ev);
631                break;
632        }
633
634        /*
635         * The only time we want to intercept motion events is if we are in the
636         * drag mode.
637         */
638        return mTouchState != TOUCH_STATE_REST;
639    }
640
641    protected void animateClickFeedback(View v, final Runnable r) {
642        // animate the view slightly to show click feedback running some logic after it is "pressed"
643        Animation anim = AnimationUtils.loadAnimation(getContext(),
644                R.anim.paged_view_click_feedback);
645        anim.setAnimationListener(new AnimationListener() {
646            @Override
647            public void onAnimationStart(Animation animation) {}
648            @Override
649            public void onAnimationRepeat(Animation animation) {
650                r.run();
651            }
652            @Override
653            public void onAnimationEnd(Animation animation) {}
654        });
655        v.startAnimation(anim);
656    }
657
658    /*
659     * Determines if we should change the touch state to start scrolling after the
660     * user moves their touch point too far.
661     */
662    private void determineScrollingStart(MotionEvent ev) {
663        /*
664         * Locally do absolute value. mLastMotionX is set to the y value
665         * of the down event.
666         */
667        final int pointerIndex = ev.findPointerIndex(mActivePointerId);
668        final float x = ev.getX(pointerIndex);
669        final float y = ev.getY(pointerIndex);
670        final int xDiff = (int) Math.abs(x - mLastMotionX);
671        final int yDiff = (int) Math.abs(y - mLastMotionY);
672
673        final int touchSlop = mTouchSlop;
674        boolean xPaged = xDiff > mPagingTouchSlop;
675        boolean xMoved = xDiff > touchSlop;
676        boolean yMoved = yDiff > touchSlop;
677
678        if (xMoved || yMoved) {
679            if (mUsePagingTouchSlop ? xPaged : xMoved) {
680                // Scroll if the user moved far enough along the X axis
681                mTouchState = TOUCH_STATE_SCROLLING;
682                mLastMotionX = x;
683                mTouchX = mScrollX;
684                mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
685                pageBeginMoving();
686            }
687            // Either way, cancel any pending longpress
688            if (mAllowLongPress) {
689                mAllowLongPress = false;
690                // Try canceling the long press. It could also have been scheduled
691                // by a distant descendant, so use the mAllowLongPress flag to block
692                // everything
693                final View currentPage = getPageAt(mCurrentPage);
694                currentPage.cancelLongPress();
695            }
696        }
697    }
698
699    @Override
700    public boolean onTouchEvent(MotionEvent ev) {
701        if (mVelocityTracker == null) {
702            mVelocityTracker = VelocityTracker.obtain();
703        }
704        mVelocityTracker.addMovement(ev);
705
706        final int action = ev.getAction();
707
708        switch (action & MotionEvent.ACTION_MASK) {
709        case MotionEvent.ACTION_DOWN:
710            /*
711             * If being flinged and user touches, stop the fling. isFinished
712             * will be false if being flinged.
713             */
714            if (!mScroller.isFinished()) {
715                mScroller.abortAnimation();
716            }
717
718            // Remember where the motion event started
719            mDownMotionX = mLastMotionX = ev.getX();
720            mActivePointerId = ev.getPointerId(0);
721            if (mTouchState == TOUCH_STATE_SCROLLING) {
722                pageBeginMoving();
723            }
724            break;
725
726        case MotionEvent.ACTION_MOVE:
727            if (mTouchState == TOUCH_STATE_SCROLLING) {
728                // Scroll to follow the motion event
729                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
730                final float x = ev.getX(pointerIndex);
731                final int deltaX = (int) (mLastMotionX - x);
732                mLastMotionX = x;
733
734                int sx = getScrollX();
735                if (deltaX < 0) {
736                    if (sx > 0) {
737                        mTouchX += Math.max(-mTouchX, deltaX);
738                        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
739                        if (!mDeferScrollUpdate) {
740                            scrollBy(Math.max(-sx, deltaX), 0);
741                        } else {
742                            // This will trigger a call to computeScroll() on next drawChild() call
743                            invalidate();
744                        }
745                    }
746                } else if (deltaX > 0) {
747                    final int lastChildIndex = getChildCount() - 1;
748                    final int availableToScroll = getChildOffset(lastChildIndex) -
749                        getRelativeChildOffset(lastChildIndex) - sx;
750                    if (availableToScroll > 0) {
751                        mTouchX += Math.min(availableToScroll, deltaX);
752                        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
753                        if (!mDeferScrollUpdate) {
754                            scrollBy(Math.min(availableToScroll, deltaX), 0);
755                        } else {
756                            // This will trigger a call to computeScroll() on next drawChild() call
757                            invalidate();
758                        }
759                    }
760                } else {
761                    awakenScrollBars();
762                }
763            } else if ((mTouchState == TOUCH_STATE_PREV_PAGE) ||
764                    (mTouchState == TOUCH_STATE_NEXT_PAGE)) {
765                determineScrollingStart(ev);
766            }
767            break;
768
769        case MotionEvent.ACTION_UP:
770            if (mTouchState == TOUCH_STATE_SCROLLING) {
771                final int activePointerId = mActivePointerId;
772                final int pointerIndex = ev.findPointerIndex(activePointerId);
773                final float x = ev.getX(pointerIndex);
774                final VelocityTracker velocityTracker = mVelocityTracker;
775                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
776                int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
777                boolean isfling = Math.abs(mDownMotionX - x) > MIN_LENGTH_FOR_FLING;
778
779                final int snapVelocity = mSnapVelocity;
780                if (isfling && velocityX > snapVelocity && mCurrentPage > 0) {
781                    snapToPageWithVelocity(mCurrentPage - 1, velocityX);
782                } else if (isfling && velocityX < -snapVelocity &&
783                        mCurrentPage < getChildCount() - 1) {
784                    snapToPageWithVelocity(mCurrentPage + 1, velocityX);
785                } else {
786                    snapToDestination();
787                }
788
789                if (mVelocityTracker != null) {
790                    mVelocityTracker.recycle();
791                    mVelocityTracker = null;
792                }
793            } else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
794                // at this point we have not moved beyond the touch slop
795                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
796                // we can just page
797                int nextPage = Math.max(0, mCurrentPage - 1);
798                if (nextPage != mCurrentPage) {
799                    snapToPage(nextPage);
800                } else {
801                    snapToDestination();
802                }
803            } else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
804                // at this point we have not moved beyond the touch slop
805                // (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
806                // we can just page
807                int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
808                if (nextPage != mCurrentPage) {
809                    snapToPage(nextPage);
810                } else {
811                    snapToDestination();
812                }
813            }
814            mTouchState = TOUCH_STATE_REST;
815            mActivePointerId = INVALID_POINTER;
816            break;
817
818        case MotionEvent.ACTION_CANCEL:
819            mTouchState = TOUCH_STATE_REST;
820            mActivePointerId = INVALID_POINTER;
821            break;
822
823        case MotionEvent.ACTION_POINTER_UP:
824            onSecondaryPointerUp(ev);
825            break;
826        }
827
828        return true;
829    }
830
831    private void onSecondaryPointerUp(MotionEvent ev) {
832        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
833                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
834        final int pointerId = ev.getPointerId(pointerIndex);
835        if (pointerId == mActivePointerId) {
836            // This was our active pointer going up. Choose a new
837            // active pointer and adjust accordingly.
838            // TODO: Make this decision more intelligent.
839            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
840            mLastMotionX = mDownMotionX = ev.getX(newPointerIndex);
841            mLastMotionY = ev.getY(newPointerIndex);
842            mActivePointerId = ev.getPointerId(newPointerIndex);
843            if (mVelocityTracker != null) {
844                mVelocityTracker.clear();
845            }
846        }
847    }
848
849    @Override
850    public void requestChildFocus(View child, View focused) {
851        super.requestChildFocus(child, focused);
852        int page = indexOfChild(child);
853        if (page >= 0 && !isInTouchMode()) {
854            snapToPage(page);
855        }
856    }
857
858    protected int getChildIndexForRelativeOffset(int relativeOffset) {
859        final int childCount = getChildCount();
860        int left = getRelativeChildOffset(0);
861        for (int i = 0; i < childCount; ++i) {
862            final int right = (left + getChildAt(i).getMeasuredWidth());
863            if (left <= relativeOffset && relativeOffset <= right) {
864                return i;
865            }
866            left = right;
867        }
868        return -1;
869    }
870
871    protected int getRelativeChildOffset(int index) {
872        return (getMeasuredWidth() - getChildAt(index).getMeasuredWidth()) / 2;
873    }
874
875    protected int getChildOffset(int index) {
876        if (getChildCount() == 0)
877            return 0;
878
879        int offset = getRelativeChildOffset(0);
880        for (int i = 0; i < index; ++i) {
881            offset += getChildAt(i).getMeasuredWidth();
882        }
883        return offset;
884    }
885
886    int getPageNearestToCenterOfScreen() {
887        int minDistanceFromScreenCenter = getMeasuredWidth();
888        int minDistanceFromScreenCenterIndex = -1;
889        int screenCenter = mScrollX + (getMeasuredWidth() / 2);
890        final int childCount = getChildCount();
891        for (int i = 0; i < childCount; ++i) {
892            View layout = (View) getChildAt(i);
893            int childWidth = layout.getMeasuredWidth();
894            int halfChildWidth = (childWidth / 2);
895            int childCenter = getChildOffset(i) + halfChildWidth;
896            int distanceFromScreenCenter = Math.abs(childCenter - screenCenter);
897            if (distanceFromScreenCenter < minDistanceFromScreenCenter) {
898                minDistanceFromScreenCenter = distanceFromScreenCenter;
899                minDistanceFromScreenCenterIndex = i;
900            }
901        }
902        return minDistanceFromScreenCenterIndex;
903    }
904
905    protected void snapToDestination() {
906        snapToPage(getPageNearestToCenterOfScreen(), PAGE_SNAP_ANIMATION_DURATION);
907    }
908
909    protected void snapToPageWithVelocity(int whichPage, int velocity) {
910        // We ignore velocity in this implementation, but children (e.g. SmoothPagedView)
911        // can use it
912        snapToPage(whichPage);
913    }
914
915    protected void snapToPage(int whichPage) {
916        snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
917    }
918
919    protected void snapToPage(int whichPage, int duration) {
920        whichPage = Math.max(0, Math.min(whichPage, getPageCount() - 1));
921
922        int newX = getChildOffset(whichPage) - getRelativeChildOffset(whichPage);
923        final int sX = getScrollX();
924        final int delta = newX - sX;
925        snapToPage(whichPage, delta, duration);
926    }
927
928    protected void snapToPage(int whichPage, int delta, int duration) {
929        mNextPage = whichPage;
930
931        View focusedChild = getFocusedChild();
932        if (focusedChild != null && whichPage != mCurrentPage &&
933                focusedChild == getChildAt(mCurrentPage)) {
934            focusedChild.clearFocus();
935        }
936
937        pageBeginMoving();
938        awakenScrollBars(duration);
939        if (duration == 0) {
940            duration = Math.abs(delta);
941        }
942
943        if (!mScroller.isFinished()) mScroller.abortAnimation();
944        mScroller.startScroll(getScrollX(), 0, delta, 0, duration);
945
946        // only load some associated pages
947        loadAssociatedPages(mNextPage);
948        notifyPageSwitchListener();
949        invalidate();
950    }
951
952    @Override
953    protected Parcelable onSaveInstanceState() {
954        final SavedState state = new SavedState(super.onSaveInstanceState());
955        state.currentPage = mCurrentPage;
956        return state;
957    }
958
959    @Override
960    protected void onRestoreInstanceState(Parcelable state) {
961        SavedState savedState = (SavedState) state;
962        super.onRestoreInstanceState(savedState.getSuperState());
963        if (savedState.currentPage != -1) {
964            mCurrentPage = savedState.currentPage;
965        }
966    }
967
968    public void scrollLeft() {
969        if (mScroller.isFinished()) {
970            if (mCurrentPage > 0) snapToPage(mCurrentPage - 1);
971        } else {
972            if (mNextPage > 0) snapToPage(mNextPage - 1);
973        }
974    }
975
976    public void scrollRight() {
977        if (mScroller.isFinished()) {
978            if (mCurrentPage < getChildCount() -1) snapToPage(mCurrentPage + 1);
979        } else {
980            if (mNextPage < getChildCount() -1) snapToPage(mNextPage + 1);
981        }
982    }
983
984    public int getPageForView(View v) {
985        int result = -1;
986        if (v != null) {
987            ViewParent vp = v.getParent();
988            int count = getChildCount();
989            for (int i = 0; i < count; i++) {
990                if (vp == getChildAt(i)) {
991                    return i;
992                }
993            }
994        }
995        return result;
996    }
997
998    /**
999     * @return True is long presses are still allowed for the current touch
1000     */
1001    public boolean allowLongPress() {
1002        return mAllowLongPress;
1003    }
1004
1005    /**
1006     * Set true to allow long-press events to be triggered, usually checked by
1007     * {@link Launcher} to accept or block dpad-initiated long-presses.
1008     */
1009    public void setAllowLongPress(boolean allowLongPress) {
1010        mAllowLongPress = allowLongPress;
1011    }
1012
1013    public static class SavedState extends BaseSavedState {
1014        int currentPage = -1;
1015
1016        SavedState(Parcelable superState) {
1017            super(superState);
1018        }
1019
1020        private SavedState(Parcel in) {
1021            super(in);
1022            currentPage = in.readInt();
1023        }
1024
1025        @Override
1026        public void writeToParcel(Parcel out, int flags) {
1027            super.writeToParcel(out, flags);
1028            out.writeInt(currentPage);
1029        }
1030
1031        public static final Parcelable.Creator<SavedState> CREATOR =
1032                new Parcelable.Creator<SavedState>() {
1033            public SavedState createFromParcel(Parcel in) {
1034                return new SavedState(in);
1035            }
1036
1037            public SavedState[] newArray(int size) {
1038                return new SavedState[size];
1039            }
1040        };
1041    }
1042
1043    public void loadAssociatedPages(int page) {
1044        if (mContentIsRefreshable) {
1045            final int count = getChildCount();
1046            if (page < count) {
1047                int lowerPageBound = getAssociatedLowerPageBound(page);
1048                int upperPageBound = getAssociatedUpperPageBound(page);
1049                for (int i = 0; i < count; ++i) {
1050                    final ViewGroup layout = (ViewGroup) getChildAt(i);
1051                    final int childCount = layout.getChildCount();
1052                    if (lowerPageBound <= i && i <= upperPageBound) {
1053                        if (mDirtyPageContent.get(i)) {
1054                            syncPageItems(i);
1055                            mDirtyPageContent.set(i, false);
1056                        }
1057                    } else {
1058                        if (childCount > 0) {
1059                            layout.removeAllViews();
1060                        }
1061                        mDirtyPageContent.set(i, true);
1062                    }
1063                }
1064            }
1065        }
1066    }
1067
1068    protected int getAssociatedLowerPageBound(int page) {
1069        return Math.max(0, page - 1);
1070    }
1071    protected int getAssociatedUpperPageBound(int page) {
1072        final int count = getChildCount();
1073        return Math.min(page + 1, count - 1);
1074    }
1075
1076    protected void startChoiceMode(int mode, ActionMode.Callback callback) {
1077        if (isChoiceMode(CHOICE_MODE_NONE)) {
1078            mChoiceMode = mode;
1079            mActionMode = startActionMode(callback);
1080        }
1081    }
1082
1083    public void endChoiceMode() {
1084        if (!isChoiceMode(CHOICE_MODE_NONE)) {
1085            mChoiceMode = CHOICE_MODE_NONE;
1086            resetCheckedGrandchildren();
1087            mActionMode.finish();
1088            mActionMode = null;
1089        }
1090    }
1091
1092    protected boolean isChoiceMode(int mode) {
1093        return mChoiceMode == mode;
1094    }
1095
1096    protected ArrayList<Checkable> getCheckedGrandchildren() {
1097        ArrayList<Checkable> checked = new ArrayList<Checkable>();
1098        final int childCount = getChildCount();
1099        for (int i = 0; i < childCount; ++i) {
1100            final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
1101            final int grandChildCount = layout.getChildCount();
1102            for (int j = 0; j < grandChildCount; ++j) {
1103                final View v = layout.getChildAt(j);
1104                if (v instanceof Checkable && ((Checkable) v).isChecked()) {
1105                    checked.add((Checkable) v);
1106                }
1107            }
1108        }
1109        return checked;
1110    }
1111
1112    /**
1113     * If in CHOICE_MODE_SINGLE and an item is checked, returns that item.
1114     * Otherwise, returns null.
1115     */
1116    protected Checkable getSingleCheckedGrandchild() {
1117        if (mChoiceMode == CHOICE_MODE_SINGLE) {
1118            final int childCount = getChildCount();
1119            for (int i = 0; i < childCount; ++i) {
1120                final PagedViewCellLayout layout = (PagedViewCellLayout) getChildAt(i);
1121                final int grandChildCount = layout.getChildCount();
1122                for (int j = 0; j < grandChildCount; ++j) {
1123                    final View v = layout.getChildAt(j);
1124                    if (v instanceof Checkable && ((Checkable) v).isChecked()) {
1125                        return (Checkable) v;
1126                    }
1127                }
1128            }
1129        }
1130        return null;
1131    }
1132
1133    public Object getChosenItem() {
1134        View checkedView = (View) getSingleCheckedGrandchild();
1135        if (checkedView != null) {
1136            return checkedView.getTag();
1137        }
1138        return null;
1139    }
1140
1141    protected void resetCheckedGrandchildren() {
1142        // loop through children, and set all of their children to _not_ be checked
1143        final ArrayList<Checkable> checked = getCheckedGrandchildren();
1144        for (int i = 0; i < checked.size(); ++i) {
1145            final Checkable c = checked.get(i);
1146            c.setChecked(false);
1147        }
1148    }
1149
1150    /**
1151     * This method is called ONLY to synchronize the number of pages that the paged view has.
1152     * To actually fill the pages with information, implement syncPageItems() below.  It is
1153     * guaranteed that syncPageItems() will be called for a particular page before it is shown,
1154     * and therefore, individual page items do not need to be updated in this method.
1155     */
1156    public abstract void syncPages();
1157
1158    /**
1159     * This method is called to synchronize the items that are on a particular page.  If views on
1160     * the page can be reused, then they should be updated within this method.
1161     */
1162    public abstract void syncPageItems(int page);
1163
1164    public void invalidatePageData() {
1165        if (mContentIsRefreshable) {
1166            // Update all the pages
1167            syncPages();
1168
1169            // Mark each of the pages as dirty
1170            final int count = getChildCount();
1171            mDirtyPageContent.clear();
1172            for (int i = 0; i < count; ++i) {
1173                mDirtyPageContent.add(true);
1174            }
1175
1176            // Load any pages that are necessary for the current window of views
1177            loadAssociatedPages(mCurrentPage);
1178            mDirtyPageAlpha = true;
1179            requestLayout();
1180        }
1181    }
1182}
1183