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