1/* Copyright (C) 2010 The Android Open Source Project
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16package android.widget;
17
18import java.lang.ref.WeakReference;
19
20import android.animation.ObjectAnimator;
21import android.animation.PropertyValuesHolder;
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.graphics.Bitmap;
25import android.graphics.BlurMaskFilter;
26import android.graphics.Canvas;
27import android.graphics.Matrix;
28import android.graphics.Paint;
29import android.graphics.PorterDuff;
30import android.graphics.PorterDuffXfermode;
31import android.graphics.Rect;
32import android.graphics.RectF;
33import android.graphics.Region;
34import android.graphics.TableMaskFilter;
35import android.os.Bundle;
36import android.util.AttributeSet;
37import android.util.Log;
38import android.view.InputDevice;
39import android.view.MotionEvent;
40import android.view.VelocityTracker;
41import android.view.View;
42import android.view.ViewConfiguration;
43import android.view.ViewGroup;
44import android.view.accessibility.AccessibilityEvent;
45import android.view.accessibility.AccessibilityNodeInfo;
46import android.view.animation.LinearInterpolator;
47import android.widget.RemoteViews.RemoteView;
48
49@RemoteView
50/**
51 * A view that displays its children in a stack and allows users to discretely swipe
52 * through the children.
53 */
54public class StackView extends AdapterViewAnimator {
55    private final String TAG = "StackView";
56
57    /**
58     * Default animation parameters
59     */
60    private static final int DEFAULT_ANIMATION_DURATION = 400;
61    private static final int MINIMUM_ANIMATION_DURATION = 50;
62    private static final int STACK_RELAYOUT_DURATION = 100;
63
64    /**
65     * Parameters effecting the perspective visuals
66     */
67    private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
68    private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
69
70    private float mPerspectiveShiftX;
71    private float mPerspectiveShiftY;
72    private float mNewPerspectiveShiftX;
73    private float mNewPerspectiveShiftY;
74
75    @SuppressWarnings({"FieldCanBeLocal"})
76    private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
77
78    /**
79     * Represent the two possible stack modes, one where items slide up, and the other
80     * where items slide down. The perspective is also inverted between these two modes.
81     */
82    private static final int ITEMS_SLIDE_UP = 0;
83    private static final int ITEMS_SLIDE_DOWN = 1;
84
85    /**
86     * These specify the different gesture states
87     */
88    private static final int GESTURE_NONE = 0;
89    private static final int GESTURE_SLIDE_UP = 1;
90    private static final int GESTURE_SLIDE_DOWN = 2;
91
92    /**
93     * Specifies how far you need to swipe (up or down) before it
94     * will be consider a completed gesture when you lift your finger
95     */
96    private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
97
98    /**
99     * Specifies the total distance, relative to the size of the stack,
100     * that views will be slid, either up or down
101     */
102    private static final float SLIDE_UP_RATIO = 0.7f;
103
104    /**
105     * Sentinel value for no current active pointer.
106     * Used by {@link #mActivePointerId}.
107     */
108    private static final int INVALID_POINTER = -1;
109
110    /**
111     * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
112     */
113    private static final int NUM_ACTIVE_VIEWS = 5;
114
115    private static final int FRAME_PADDING = 4;
116
117    private final Rect mTouchRect = new Rect();
118
119    private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
120
121    private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
122
123    /**
124     * These variables are all related to the current state of touch interaction
125     * with the stack
126     */
127    private float mInitialY;
128    private float mInitialX;
129    private int mActivePointerId;
130    private int mYVelocity = 0;
131    private int mSwipeGestureType = GESTURE_NONE;
132    private int mSlideAmount;
133    private int mSwipeThreshold;
134    private int mTouchSlop;
135    private int mMaximumVelocity;
136    private VelocityTracker mVelocityTracker;
137    private boolean mTransitionIsSetup = false;
138    private int mResOutColor;
139    private int mClickColor;
140
141    private static HolographicHelper sHolographicHelper;
142    private ImageView mHighlight;
143    private ImageView mClickFeedback;
144    private boolean mClickFeedbackIsValid = false;
145    private StackSlider mStackSlider;
146    private boolean mFirstLayoutHappened = false;
147    private long mLastInteractionTime = 0;
148    private long mLastScrollTime;
149    private int mStackMode;
150    private int mFramePadding;
151    private final Rect stackInvalidateRect = new Rect();
152
153    /**
154     * {@inheritDoc}
155     */
156    public StackView(Context context) {
157        this(context, null);
158    }
159
160    /**
161     * {@inheritDoc}
162     */
163    public StackView(Context context, AttributeSet attrs) {
164        this(context, attrs, com.android.internal.R.attr.stackViewStyle);
165    }
166
167    /**
168     * {@inheritDoc}
169     */
170    public StackView(Context context, AttributeSet attrs, int defStyleAttr) {
171        this(context, attrs, defStyleAttr, 0);
172    }
173
174    /**
175     * {@inheritDoc}
176     */
177    public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
178        super(context, attrs, defStyleAttr, defStyleRes);
179        final TypedArray a = context.obtainStyledAttributes(
180                attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes);
181
182        mResOutColor = a.getColor(
183                com.android.internal.R.styleable.StackView_resOutColor, 0);
184        mClickColor = a.getColor(
185                com.android.internal.R.styleable.StackView_clickColor, 0);
186
187        a.recycle();
188        initStackView();
189    }
190
191    private void initStackView() {
192        configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
193        setStaticTransformationsEnabled(true);
194        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
195        mTouchSlop = configuration.getScaledTouchSlop();
196        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
197        mActivePointerId = INVALID_POINTER;
198
199        mHighlight = new ImageView(getContext());
200        mHighlight.setLayoutParams(new LayoutParams(mHighlight));
201        addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
202
203        mClickFeedback = new ImageView(getContext());
204        mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
205        addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
206        mClickFeedback.setVisibility(INVISIBLE);
207
208        mStackSlider = new StackSlider();
209
210        if (sHolographicHelper == null) {
211            sHolographicHelper = new HolographicHelper(mContext);
212        }
213        setClipChildren(false);
214        setClipToPadding(false);
215
216        // This sets the form of the StackView, which is currently to have the perspective-shifted
217        // views above the active view, and have items slide down when sliding out. The opposite is
218        // available by using ITEMS_SLIDE_UP.
219        mStackMode = ITEMS_SLIDE_DOWN;
220
221        // This is a flag to indicate the the stack is loading for the first time
222        mWhichChild = -1;
223
224        // Adjust the frame padding based on the density, since the highlight changes based
225        // on the density
226        final float density = mContext.getResources().getDisplayMetrics().density;
227        mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
228    }
229
230    /**
231     * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
232     */
233    void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
234        if (!animate) {
235            ((StackFrame) view).cancelSliderAnimator();
236            view.setRotationX(0f);
237            LayoutParams lp = (LayoutParams) view.getLayoutParams();
238            lp.setVerticalOffset(0);
239            lp.setHorizontalOffset(0);
240        }
241
242        if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
243            transformViewAtIndex(toIndex, view, false);
244            view.setVisibility(VISIBLE);
245            view.setAlpha(1.0f);
246        } else if (fromIndex == 0 && toIndex == 1) {
247            // Slide item in
248            ((StackFrame) view).cancelSliderAnimator();
249            view.setVisibility(VISIBLE);
250
251            int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
252            StackSlider animationSlider = new StackSlider(mStackSlider);
253            animationSlider.setView(view);
254
255            if (animate) {
256                PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
257                PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
258                ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
259                        slideInX, slideInY);
260                slideIn.setDuration(duration);
261                slideIn.setInterpolator(new LinearInterpolator());
262                ((StackFrame) view).setSliderAnimator(slideIn);
263                slideIn.start();
264            } else {
265                animationSlider.setYProgress(0f);
266                animationSlider.setXProgress(0f);
267            }
268        } else if (fromIndex == 1 && toIndex == 0) {
269            // Slide item out
270            ((StackFrame) view).cancelSliderAnimator();
271            int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
272
273            StackSlider animationSlider = new StackSlider(mStackSlider);
274            animationSlider.setView(view);
275            if (animate) {
276                PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
277                PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
278                ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
279                        slideOutX, slideOutY);
280                slideOut.setDuration(duration);
281                slideOut.setInterpolator(new LinearInterpolator());
282                ((StackFrame) view).setSliderAnimator(slideOut);
283                slideOut.start();
284            } else {
285                animationSlider.setYProgress(1.0f);
286                animationSlider.setXProgress(0f);
287            }
288        } else if (toIndex == 0) {
289            // Make sure this view that is "waiting in the wings" is invisible
290            view.setAlpha(0.0f);
291            view.setVisibility(INVISIBLE);
292        } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
293            view.setVisibility(VISIBLE);
294            view.setAlpha(1.0f);
295            view.setRotationX(0f);
296            LayoutParams lp = (LayoutParams) view.getLayoutParams();
297            lp.setVerticalOffset(0);
298            lp.setHorizontalOffset(0);
299        } else if (fromIndex == -1) {
300            view.setAlpha(1.0f);
301            view.setVisibility(VISIBLE);
302        } else if (toIndex == -1) {
303            if (animate) {
304                postDelayed(new Runnable() {
305                    public void run() {
306                        view.setAlpha(0);
307                    }
308                }, STACK_RELAYOUT_DURATION);
309            } else {
310                view.setAlpha(0f);
311            }
312        }
313
314        // Implement the faked perspective
315        if (toIndex != -1) {
316            transformViewAtIndex(toIndex, view, animate);
317        }
318    }
319
320    private void transformViewAtIndex(int index, final View view, boolean animate) {
321        final float maxPerspectiveShiftY = mPerspectiveShiftY;
322        final float maxPerspectiveShiftX = mPerspectiveShiftX;
323
324        if (mStackMode == ITEMS_SLIDE_DOWN) {
325            index = mMaxNumActiveViews - index - 1;
326            if (index == mMaxNumActiveViews - 1) index--;
327        } else {
328            index--;
329            if (index < 0) index++;
330        }
331
332        float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
333
334        final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
335
336        float perspectiveTranslationY = r * maxPerspectiveShiftY;
337        float scaleShiftCorrectionY = (scale - 1) *
338                (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
339        final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
340
341        float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
342        float scaleShiftCorrectionX =  (1 - scale) *
343                (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
344        final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
345
346        // If this view is currently being animated for a certain position, we need to cancel
347        // this animation so as not to interfere with the new transformation.
348        if (view instanceof StackFrame) {
349            ((StackFrame) view).cancelTransformAnimator();
350        }
351
352        if (animate) {
353            PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
354            PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
355            PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
356            PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
357
358            ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
359                    translationY, translationX);
360            oa.setDuration(STACK_RELAYOUT_DURATION);
361            if (view instanceof StackFrame) {
362                ((StackFrame) view).setTransformAnimator(oa);
363            }
364            oa.start();
365        } else {
366            view.setTranslationX(transX);
367            view.setTranslationY(transY);
368            view.setScaleX(scale);
369            view.setScaleY(scale);
370        }
371    }
372
373    private void setupStackSlider(View v, int mode) {
374        mStackSlider.setMode(mode);
375        if (v != null) {
376            mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
377            mHighlight.setRotation(v.getRotation());
378            mHighlight.setTranslationY(v.getTranslationY());
379            mHighlight.setTranslationX(v.getTranslationX());
380            mHighlight.bringToFront();
381            v.bringToFront();
382            mStackSlider.setView(v);
383
384            v.setVisibility(VISIBLE);
385        }
386    }
387
388    /**
389     * {@inheritDoc}
390     */
391    @Override
392    @android.view.RemotableViewMethod
393    public void showNext() {
394        if (mSwipeGestureType != GESTURE_NONE) return;
395        if (!mTransitionIsSetup) {
396            View v = getViewAtRelativeIndex(1);
397            if (v != null) {
398                setupStackSlider(v, StackSlider.NORMAL_MODE);
399                mStackSlider.setYProgress(0);
400                mStackSlider.setXProgress(0);
401            }
402        }
403        super.showNext();
404    }
405
406    /**
407     * {@inheritDoc}
408     */
409    @Override
410    @android.view.RemotableViewMethod
411    public void showPrevious() {
412        if (mSwipeGestureType != GESTURE_NONE) return;
413        if (!mTransitionIsSetup) {
414            View v = getViewAtRelativeIndex(0);
415            if (v != null) {
416                setupStackSlider(v, StackSlider.NORMAL_MODE);
417                mStackSlider.setYProgress(1);
418                mStackSlider.setXProgress(0);
419            }
420        }
421        super.showPrevious();
422    }
423
424    @Override
425    void showOnly(int childIndex, boolean animate) {
426        super.showOnly(childIndex, animate);
427
428        // Here we need to make sure that the z-order of the children is correct
429        for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
430            int index = modulo(i, getWindowSize());
431            ViewAndMetaData vm = mViewsMap.get(index);
432            if (vm != null) {
433                View v = mViewsMap.get(index).view;
434                if (v != null) v.bringToFront();
435            }
436        }
437        if (mHighlight != null) {
438            mHighlight.bringToFront();
439        }
440        mTransitionIsSetup = false;
441        mClickFeedbackIsValid = false;
442    }
443
444    void updateClickFeedback() {
445        if (!mClickFeedbackIsValid) {
446            View v = getViewAtRelativeIndex(1);
447            if (v != null) {
448                mClickFeedback.setImageBitmap(
449                        sHolographicHelper.createClickOutline(v, mClickColor));
450                mClickFeedback.setTranslationX(v.getTranslationX());
451                mClickFeedback.setTranslationY(v.getTranslationY());
452            }
453            mClickFeedbackIsValid = true;
454        }
455    }
456
457    @Override
458    void showTapFeedback(View v) {
459        updateClickFeedback();
460        mClickFeedback.setVisibility(VISIBLE);
461        mClickFeedback.bringToFront();
462        invalidate();
463    }
464
465    @Override
466    void hideTapFeedback(View v) {
467        mClickFeedback.setVisibility(INVISIBLE);
468        invalidate();
469    }
470
471    private void updateChildTransforms() {
472        for (int i = 0; i < getNumActiveViews(); i++) {
473            View v = getViewAtRelativeIndex(i);
474            if (v != null) {
475                transformViewAtIndex(i, v, false);
476            }
477        }
478    }
479
480    private static class StackFrame extends FrameLayout {
481        WeakReference<ObjectAnimator> transformAnimator;
482        WeakReference<ObjectAnimator> sliderAnimator;
483
484        public StackFrame(Context context) {
485            super(context);
486        }
487
488        void setTransformAnimator(ObjectAnimator oa) {
489            transformAnimator = new WeakReference<ObjectAnimator>(oa);
490        }
491
492        void setSliderAnimator(ObjectAnimator oa) {
493            sliderAnimator = new WeakReference<ObjectAnimator>(oa);
494        }
495
496        boolean cancelTransformAnimator() {
497            if (transformAnimator != null) {
498                ObjectAnimator oa = transformAnimator.get();
499                if (oa != null) {
500                    oa.cancel();
501                    return true;
502                }
503            }
504            return false;
505        }
506
507        boolean cancelSliderAnimator() {
508            if (sliderAnimator != null) {
509                ObjectAnimator oa = sliderAnimator.get();
510                if (oa != null) {
511                    oa.cancel();
512                    return true;
513                }
514            }
515            return false;
516        }
517    }
518
519    @Override
520    FrameLayout getFrameForChild() {
521        StackFrame fl = new StackFrame(mContext);
522        fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
523        return fl;
524    }
525
526    /**
527     * Apply any necessary tranforms for the child that is being added.
528     */
529    void applyTransformForChildAtIndex(View child, int relativeIndex) {
530    }
531
532    @Override
533    protected void dispatchDraw(Canvas canvas) {
534        boolean expandClipRegion = false;
535
536        canvas.getClipBounds(stackInvalidateRect);
537        final int childCount = getChildCount();
538        for (int i = 0; i < childCount; i++) {
539            final View child =  getChildAt(i);
540            LayoutParams lp = (LayoutParams) child.getLayoutParams();
541            if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
542                    child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
543                lp.resetInvalidateRect();
544            }
545            Rect childInvalidateRect = lp.getInvalidateRect();
546            if (!childInvalidateRect.isEmpty()) {
547                expandClipRegion = true;
548                stackInvalidateRect.union(childInvalidateRect);
549            }
550        }
551
552        // We only expand the clip bounds if necessary.
553        if (expandClipRegion) {
554            canvas.save(Canvas.CLIP_SAVE_FLAG);
555            canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
556            super.dispatchDraw(canvas);
557            canvas.restore();
558        } else {
559            super.dispatchDraw(canvas);
560        }
561    }
562
563    private void onLayout() {
564        if (!mFirstLayoutHappened) {
565            mFirstLayoutHappened = true;
566            updateChildTransforms();
567        }
568
569        final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
570        if (mSlideAmount != newSlideAmount) {
571            mSlideAmount = newSlideAmount;
572            mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
573        }
574
575        if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
576                Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
577
578            mPerspectiveShiftY = mNewPerspectiveShiftY;
579            mPerspectiveShiftX = mNewPerspectiveShiftX;
580            updateChildTransforms();
581        }
582    }
583
584    @Override
585    public boolean onGenericMotionEvent(MotionEvent event) {
586        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
587            switch (event.getAction()) {
588                case MotionEvent.ACTION_SCROLL: {
589                    final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
590                    if (vscroll < 0) {
591                        pacedScroll(false);
592                        return true;
593                    } else if (vscroll > 0) {
594                        pacedScroll(true);
595                        return true;
596                    }
597                }
598            }
599        }
600        return super.onGenericMotionEvent(event);
601    }
602
603    // This ensures that the frequency of stack flips caused by scrolls is capped
604    private void pacedScroll(boolean up) {
605        long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
606        if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
607            if (up) {
608                showPrevious();
609            } else {
610                showNext();
611            }
612            mLastScrollTime = System.currentTimeMillis();
613        }
614    }
615
616    /**
617     * {@inheritDoc}
618     */
619    @Override
620    public boolean onInterceptTouchEvent(MotionEvent ev) {
621        int action = ev.getAction();
622        switch(action & MotionEvent.ACTION_MASK) {
623            case MotionEvent.ACTION_DOWN: {
624                if (mActivePointerId == INVALID_POINTER) {
625                    mInitialX = ev.getX();
626                    mInitialY = ev.getY();
627                    mActivePointerId = ev.getPointerId(0);
628                }
629                break;
630            }
631            case MotionEvent.ACTION_MOVE: {
632                int pointerIndex = ev.findPointerIndex(mActivePointerId);
633                if (pointerIndex == INVALID_POINTER) {
634                    // no data for our primary pointer, this shouldn't happen, log it
635                    Log.d(TAG, "Error: No data for our primary pointer.");
636                    return false;
637                }
638                float newY = ev.getY(pointerIndex);
639                float deltaY = newY - mInitialY;
640
641                beginGestureIfNeeded(deltaY);
642                break;
643            }
644            case MotionEvent.ACTION_POINTER_UP: {
645                onSecondaryPointerUp(ev);
646                break;
647            }
648            case MotionEvent.ACTION_UP:
649            case MotionEvent.ACTION_CANCEL: {
650                mActivePointerId = INVALID_POINTER;
651                mSwipeGestureType = GESTURE_NONE;
652            }
653        }
654
655        return mSwipeGestureType != GESTURE_NONE;
656    }
657
658    private void beginGestureIfNeeded(float deltaY) {
659        if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
660            final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
661            cancelLongPress();
662            requestDisallowInterceptTouchEvent(true);
663
664            if (mAdapter == null) return;
665            final int adapterCount = getCount();
666
667            int activeIndex;
668            if (mStackMode == ITEMS_SLIDE_UP) {
669                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
670            } else {
671                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
672            }
673
674            boolean endOfStack = mLoopViews && adapterCount == 1 &&
675                ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP) ||
676                 (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
677            boolean beginningOfStack = mLoopViews && adapterCount == 1 &&
678                ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP) ||
679                 (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
680
681            int stackMode;
682            if (mLoopViews && !beginningOfStack && !endOfStack) {
683                stackMode = StackSlider.NORMAL_MODE;
684            } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
685                activeIndex++;
686                stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
687            } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
688                stackMode = StackSlider.END_OF_STACK_MODE;
689            } else {
690                stackMode = StackSlider.NORMAL_MODE;
691            }
692
693            mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
694
695            View v = getViewAtRelativeIndex(activeIndex);
696            if (v == null) return;
697
698            setupStackSlider(v, stackMode);
699
700            // We only register this gesture if we've made it this far without a problem
701            mSwipeGestureType = swipeGestureType;
702            cancelHandleClick();
703        }
704    }
705
706    /**
707     * {@inheritDoc}
708     */
709    @Override
710    public boolean onTouchEvent(MotionEvent ev) {
711        super.onTouchEvent(ev);
712
713        int action = ev.getAction();
714        int pointerIndex = ev.findPointerIndex(mActivePointerId);
715        if (pointerIndex == INVALID_POINTER) {
716            // no data for our primary pointer, this shouldn't happen, log it
717            Log.d(TAG, "Error: No data for our primary pointer.");
718            return false;
719        }
720
721        float newY = ev.getY(pointerIndex);
722        float newX = ev.getX(pointerIndex);
723        float deltaY = newY - mInitialY;
724        float deltaX = newX - mInitialX;
725        if (mVelocityTracker == null) {
726            mVelocityTracker = VelocityTracker.obtain();
727        }
728        mVelocityTracker.addMovement(ev);
729
730        switch (action & MotionEvent.ACTION_MASK) {
731            case MotionEvent.ACTION_MOVE: {
732                beginGestureIfNeeded(deltaY);
733
734                float rx = deltaX / (mSlideAmount * 1.0f);
735                if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
736                    float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
737                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
738                    mStackSlider.setYProgress(1 - r);
739                    mStackSlider.setXProgress(rx);
740                    return true;
741                } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
742                    float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
743                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
744                    mStackSlider.setYProgress(r);
745                    mStackSlider.setXProgress(rx);
746                    return true;
747                }
748                break;
749            }
750            case MotionEvent.ACTION_UP: {
751                handlePointerUp(ev);
752                break;
753            }
754            case MotionEvent.ACTION_POINTER_UP: {
755                onSecondaryPointerUp(ev);
756                break;
757            }
758            case MotionEvent.ACTION_CANCEL: {
759                mActivePointerId = INVALID_POINTER;
760                mSwipeGestureType = GESTURE_NONE;
761                break;
762            }
763        }
764        return true;
765    }
766
767    private void onSecondaryPointerUp(MotionEvent ev) {
768        final int activePointerIndex = ev.getActionIndex();
769        final int pointerId = ev.getPointerId(activePointerIndex);
770        if (pointerId == mActivePointerId) {
771
772            int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
773
774            View v = getViewAtRelativeIndex(activeViewIndex);
775            if (v == null) return;
776
777            // Our primary pointer has gone up -- let's see if we can find
778            // another pointer on the view. If so, then we should replace
779            // our primary pointer with this new pointer and adjust things
780            // so that the view doesn't jump
781            for (int index = 0; index < ev.getPointerCount(); index++) {
782                if (index != activePointerIndex) {
783
784                    float x = ev.getX(index);
785                    float y = ev.getY(index);
786
787                    mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
788                    if (mTouchRect.contains(Math.round(x), Math.round(y))) {
789                        float oldX = ev.getX(activePointerIndex);
790                        float oldY = ev.getY(activePointerIndex);
791
792                        // adjust our frame of reference to avoid a jump
793                        mInitialY += (y - oldY);
794                        mInitialX += (x - oldX);
795
796                        mActivePointerId = ev.getPointerId(index);
797                        if (mVelocityTracker != null) {
798                            mVelocityTracker.clear();
799                        }
800                        // ok, we're good, we found a new pointer which is touching the active view
801                        return;
802                    }
803                }
804            }
805            // if we made it this far, it means we didn't find a satisfactory new pointer :(,
806            // so end the gesture
807            handlePointerUp(ev);
808        }
809    }
810
811    private void handlePointerUp(MotionEvent ev) {
812        int pointerIndex = ev.findPointerIndex(mActivePointerId);
813        float newY = ev.getY(pointerIndex);
814        int deltaY = (int) (newY - mInitialY);
815        mLastInteractionTime = System.currentTimeMillis();
816
817        if (mVelocityTracker != null) {
818            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
819            mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
820        }
821
822        if (mVelocityTracker != null) {
823            mVelocityTracker.recycle();
824            mVelocityTracker = null;
825        }
826
827        if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
828                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
829            // We reset the gesture variable, because otherwise we will ignore showPrevious() /
830            // showNext();
831            mSwipeGestureType = GESTURE_NONE;
832
833            // Swipe threshold exceeded, swipe down
834            if (mStackMode == ITEMS_SLIDE_UP) {
835                showPrevious();
836            } else {
837                showNext();
838            }
839            mHighlight.bringToFront();
840        } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
841                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
842            // We reset the gesture variable, because otherwise we will ignore showPrevious() /
843            // showNext();
844            mSwipeGestureType = GESTURE_NONE;
845
846            // Swipe threshold exceeded, swipe up
847            if (mStackMode == ITEMS_SLIDE_UP) {
848                showNext();
849            } else {
850                showPrevious();
851            }
852
853            mHighlight.bringToFront();
854        } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
855            // Didn't swipe up far enough, snap back down
856            int duration;
857            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
858            if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
859                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
860            } else {
861                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
862            }
863
864            StackSlider animationSlider = new StackSlider(mStackSlider);
865            PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
866            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
867            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
868                    snapBackX, snapBackY);
869            pa.setDuration(duration);
870            pa.setInterpolator(new LinearInterpolator());
871            pa.start();
872        } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
873            // Didn't swipe down far enough, snap back up
874            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
875            int duration;
876            if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
877                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
878            } else {
879                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
880            }
881
882            StackSlider animationSlider = new StackSlider(mStackSlider);
883            PropertyValuesHolder snapBackY =
884                    PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
885            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
886            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
887                    snapBackX, snapBackY);
888            pa.setDuration(duration);
889            pa.start();
890        }
891
892        mActivePointerId = INVALID_POINTER;
893        mSwipeGestureType = GESTURE_NONE;
894    }
895
896    private class StackSlider {
897        View mView;
898        float mYProgress;
899        float mXProgress;
900
901        static final int NORMAL_MODE = 0;
902        static final int BEGINNING_OF_STACK_MODE = 1;
903        static final int END_OF_STACK_MODE = 2;
904
905        int mMode = NORMAL_MODE;
906
907        public StackSlider() {
908        }
909
910        public StackSlider(StackSlider copy) {
911            mView = copy.mView;
912            mYProgress = copy.mYProgress;
913            mXProgress = copy.mXProgress;
914            mMode = copy.mMode;
915        }
916
917        private float cubic(float r) {
918            return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
919        }
920
921        private float highlightAlphaInterpolator(float r) {
922            float pivot = 0.4f;
923            if (r < pivot) {
924                return 0.85f * cubic(r / pivot);
925            } else {
926                return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
927            }
928        }
929
930        private float viewAlphaInterpolator(float r) {
931            float pivot = 0.3f;
932            if (r > pivot) {
933                return (r - pivot) / (1 - pivot);
934            } else {
935                return 0;
936            }
937        }
938
939        private float rotationInterpolator(float r) {
940            float pivot = 0.2f;
941            if (r < pivot) {
942                return 0;
943            } else {
944                return (r - pivot) / (1 - pivot);
945            }
946        }
947
948        void setView(View v) {
949            mView = v;
950        }
951
952        public void setYProgress(float r) {
953            // enforce r between 0 and 1
954            r = Math.min(1.0f, r);
955            r = Math.max(0, r);
956
957            mYProgress = r;
958            if (mView == null) return;
959
960            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
961            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
962
963            int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
964
965            // We need to prevent any clipping issues which may arise by setting a layer type.
966            // This doesn't come for free however, so we only want to enable it when required.
967            if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
968                if (mView.getLayerType() == LAYER_TYPE_NONE) {
969                    mView.setLayerType(LAYER_TYPE_HARDWARE, null);
970                }
971            } else {
972                if (mView.getLayerType() != LAYER_TYPE_NONE) {
973                    mView.setLayerType(LAYER_TYPE_NONE, null);
974                }
975            }
976
977            switch (mMode) {
978                case NORMAL_MODE:
979                    viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
980                    highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
981                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
982
983                    float alpha = viewAlphaInterpolator(1 - r);
984
985                    // We make sure that views which can't be seen (have 0 alpha) are also invisible
986                    // so that they don't interfere with click events.
987                    if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
988                        mView.setVisibility(VISIBLE);
989                    } else if (alpha == 0 && mView.getAlpha() != 0
990                            && mView.getVisibility() == VISIBLE) {
991                        mView.setVisibility(INVISIBLE);
992                    }
993
994                    mView.setAlpha(alpha);
995                    mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
996                    mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
997                    break;
998                case END_OF_STACK_MODE:
999                    r = r * 0.2f;
1000                    viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
1001                    highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
1002                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
1003                    break;
1004                case BEGINNING_OF_STACK_MODE:
1005                    r = (1-r) * 0.2f;
1006                    viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
1007                    highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
1008                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
1009                    break;
1010            }
1011        }
1012
1013        public void setXProgress(float r) {
1014            // enforce r between 0 and 1
1015            r = Math.min(2.0f, r);
1016            r = Math.max(-2.0f, r);
1017
1018            mXProgress = r;
1019
1020            if (mView == null) return;
1021            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1022            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
1023
1024            r *= 0.2f;
1025            viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
1026            highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
1027        }
1028
1029        void setMode(int mode) {
1030            mMode = mode;
1031        }
1032
1033        float getDurationForNeutralPosition() {
1034            return getDuration(false, 0);
1035        }
1036
1037        float getDurationForOffscreenPosition() {
1038            return getDuration(true, 0);
1039        }
1040
1041        float getDurationForNeutralPosition(float velocity) {
1042            return getDuration(false, velocity);
1043        }
1044
1045        float getDurationForOffscreenPosition(float velocity) {
1046            return getDuration(true, velocity);
1047        }
1048
1049        private float getDuration(boolean invert, float velocity) {
1050            if (mView != null) {
1051                final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1052
1053                float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
1054                        Math.pow(viewLp.verticalOffset, 2));
1055                float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
1056                        Math.pow(0.4f * mSlideAmount, 2));
1057
1058                if (velocity == 0) {
1059                    return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
1060                } else {
1061                    float duration = invert ? d / Math.abs(velocity) :
1062                            (maxd - d) / Math.abs(velocity);
1063                    if (duration < MINIMUM_ANIMATION_DURATION ||
1064                            duration > DEFAULT_ANIMATION_DURATION) {
1065                        return getDuration(invert, 0);
1066                    } else {
1067                        return duration;
1068                    }
1069                }
1070            }
1071            return 0;
1072        }
1073
1074        // Used for animations
1075        @SuppressWarnings({"UnusedDeclaration"})
1076        public float getYProgress() {
1077            return mYProgress;
1078        }
1079
1080        // Used for animations
1081        @SuppressWarnings({"UnusedDeclaration"})
1082        public float getXProgress() {
1083            return mXProgress;
1084        }
1085    }
1086
1087    LayoutParams createOrReuseLayoutParams(View v) {
1088        final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
1089        if (currentLp instanceof LayoutParams) {
1090            LayoutParams lp = (LayoutParams) currentLp;
1091            lp.setHorizontalOffset(0);
1092            lp.setVerticalOffset(0);
1093            lp.width = 0;
1094            lp.width = 0;
1095            return lp;
1096        }
1097        return new LayoutParams(v);
1098    }
1099
1100    @Override
1101    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1102        checkForAndHandleDataChanged();
1103
1104        final int childCount = getChildCount();
1105        for (int i = 0; i < childCount; i++) {
1106            final View child = getChildAt(i);
1107
1108            int childRight = mPaddingLeft + child.getMeasuredWidth();
1109            int childBottom = mPaddingTop + child.getMeasuredHeight();
1110            LayoutParams lp = (LayoutParams) child.getLayoutParams();
1111
1112            child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
1113                    childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
1114
1115        }
1116        onLayout();
1117    }
1118
1119    @Override
1120    public void advance() {
1121        long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
1122
1123        if (mAdapter == null) return;
1124        final int adapterCount = getCount();
1125        if (adapterCount == 1 && mLoopViews) return;
1126
1127        if (mSwipeGestureType == GESTURE_NONE &&
1128                timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
1129            showNext();
1130        }
1131    }
1132
1133    private void measureChildren() {
1134        final int count = getChildCount();
1135
1136        final int measuredWidth = getMeasuredWidth();
1137        final int measuredHeight = getMeasuredHeight();
1138
1139        final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
1140                - mPaddingLeft - mPaddingRight;
1141        final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
1142                - mPaddingTop - mPaddingBottom;
1143
1144        int maxWidth = 0;
1145        int maxHeight = 0;
1146
1147        for (int i = 0; i < count; i++) {
1148            final View child = getChildAt(i);
1149            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
1150                    MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
1151
1152            if (child != mHighlight && child != mClickFeedback) {
1153                final int childMeasuredWidth = child.getMeasuredWidth();
1154                final int childMeasuredHeight = child.getMeasuredHeight();
1155                if (childMeasuredWidth > maxWidth) {
1156                    maxWidth = childMeasuredWidth;
1157                }
1158                if (childMeasuredHeight > maxHeight) {
1159                    maxHeight = childMeasuredHeight;
1160                }
1161            }
1162        }
1163
1164        mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
1165        mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
1166
1167        // If we have extra space, we try and spread the items out
1168        if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
1169            mNewPerspectiveShiftX = measuredWidth - maxWidth;
1170        }
1171
1172        if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
1173            mNewPerspectiveShiftY = measuredHeight - maxHeight;
1174        }
1175    }
1176
1177    @Override
1178    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1179        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
1180        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
1181        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
1182        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
1183
1184        boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
1185
1186        // We need to deal with the case where our parent hasn't told us how
1187        // big we should be. In this case we should
1188        float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
1189        if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
1190            heightSpecSize = haveChildRefSize ?
1191                    Math.round(mReferenceChildHeight * (1 + factorY)) +
1192                    mPaddingTop + mPaddingBottom : 0;
1193        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
1194            if (haveChildRefSize) {
1195                int height = Math.round(mReferenceChildHeight * (1 + factorY))
1196                        + mPaddingTop + mPaddingBottom;
1197                if (height <= heightSpecSize) {
1198                    heightSpecSize = height;
1199                } else {
1200                    heightSpecSize |= MEASURED_STATE_TOO_SMALL;
1201
1202                }
1203            } else {
1204                heightSpecSize = 0;
1205            }
1206        }
1207
1208        float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
1209        if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
1210            widthSpecSize = haveChildRefSize ?
1211                    Math.round(mReferenceChildWidth * (1 + factorX)) +
1212                    mPaddingLeft + mPaddingRight : 0;
1213        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
1214            if (haveChildRefSize) {
1215                int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
1216                if (width <= widthSpecSize) {
1217                    widthSpecSize = width;
1218                } else {
1219                    widthSpecSize |= MEASURED_STATE_TOO_SMALL;
1220                }
1221            } else {
1222                widthSpecSize = 0;
1223            }
1224        }
1225        setMeasuredDimension(widthSpecSize, heightSpecSize);
1226        measureChildren();
1227    }
1228
1229    @Override
1230    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1231        super.onInitializeAccessibilityEvent(event);
1232        event.setClassName(StackView.class.getName());
1233    }
1234
1235    @Override
1236    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1237        super.onInitializeAccessibilityNodeInfo(info);
1238        info.setClassName(StackView.class.getName());
1239        info.setScrollable(getChildCount() > 1);
1240        if (isEnabled()) {
1241            if (getDisplayedChild() < getChildCount() - 1) {
1242                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
1243            }
1244            if (getDisplayedChild() > 0) {
1245                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
1246            }
1247        }
1248    }
1249
1250    @Override
1251    public boolean performAccessibilityAction(int action, Bundle arguments) {
1252        if (super.performAccessibilityAction(action, arguments)) {
1253            return true;
1254        }
1255        if (!isEnabled()) {
1256            return false;
1257        }
1258        switch (action) {
1259            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
1260                if (getDisplayedChild() < getChildCount() - 1) {
1261                    showNext();
1262                    return true;
1263                }
1264            } return false;
1265            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
1266                if (getDisplayedChild() > 0) {
1267                    showPrevious();
1268                    return true;
1269                }
1270            } return false;
1271        }
1272        return false;
1273    }
1274
1275    class LayoutParams extends ViewGroup.LayoutParams {
1276        int horizontalOffset;
1277        int verticalOffset;
1278        View mView;
1279        private final Rect parentRect = new Rect();
1280        private final Rect invalidateRect = new Rect();
1281        private final RectF invalidateRectf = new RectF();
1282        private final Rect globalInvalidateRect = new Rect();
1283
1284        LayoutParams(View view) {
1285            super(0, 0);
1286            width = 0;
1287            height = 0;
1288            horizontalOffset = 0;
1289            verticalOffset = 0;
1290            mView = view;
1291        }
1292
1293        LayoutParams(Context c, AttributeSet attrs) {
1294            super(c, attrs);
1295            horizontalOffset = 0;
1296            verticalOffset = 0;
1297            width = 0;
1298            height = 0;
1299        }
1300
1301        void invalidateGlobalRegion(View v, Rect r) {
1302            // We need to make a new rect here, so as not to modify the one passed
1303            globalInvalidateRect.set(r);
1304            globalInvalidateRect.union(0, 0, getWidth(), getHeight());
1305            View p = v;
1306            if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1307
1308            boolean firstPass = true;
1309            parentRect.set(0, 0, 0, 0);
1310            while (p.getParent() != null && p.getParent() instanceof View
1311                    && !parentRect.contains(globalInvalidateRect)) {
1312                if (!firstPass) {
1313                    globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1314                            - p.getScrollY());
1315                }
1316                firstPass = false;
1317                p = (View) p.getParent();
1318                parentRect.set(p.getScrollX(), p.getScrollY(),
1319                        p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1320                p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1321                        globalInvalidateRect.right, globalInvalidateRect.bottom);
1322            }
1323
1324            p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1325                    globalInvalidateRect.right, globalInvalidateRect.bottom);
1326        }
1327
1328        Rect getInvalidateRect() {
1329            return invalidateRect;
1330        }
1331
1332        void resetInvalidateRect() {
1333            invalidateRect.set(0, 0, 0, 0);
1334        }
1335
1336        // This is public so that ObjectAnimator can access it
1337        public void setVerticalOffset(int newVerticalOffset) {
1338            setOffsets(horizontalOffset, newVerticalOffset);
1339        }
1340
1341        public void setHorizontalOffset(int newHorizontalOffset) {
1342            setOffsets(newHorizontalOffset, verticalOffset);
1343        }
1344
1345        public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1346            int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1347            horizontalOffset = newHorizontalOffset;
1348            int verticalOffsetDelta = newVerticalOffset - verticalOffset;
1349            verticalOffset = newVerticalOffset;
1350
1351            if (mView != null) {
1352                mView.requestLayout();
1353                int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1354                int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1355                int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1356                int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
1357
1358                invalidateRectf.set(left, top, right, bottom);
1359
1360                float xoffset = -invalidateRectf.left;
1361                float yoffset = -invalidateRectf.top;
1362                invalidateRectf.offset(xoffset, yoffset);
1363                mView.getMatrix().mapRect(invalidateRectf);
1364                invalidateRectf.offset(-xoffset, -yoffset);
1365
1366                invalidateRect.set((int) Math.floor(invalidateRectf.left),
1367                        (int) Math.floor(invalidateRectf.top),
1368                        (int) Math.ceil(invalidateRectf.right),
1369                        (int) Math.ceil(invalidateRectf.bottom));
1370
1371                invalidateGlobalRegion(mView, invalidateRect);
1372            }
1373        }
1374    }
1375
1376    private static class HolographicHelper {
1377        private final Paint mHolographicPaint = new Paint();
1378        private final Paint mErasePaint = new Paint();
1379        private final Paint mBlurPaint = new Paint();
1380        private static final int RES_OUT = 0;
1381        private static final int CLICK_FEEDBACK = 1;
1382        private float mDensity;
1383        private BlurMaskFilter mSmallBlurMaskFilter;
1384        private BlurMaskFilter mLargeBlurMaskFilter;
1385        private final Canvas mCanvas = new Canvas();
1386        private final Canvas mMaskCanvas = new Canvas();
1387        private final int[] mTmpXY = new int[2];
1388        private final Matrix mIdentityMatrix = new Matrix();
1389
1390        HolographicHelper(Context context) {
1391            mDensity = context.getResources().getDisplayMetrics().density;
1392
1393            mHolographicPaint.setFilterBitmap(true);
1394            mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
1395            mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1396            mErasePaint.setFilterBitmap(true);
1397
1398            mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1399            mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
1400        }
1401
1402        Bitmap createClickOutline(View v, int color) {
1403            return createOutline(v, CLICK_FEEDBACK, color);
1404        }
1405
1406        Bitmap createResOutline(View v, int color) {
1407            return createOutline(v, RES_OUT, color);
1408        }
1409
1410        Bitmap createOutline(View v, int type, int color) {
1411            mHolographicPaint.setColor(color);
1412            if (type == RES_OUT) {
1413                mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
1414            } else if (type == CLICK_FEEDBACK) {
1415                mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
1416            }
1417
1418            if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1419                return null;
1420            }
1421
1422            Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
1423                    v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
1424            mCanvas.setBitmap(bitmap);
1425
1426            float rotationX = v.getRotationX();
1427            float rotation = v.getRotation();
1428            float translationY = v.getTranslationY();
1429            float translationX = v.getTranslationX();
1430            v.setRotationX(0);
1431            v.setRotation(0);
1432            v.setTranslationY(0);
1433            v.setTranslationX(0);
1434            v.draw(mCanvas);
1435            v.setRotationX(rotationX);
1436            v.setRotation(rotation);
1437            v.setTranslationY(translationY);
1438            v.setTranslationX(translationX);
1439
1440            drawOutline(mCanvas, bitmap);
1441            mCanvas.setBitmap(null);
1442            return bitmap;
1443        }
1444
1445        void drawOutline(Canvas dest, Bitmap src) {
1446            final int[] xy = mTmpXY;
1447            Bitmap mask = src.extractAlpha(mBlurPaint, xy);
1448            mMaskCanvas.setBitmap(mask);
1449            mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
1450            dest.drawColor(0, PorterDuff.Mode.CLEAR);
1451            dest.setMatrix(mIdentityMatrix);
1452            dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
1453            mMaskCanvas.setBitmap(null);
1454            mask.recycle();
1455        }
1456    }
1457}
1458