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