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