StackView.java revision a32edd4b4c894f4fb3d9fd7e9d5b80321df79e20
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 android.widget;
18
19import android.animation.ObjectAnimator;
20import android.animation.PropertyValuesHolder;
21import android.content.Context;
22import android.graphics.Bitmap;
23import android.graphics.BlurMaskFilter;
24import android.graphics.Canvas;
25import android.graphics.Matrix;
26import android.graphics.Paint;
27import android.graphics.PorterDuff;
28import android.graphics.PorterDuffXfermode;
29import android.graphics.Rect;
30import android.graphics.RectF;
31import android.graphics.Region;
32import android.graphics.TableMaskFilter;
33import android.util.AttributeSet;
34import android.util.Log;
35import android.view.MotionEvent;
36import android.view.VelocityTracker;
37import android.view.View;
38import android.view.ViewConfiguration;
39import android.view.ViewGroup;
40import android.view.animation.LinearInterpolator;
41import android.widget.RemoteViews.RemoteView;
42
43@RemoteView
44/**
45 * A view that displays its children in a stack and allows users to discretely swipe
46 * through the children.
47 */
48public class StackView extends AdapterViewAnimator {
49    private final String TAG = "StackView";
50
51    /**
52     * Default animation parameters
53     */
54    private static final int DEFAULT_ANIMATION_DURATION = 400;
55    private static final int MINIMUM_ANIMATION_DURATION = 50;
56
57    /**
58     * Parameters effecting the perspective visuals
59     */
60    private static float PERSPECTIVE_SHIFT_FACTOR = 0.12f;
61    @SuppressWarnings({"FieldCanBeLocal"})
62    private static float PERSPECTIVE_SCALE_FACTOR = 0.35f;
63
64    /**
65     * Represent the two possible stack modes, one where items slide up, and the other
66     * where items slide down. The perspective is also inverted between these two modes.
67     */
68    private static final int ITEMS_SLIDE_UP = 0;
69    private static final int ITEMS_SLIDE_DOWN = 1;
70
71    /**
72     * These specify the different gesture states
73     */
74    private static final int GESTURE_NONE = 0;
75    private static final int GESTURE_SLIDE_UP = 1;
76    private static final int GESTURE_SLIDE_DOWN = 2;
77
78    /**
79     * Specifies how far you need to swipe (up or down) before it
80     * will be consider a completed gesture when you lift your finger
81     */
82    private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
83
84    /**
85     * Specifies the total distance, relative to the size of the stack,
86     * that views will be slid, either up or down
87     */
88    private static final float SLIDE_UP_RATIO = 0.7f;
89
90    /**
91     * Sentinel value for no current active pointer.
92     * Used by {@link #mActivePointerId}.
93     */
94    private static final int INVALID_POINTER = -1;
95
96    /**
97     * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
98     */
99    private static final int NUM_ACTIVE_VIEWS = 5;
100
101    private static final int FRAME_PADDING = 4;
102
103    /**
104     * These variables are all related to the current state of touch interaction
105     * with the stack
106     */
107    private float mInitialY;
108    private float mInitialX;
109    private int mActivePointerId;
110    private int mYVelocity = 0;
111    private int mSwipeGestureType = GESTURE_NONE;
112    private int mSlideAmount;
113    private int mSwipeThreshold;
114    private int mTouchSlop;
115    private int mMaximumVelocity;
116    private VelocityTracker mVelocityTracker;
117    private boolean mTransitionIsSetup = false;
118
119    private static HolographicHelper sHolographicHelper;
120    private ImageView mHighlight;
121    private StackSlider mStackSlider;
122    private boolean mFirstLayoutHappened = false;
123    private int mStackMode;
124    private int mFramePadding;
125    private final Rect invalidateRect = new Rect();
126
127    public StackView(Context context) {
128        super(context);
129        initStackView();
130    }
131
132    public StackView(Context context, AttributeSet attrs) {
133        super(context, attrs);
134        initStackView();
135    }
136
137    private void initStackView() {
138        configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
139        setStaticTransformationsEnabled(true);
140        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
141        mTouchSlop = configuration.getScaledTouchSlop();
142        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
143        mActivePointerId = INVALID_POINTER;
144
145        mHighlight = new ImageView(getContext());
146        mHighlight.setLayoutParams(new LayoutParams(mHighlight));
147        addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
148        mStackSlider = new StackSlider();
149
150        if (sHolographicHelper == null) {
151            sHolographicHelper = new HolographicHelper(mContext);
152        }
153        setClipChildren(false);
154        setClipToPadding(false);
155
156        // This sets the form of the StackView, which is currently to have the perspective-shifted
157        // views above the active view, and have items slide down when sliding out. The opposite is
158        // available by using ITEMS_SLIDE_UP.
159        mStackMode = ITEMS_SLIDE_DOWN;
160
161        // This is a flag to indicate the the stack is loading for the first time
162        mWhichChild = -1;
163
164        // Adjust the frame padding based on the density, since the highlight changes based
165        // on the density
166        final float density = mContext.getResources().getDisplayMetrics().density;
167        mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
168    }
169
170    /**
171     * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
172     */
173    void animateViewForTransition(int fromIndex, int toIndex, View view) {
174        if (fromIndex == -1 && toIndex != 0) {
175            // Fade item in
176            if (view.getAlpha() == 1) {
177                view.setAlpha(0);
178            }
179            view.setVisibility(VISIBLE);
180
181            ObjectAnimator fadeIn = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 1.0f);
182            fadeIn.setDuration(DEFAULT_ANIMATION_DURATION);
183            fadeIn.start();
184        } else if (fromIndex == 0 && toIndex == 1) {
185            // Slide item in
186            view.setVisibility(VISIBLE);
187
188            int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
189
190            StackSlider animationSlider = new StackSlider(mStackSlider);
191            PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
192            PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
193            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
194                    slideInX, slideInY);
195            pa.setDuration(duration);
196            pa.setInterpolator(new LinearInterpolator());
197            pa.start();
198        } else if (fromIndex == 1 && toIndex == 0) {
199            // Slide item out
200            int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
201
202            StackSlider animationSlider = new StackSlider(mStackSlider);
203            PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
204            PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
205            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
206                    slideOutX, slideOutY);
207            pa.setDuration(duration);
208            pa.setInterpolator(new LinearInterpolator());
209            pa.start();
210        } else if (fromIndex == -1 && toIndex == 0) {
211            // Make sure this view that is "waiting in the wings" is invisible
212            view.setAlpha(0.0f);
213            view.setVisibility(INVISIBLE);
214            LayoutParams lp = (LayoutParams) view.getLayoutParams();
215            lp.setVerticalOffset(-mSlideAmount);
216        } else if (toIndex == -1) {
217            // Fade item out
218            ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 0.0f);
219            fadeOut.setDuration(DEFAULT_ANIMATION_DURATION);
220            fadeOut.start();
221        }
222
223        // Implement the faked perspective
224        if (toIndex != -1) {
225            transformViewAtIndex(toIndex, view);
226        }
227    }
228
229    private void setupStackSlider(View v, int mode) {
230        mStackSlider.setMode(mode);
231        if (v != null) {
232            mHighlight.setImageBitmap(sHolographicHelper.createOutline(v));
233            mHighlight.setRotation(v.getRotation());
234            mHighlight.setTranslationY(v.getTranslationY());
235            mHighlight.bringToFront();
236            v.bringToFront();
237            mStackSlider.setView(v);
238
239            v.setVisibility(VISIBLE);
240        }
241    }
242
243    @Override
244    @android.view.RemotableViewMethod
245    public void showNext() {
246        if (!mTransitionIsSetup) {
247            View v = getViewAtRelativeIndex(1);
248            if (v != null) {
249                setupStackSlider(v, StackSlider.NORMAL_MODE);
250                mStackSlider.setYProgress(0);
251                mStackSlider.setXProgress(0);
252            }
253        }
254        super.showNext();
255    }
256
257    @Override
258    @android.view.RemotableViewMethod
259    public void showPrevious() {
260        if (!mTransitionIsSetup) {
261            View v = getViewAtRelativeIndex(0);
262            if (v != null) {
263                setupStackSlider(v, StackSlider.NORMAL_MODE);
264                mStackSlider.setYProgress(1);
265                mStackSlider.setXProgress(0);
266            }
267        }
268        super.showPrevious();
269    }
270
271    private void transformViewAtIndex(int index, View view) {
272        float maxPerpectiveShift = mMeasuredHeight * PERSPECTIVE_SHIFT_FACTOR;
273
274        index = mMaxNumActiveViews - index - 1;
275        if (index == mMaxNumActiveViews - 1) index--;
276
277        float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
278
279        float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
280        PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", scale);
281        PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", scale);
282
283        r = (float) Math.pow(r, 2);
284
285        int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
286        float perspectiveTranslation = -stackDirection * r * maxPerpectiveShift;
287        float scaleShiftCorrection = stackDirection * (1 - scale) *
288                (mMeasuredHeight * (1 - PERSPECTIVE_SHIFT_FACTOR) / 2.0f);
289        float transY = perspectiveTranslation + scaleShiftCorrection;
290
291        PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
292        ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(view, scaleX, scaleY, translationY);
293        pa.setDuration(100);
294        pa.start();
295    }
296
297    @Override
298    void showOnly(int childIndex, boolean animate, boolean onLayout) {
299        super.showOnly(childIndex, animate, onLayout);
300
301        // Here we need to make sure that the z-order of the children is correct
302        for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
303            int index = modulo(i, getWindowSize());
304            View v = mViewsMap.get(index).view;
305            if (v != null) v.bringToFront();
306        }
307        mTransitionIsSetup = false;
308    }
309
310    private void updateChildTransforms() {
311        for (int i = 0; i < getNumActiveViews(); i++) {
312            View v = getViewAtRelativeIndex(i);
313            if (v != null) {
314                transformViewAtIndex(i, v);
315            }
316        }
317    }
318
319    @Override
320    FrameLayout getFrameForChild() {
321        FrameLayout fl = new FrameLayout(mContext);
322        fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
323        return fl;
324    }
325
326    /**
327     * Apply any necessary tranforms for the child that is being added.
328     */
329    void applyTransformForChildAtIndex(View child, int relativeIndex) {
330    }
331
332    @Override
333    protected void dispatchDraw(Canvas canvas) {
334        canvas.getClipBounds(invalidateRect);
335        final int childCount = getChildCount();
336        for (int i = 0; i < childCount; i++) {
337            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
338            invalidateRect.union(lp.getInvalidateRect());
339            lp.resetInvalidateRect();
340        }
341
342        canvas.save(Canvas.CLIP_SAVE_FLAG);
343        canvas.clipRect(invalidateRect, Region.Op.UNION);
344        super.dispatchDraw(canvas);
345        canvas.restore();
346    }
347
348    private void onLayout() {
349        if (!mFirstLayoutHappened) {
350            mSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
351            updateChildTransforms();
352            mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * mSlideAmount);
353            mFirstLayoutHappened = true;
354        }
355    }
356
357    @Override
358    public boolean onInterceptTouchEvent(MotionEvent ev) {
359        int action = ev.getAction();
360        switch(action & MotionEvent.ACTION_MASK) {
361            case MotionEvent.ACTION_DOWN: {
362                if (mActivePointerId == INVALID_POINTER) {
363                    mInitialX = ev.getX();
364                    mInitialY = ev.getY();
365                    mActivePointerId = ev.getPointerId(0);
366                }
367                break;
368            }
369            case MotionEvent.ACTION_MOVE: {
370                int pointerIndex = ev.findPointerIndex(mActivePointerId);
371                if (pointerIndex == INVALID_POINTER) {
372                    // no data for our primary pointer, this shouldn't happen, log it
373                    Log.d(TAG, "Error: No data for our primary pointer.");
374                    return false;
375                }
376                float newY = ev.getY(pointerIndex);
377                float deltaY = newY - mInitialY;
378
379                beginGestureIfNeeded(deltaY);
380                break;
381            }
382            case MotionEvent.ACTION_POINTER_UP: {
383                onSecondaryPointerUp(ev);
384                break;
385            }
386            case MotionEvent.ACTION_UP:
387            case MotionEvent.ACTION_CANCEL: {
388                mActivePointerId = INVALID_POINTER;
389                mSwipeGestureType = GESTURE_NONE;
390            }
391        }
392
393        return mSwipeGestureType != GESTURE_NONE;
394    }
395
396    private void beginGestureIfNeeded(float deltaY) {
397        if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
398            int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
399            cancelLongPress();
400            requestDisallowInterceptTouchEvent(true);
401
402            if (mAdapter == null) return;
403
404            int activeIndex;
405            if (mStackMode == ITEMS_SLIDE_UP) {
406                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
407            } else {
408                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
409            }
410
411            int stackMode;
412            if (mLoopViews) {
413                stackMode = StackSlider.NORMAL_MODE;
414            } else if (mCurrentWindowStartUnbounded + activeIndex == -1) {
415                activeIndex++;
416                stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
417            } else if (mCurrentWindowStartUnbounded + activeIndex == mAdapter.getCount() - 1) {
418                stackMode = StackSlider.END_OF_STACK_MODE;
419            } else {
420                stackMode = StackSlider.NORMAL_MODE;
421            }
422
423            mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
424
425            View v = getViewAtRelativeIndex(activeIndex);
426            if (v == null) return;
427
428            setupStackSlider(v, stackMode);
429
430            // We only register this gesture if we've made it this far without a problem
431            mSwipeGestureType = swipeGestureType;
432            cancelHandleClick();
433        }
434    }
435
436    @Override
437    public boolean onTouchEvent(MotionEvent ev) {
438        super.onTouchEvent(ev);
439
440        int action = ev.getAction();
441        int pointerIndex = ev.findPointerIndex(mActivePointerId);
442        if (pointerIndex == INVALID_POINTER) {
443            // no data for our primary pointer, this shouldn't happen, log it
444            Log.d(TAG, "Error: No data for our primary pointer.");
445            return false;
446        }
447
448        float newY = ev.getY(pointerIndex);
449        float newX = ev.getX(pointerIndex);
450        float deltaY = newY - mInitialY;
451        float deltaX = newX - mInitialX;
452        if (mVelocityTracker == null) {
453            mVelocityTracker = VelocityTracker.obtain();
454        }
455        mVelocityTracker.addMovement(ev);
456
457        switch (action & MotionEvent.ACTION_MASK) {
458            case MotionEvent.ACTION_MOVE: {
459                beginGestureIfNeeded(deltaY);
460
461                float rx = deltaX / (mSlideAmount * 1.0f);
462                if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
463                    float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
464                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
465                    mStackSlider.setYProgress(1 - r);
466                    mStackSlider.setXProgress(rx);
467                    return true;
468                } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
469                    float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
470                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
471                    mStackSlider.setYProgress(r);
472                    mStackSlider.setXProgress(rx);
473                    return true;
474                }
475                break;
476            }
477            case MotionEvent.ACTION_UP: {
478                handlePointerUp(ev);
479                break;
480            }
481            case MotionEvent.ACTION_POINTER_UP: {
482                onSecondaryPointerUp(ev);
483                break;
484            }
485            case MotionEvent.ACTION_CANCEL: {
486                mActivePointerId = INVALID_POINTER;
487                mSwipeGestureType = GESTURE_NONE;
488                break;
489            }
490        }
491        return true;
492    }
493
494    private final Rect touchRect = new Rect();
495    private void onSecondaryPointerUp(MotionEvent ev) {
496        final int activePointerIndex = ev.getActionIndex();
497        final int pointerId = ev.getPointerId(activePointerIndex);
498        if (pointerId == mActivePointerId) {
499
500            int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
501
502            View v = getViewAtRelativeIndex(activeViewIndex);
503            if (v == null) return;
504
505            // Our primary pointer has gone up -- let's see if we can find
506            // another pointer on the view. If so, then we should replace
507            // our primary pointer with this new pointer and adjust things
508            // so that the view doesn't jump
509            for (int index = 0; index < ev.getPointerCount(); index++) {
510                if (index != activePointerIndex) {
511
512                    float x = ev.getX(index);
513                    float y = ev.getY(index);
514
515                    touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
516                    if (touchRect.contains(Math.round(x), Math.round(y))) {
517                        float oldX = ev.getX(activePointerIndex);
518                        float oldY = ev.getY(activePointerIndex);
519
520                        // adjust our frame of reference to avoid a jump
521                        mInitialY += (y - oldY);
522                        mInitialX += (x - oldX);
523
524                        mActivePointerId = ev.getPointerId(index);
525                        if (mVelocityTracker != null) {
526                            mVelocityTracker.clear();
527                        }
528                        // ok, we're good, we found a new pointer which is touching the active view
529                        return;
530                    }
531                }
532            }
533            // if we made it this far, it means we didn't find a satisfactory new pointer :(,
534            // so end the gesture
535            handlePointerUp(ev);
536        }
537    }
538
539    private void handlePointerUp(MotionEvent ev) {
540        int pointerIndex = ev.findPointerIndex(mActivePointerId);
541        float newY = ev.getY(pointerIndex);
542        int deltaY = (int) (newY - mInitialY);
543
544        if (mVelocityTracker != null) {
545            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
546            mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
547        }
548
549        if (mVelocityTracker != null) {
550            mVelocityTracker.recycle();
551            mVelocityTracker = null;
552        }
553
554        if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
555                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
556            // Swipe threshold exceeded, swipe down
557            if (mStackMode == ITEMS_SLIDE_UP) {
558                showPrevious();
559            } else {
560                showNext();
561            }
562            mHighlight.bringToFront();
563        } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
564                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
565            // Swipe threshold exceeded, swipe up
566            if (mStackMode == ITEMS_SLIDE_UP) {
567                showNext();
568            } else {
569                showPrevious();
570            }
571
572            mHighlight.bringToFront();
573        } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
574            // Didn't swipe up far enough, snap back down
575            int duration;
576            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
577            if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
578                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
579            } else {
580                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
581            }
582
583            StackSlider animationSlider = new StackSlider(mStackSlider);
584            PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
585            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
586            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
587                    snapBackX, snapBackY);
588            pa.setDuration(duration);
589            pa.setInterpolator(new LinearInterpolator());
590            pa.start();
591        } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
592            // Didn't swipe down far enough, snap back up
593            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
594            int duration;
595            if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
596                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
597            } else {
598                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
599            }
600
601            StackSlider animationSlider = new StackSlider(mStackSlider);
602            PropertyValuesHolder snapBackY =
603                    PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
604            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
605            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
606                    snapBackX, snapBackY);
607            pa.setDuration(duration);
608            pa.start();
609        }
610
611        mActivePointerId = INVALID_POINTER;
612        mSwipeGestureType = GESTURE_NONE;
613    }
614
615    private class StackSlider {
616        View mView;
617        float mYProgress;
618        float mXProgress;
619
620        static final int NORMAL_MODE = 0;
621        static final int BEGINNING_OF_STACK_MODE = 1;
622        static final int END_OF_STACK_MODE = 2;
623
624        int mMode = NORMAL_MODE;
625
626        public StackSlider() {
627        }
628
629        public StackSlider(StackSlider copy) {
630            mView = copy.mView;
631            mYProgress = copy.mYProgress;
632            mXProgress = copy.mXProgress;
633            mMode = copy.mMode;
634        }
635
636        private float cubic(float r) {
637            return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
638        }
639
640        private float highlightAlphaInterpolator(float r) {
641            float pivot = 0.4f;
642            if (r < pivot) {
643                return 0.85f * cubic(r / pivot);
644            } else {
645                return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
646            }
647        }
648
649        private float viewAlphaInterpolator(float r) {
650            float pivot = 0.3f;
651            if (r > pivot) {
652                return (r - pivot) / (1 - pivot);
653            } else {
654                return 0;
655            }
656        }
657
658        private float rotationInterpolator(float r) {
659            float pivot = 0.2f;
660            if (r < pivot) {
661                return 0;
662            } else {
663                return (r - pivot) / (1 - pivot);
664            }
665        }
666
667        void setView(View v) {
668            mView = v;
669        }
670
671        public void setYProgress(float r) {
672            // enforce r between 0 and 1
673            r = Math.min(1.0f, r);
674            r = Math.max(0, r);
675
676            mYProgress = r;
677            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
678            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
679
680            int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
681
682            switch (mMode) {
683                case NORMAL_MODE:
684                    viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
685                    highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
686                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
687
688                    float alpha = viewAlphaInterpolator(1 - r);
689
690                    // We make sure that views which can't be seen (have 0 alpha) are also invisible
691                    // so that they don't interfere with click events.
692                    if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
693                        mView.setVisibility(VISIBLE);
694                    } else if (alpha == 0 && mView.getAlpha() != 0
695                            && mView.getVisibility() == VISIBLE) {
696                        mView.setVisibility(INVISIBLE);
697                    }
698
699                    mView.setAlpha(alpha);
700                    mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
701                    mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
702                    break;
703                case END_OF_STACK_MODE:
704                    r = r * 0.2f;
705                    viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
706                    highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
707                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
708                    break;
709                case BEGINNING_OF_STACK_MODE:
710                    r = (1-r) * 0.2f;
711                    viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
712                    highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
713                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
714                    break;
715            }
716        }
717
718        public void setXProgress(float r) {
719            // enforce r between 0 and 1
720            r = Math.min(2.0f, r);
721            r = Math.max(-2.0f, r);
722
723            mXProgress = r;
724
725            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
726            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
727
728            r *= 0.2f;
729            viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
730            highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
731        }
732
733        void setMode(int mode) {
734            mMode = mode;
735        }
736
737        float getDurationForNeutralPosition() {
738            return getDuration(false, 0);
739        }
740
741        float getDurationForOffscreenPosition() {
742            return getDuration(true, 0);
743        }
744
745        float getDurationForNeutralPosition(float velocity) {
746            return getDuration(false, velocity);
747        }
748
749        float getDurationForOffscreenPosition(float velocity) {
750            return getDuration(true, velocity);
751        }
752
753        private float getDuration(boolean invert, float velocity) {
754            if (mView != null) {
755                final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
756
757                float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
758                        Math.pow(viewLp.verticalOffset, 2));
759                float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
760                        Math.pow(0.4f * mSlideAmount, 2));
761
762                if (velocity == 0) {
763                    return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
764                } else {
765                    float duration = invert ? d / Math.abs(velocity) :
766                            (maxd - d) / Math.abs(velocity);
767                    if (duration < MINIMUM_ANIMATION_DURATION ||
768                            duration > DEFAULT_ANIMATION_DURATION) {
769                        return getDuration(invert, 0);
770                    } else {
771                        return duration;
772                    }
773                }
774            }
775            return 0;
776        }
777
778        // Used for animations
779        @SuppressWarnings({"UnusedDeclaration"})
780        public float getYProgress() {
781            return mYProgress;
782        }
783
784        // Used for animations
785        @SuppressWarnings({"UnusedDeclaration"})
786        public float getXProgress() {
787            return mXProgress;
788        }
789    }
790
791    @Override
792    public void onRemoteAdapterConnected() {
793        super.onRemoteAdapterConnected();
794        // On first run, we want to set the stack to the end.
795        if (mWhichChild == -1) {
796            mWhichChild = 0;
797        }
798        setDisplayedChild(mWhichChild);
799    }
800
801    LayoutParams createOrReuseLayoutParams(View v) {
802        final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
803        if (currentLp instanceof LayoutParams) {
804            LayoutParams lp = (LayoutParams) currentLp;
805            lp.setHorizontalOffset(0);
806            lp.setVerticalOffset(0);
807            lp.width = 0;
808            lp.width = 0;
809            return lp;
810        }
811        return new LayoutParams(v);
812    }
813
814    @Override
815    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
816        boolean dataChanged = mDataChanged;
817        if (dataChanged) {
818            handleDataChanged();
819
820            // if the data changes, mWhichChild might be out of the bounds of the adapter
821            // in this case, we reset mWhichChild to the beginning
822            if (mWhichChild >= mAdapter.getCount())
823                mWhichChild = 0;
824
825            showOnly(mWhichChild, true, true);
826            refreshChildren();
827        }
828
829        final int childCount = getChildCount();
830        for (int i = 0; i < childCount; i++) {
831            final View child = getChildAt(i);
832
833            int childRight = mPaddingLeft + child.getMeasuredWidth();
834            int childBottom = mPaddingTop + child.getMeasuredHeight();
835            LayoutParams lp = (LayoutParams) child.getLayoutParams();
836
837            child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
838                    childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
839
840        }
841
842        mDataChanged = false;
843        onLayout();
844    }
845
846    private void measureChildren() {
847        final int count = getChildCount();
848        final int childWidth = mMeasuredWidth - mPaddingLeft - mPaddingRight;
849        final int childHeight = Math.round(mMeasuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR))
850                - mPaddingTop - mPaddingBottom;
851
852        for (int i = 0; i < count; i++) {
853            final View child = getChildAt(i);
854            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
855                    MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
856        }
857    }
858
859    @Override
860    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
861        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
862        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
863        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
864        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
865
866        boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
867
868        // We need to deal with the case where our parent hasn't told us how
869        // big we should be. In this case we should
870        float factor = 1/(1 - PERSPECTIVE_SHIFT_FACTOR);
871        if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
872            heightSpecSize = haveChildRefSize ?
873                    Math.round(mReferenceChildHeight * (1 + factor)) +
874                    mPaddingTop + mPaddingBottom : 0;
875        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
876            heightSpecSize = haveChildRefSize ? Math.min(
877                    Math.round(mReferenceChildHeight * (1 + factor)) + mPaddingTop +
878                    mPaddingBottom, heightSpecSize) : 0;
879        }
880
881        if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
882            widthSpecSize = haveChildRefSize ? mReferenceChildWidth + mPaddingLeft +
883                    mPaddingRight : 0;
884        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
885            widthSpecSize = haveChildRefSize ? Math.min(mReferenceChildWidth + mPaddingLeft +
886                    mPaddingRight, widthSpecSize) : 0;
887        }
888
889        setMeasuredDimension(widthSpecSize, heightSpecSize);
890        measureChildren();
891    }
892
893    class LayoutParams extends ViewGroup.LayoutParams {
894        int horizontalOffset;
895        int verticalOffset;
896        View mView;
897        int left, top, right, bottom;
898        private final Rect parentRect = new Rect();
899        private final Rect invalidateRect = new Rect();
900        private final RectF invalidateRectf = new RectF();
901        private final Rect globalInvalidateRect = new Rect();
902
903        LayoutParams(View view) {
904            super(0, 0);
905            width = 0;
906            height = 0;
907            horizontalOffset = 0;
908            verticalOffset = 0;
909            mView = view;
910        }
911
912        LayoutParams(Context c, AttributeSet attrs) {
913            super(c, attrs);
914            horizontalOffset = 0;
915            verticalOffset = 0;
916            width = 0;
917            height = 0;
918        }
919
920        void invalidateGlobalRegion(View v, Rect r) {
921            // We need to make a new rect here, so as not to modify the one passed
922            globalInvalidateRect.set(r);
923            View p = v;
924            if (!(v.getParent() != null && v.getParent() instanceof View)) return;
925
926            boolean firstPass = true;
927            parentRect.set(0, 0, 0, 0);
928            int depth = 0;
929            while (p.getParent() != null && p.getParent() instanceof View
930                    && !parentRect.contains(globalInvalidateRect)) {
931                if (!firstPass) {
932                    globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
933                            - p.getScrollY());
934                    depth++;
935                }
936                firstPass = false;
937                p = (View) p.getParent();
938                parentRect.set(p.getScrollX(), p.getScrollY(),
939                               p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
940
941            }
942
943            p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
944                    globalInvalidateRect.right, globalInvalidateRect.bottom);
945        }
946
947        Rect getInvalidateRect() {
948            return invalidateRect;
949        }
950
951        void resetInvalidateRect() {
952           invalidateRect.set(0, 0, 0, 0);
953        }
954
955        // This is public so that ObjectAnimator can access it
956        public void setVerticalOffset(int newVerticalOffset) {
957            int offsetDelta = newVerticalOffset - verticalOffset;
958            verticalOffset = newVerticalOffset;
959
960            if (mView != null) {
961                mView.requestLayout();
962                int top = Math.min(mView.getTop() + offsetDelta, mView.getTop());
963                int bottom = Math.max(mView.getBottom() + offsetDelta, mView.getBottom());
964
965                invalidateRectf.set(mView.getLeft(), top, mView.getRight(), bottom);
966
967                float xoffset = -invalidateRectf.left;
968                float yoffset = -invalidateRectf.top;
969                invalidateRectf.offset(xoffset, yoffset);
970                mView.getMatrix().mapRect(invalidateRectf);
971                invalidateRectf.offset(-xoffset, -yoffset);
972                invalidateRect.union((int) Math.floor(invalidateRectf.left),
973                        (int) Math.floor(invalidateRectf.top),
974                        (int) Math.ceil(invalidateRectf.right),
975                        (int) Math.ceil(invalidateRectf.bottom));
976
977                invalidateGlobalRegion(mView, invalidateRect);
978            }
979        }
980
981        public void setHorizontalOffset(int newHorizontalOffset) {
982            int offsetDelta = newHorizontalOffset - horizontalOffset;
983            horizontalOffset = newHorizontalOffset;
984
985            if (mView != null) {
986                mView.requestLayout();
987                int left = Math.min(mView.getLeft() + offsetDelta, mView.getLeft());
988                int right = Math.max(mView.getRight() + offsetDelta, mView.getRight());
989                invalidateRectf.set(left,  mView.getTop(), right, mView.getBottom());
990
991                float xoffset = -invalidateRectf.left;
992                float yoffset = -invalidateRectf.top;
993                invalidateRectf.offset(xoffset, yoffset);
994                mView.getMatrix().mapRect(invalidateRectf);
995                invalidateRectf.offset(-xoffset, -yoffset);
996
997                invalidateRect.union((int) Math.floor(invalidateRectf.left),
998                        (int) Math.floor(invalidateRectf.top),
999                        (int) Math.ceil(invalidateRectf.right),
1000                        (int) Math.ceil(invalidateRectf.bottom));
1001
1002                invalidateGlobalRegion(mView, invalidateRect);
1003            }
1004        }
1005    }
1006
1007    private static class HolographicHelper {
1008        private final Paint mHolographicPaint = new Paint();
1009        private final Paint mErasePaint = new Paint();
1010        private final Paint mBlurPaint = new Paint();
1011
1012        HolographicHelper(Context context) {
1013            initializePaints(context);
1014        }
1015
1016        void initializePaints(Context context) {
1017            final float density = context.getResources().getDisplayMetrics().density;
1018
1019            mHolographicPaint.setColor(0xff6699ff);
1020            mHolographicPaint.setFilterBitmap(true);
1021            mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
1022            mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1023            mErasePaint.setFilterBitmap(true);
1024            mBlurPaint.setMaskFilter(new BlurMaskFilter(2*density, BlurMaskFilter.Blur.NORMAL));
1025        }
1026
1027        Bitmap createOutline(View v) {
1028            if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1029                return null;
1030            }
1031
1032            Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
1033                    Bitmap.Config.ARGB_8888);
1034            Canvas canvas = new Canvas(bitmap);
1035
1036            float rotationX = v.getRotationX();
1037            float rotation = v.getRotation();
1038            float translationY = v.getTranslationY();
1039            v.setRotationX(0);
1040            v.setRotation(0);
1041            v.setTranslationY(0);
1042            v.draw(canvas);
1043            v.setRotationX(rotationX);
1044            v.setRotation(rotation);
1045            v.setTranslationY(translationY);
1046
1047            drawOutline(canvas, bitmap);
1048            return bitmap;
1049        }
1050
1051        final Matrix id = new Matrix();
1052        void drawOutline(Canvas dest, Bitmap src) {
1053            int[] xy = new int[2];
1054            Bitmap mask = src.extractAlpha(mBlurPaint, xy);
1055            Canvas maskCanvas = new Canvas(mask);
1056            maskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
1057            dest.drawColor(0, PorterDuff.Mode.CLEAR);
1058            dest.setMatrix(id);
1059            dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
1060            mask.recycle();
1061        }
1062    }
1063}
1064