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