StackView.java revision 6364f2bbe5254b4274f3feffc48f4259eacc205e
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, false);
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            Log.v(TAG, "Disabling parental clipping.");
291            ViewGroup vg = this;
292            while (vg.getParent() != null && vg.getParent() instanceof ViewGroup) {
293                if (vg == mAncestorContainingAllChildren) break;
294                vg = (ViewGroup) vg.getParent();
295                vg.setClipChildren(false);
296                vg.setClipToPadding(false);
297            }
298        }
299    }
300
301    private void onLayout() {
302        if (!mFirstLayoutHappened) {
303            mSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
304            updateChildTransforms();
305            mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * mSlideAmount);
306            mFirstLayoutHappened = true;
307        }
308    }
309
310    @Override
311    public boolean onInterceptTouchEvent(MotionEvent ev) {
312        int action = ev.getAction();
313        switch(action & MotionEvent.ACTION_MASK) {
314            case MotionEvent.ACTION_DOWN: {
315                if (mActivePointerId == INVALID_POINTER) {
316                    mInitialX = ev.getX();
317                    mInitialY = ev.getY();
318                    mActivePointerId = ev.getPointerId(0);
319                }
320                break;
321            }
322            case MotionEvent.ACTION_MOVE: {
323                int pointerIndex = ev.findPointerIndex(mActivePointerId);
324                if (pointerIndex == INVALID_POINTER) {
325                    // no data for our primary pointer, this shouldn't happen, log it
326                    Log.d(TAG, "Error: No data for our primary pointer.");
327                    return false;
328                }
329                float newY = ev.getY(pointerIndex);
330                float deltaY = newY - mInitialY;
331
332                beginGestureIfNeeded(deltaY);
333                break;
334            }
335            case MotionEvent.ACTION_POINTER_UP: {
336                onSecondaryPointerUp(ev);
337                break;
338            }
339            case MotionEvent.ACTION_UP:
340            case MotionEvent.ACTION_CANCEL: {
341                mActivePointerId = INVALID_POINTER;
342                mSwipeGestureType = GESTURE_NONE;
343            }
344        }
345
346        return mSwipeGestureType != GESTURE_NONE;
347    }
348
349    private void beginGestureIfNeeded(float deltaY) {
350        if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
351            int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
352            cancelLongPress();
353            requestDisallowInterceptTouchEvent(true);
354
355            int activeIndex;
356            if (mStackMode == ITEMS_SLIDE_UP) {
357                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ?
358                        mNumActiveViews - 1 : mNumActiveViews - 2;
359            } else {
360                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ?
361                        mNumActiveViews - 2 : mNumActiveViews - 1;
362            }
363
364            if (mAdapter == null) return;
365
366            if (mCurrentWindowStartUnbounded + activeIndex == 0) {
367                mStackSlider.setMode(StackSlider.BEGINNING_OF_STACK_MODE);
368            } else if (mCurrentWindowStartUnbounded + activeIndex == mAdapter.getCount()) {
369                activeIndex--;
370                mStackSlider.setMode(StackSlider.END_OF_STACK_MODE);
371            } else {
372                mStackSlider.setMode(StackSlider.NORMAL_MODE);
373            }
374
375            View v = getViewAtRelativeIndex(activeIndex);
376            if (v == null) return;
377
378            mHighlight.setImageBitmap(sHolographicHelper.createOutline(v));
379            mHighlight.setRotation(v.getRotation());
380            mHighlight.setTranslationY(v.getTranslationY());
381            mHighlight.bringToFront();
382            v.bringToFront();
383            mStackSlider.setView(v);
384
385            if (swipeGestureType == GESTURE_SLIDE_DOWN)
386                v.setVisibility(VISIBLE);
387
388            // We only register this gesture if we've made it this far without a problem
389            mSwipeGestureType = swipeGestureType;
390        }
391    }
392
393    @Override
394    public boolean onTouchEvent(MotionEvent ev) {
395        int action = ev.getAction();
396        int pointerIndex = ev.findPointerIndex(mActivePointerId);
397        if (pointerIndex == INVALID_POINTER) {
398            // no data for our primary pointer, this shouldn't happen, log it
399            Log.d(TAG, "Error: No data for our primary pointer.");
400            return false;
401        }
402
403        float newY = ev.getY(pointerIndex);
404        float newX = ev.getX(pointerIndex);
405        float deltaY = newY - mInitialY;
406        float deltaX = newX - mInitialX;
407        if (mVelocityTracker == null) {
408            mVelocityTracker = VelocityTracker.obtain();
409        }
410        mVelocityTracker.addMovement(ev);
411
412        switch (action & MotionEvent.ACTION_MASK) {
413            case MotionEvent.ACTION_MOVE: {
414                beginGestureIfNeeded(deltaY);
415
416                float rx = deltaX / (mSlideAmount * 1.0f);
417                if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
418                    float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
419                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
420                    mStackSlider.setYProgress(1 - r);
421                    mStackSlider.setXProgress(rx);
422                    return true;
423                } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
424                    float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
425                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
426                    mStackSlider.setYProgress(r);
427                    mStackSlider.setXProgress(rx);
428                    return true;
429                }
430                break;
431            }
432            case MotionEvent.ACTION_UP: {
433                handlePointerUp(ev);
434                break;
435            }
436            case MotionEvent.ACTION_POINTER_UP: {
437                onSecondaryPointerUp(ev);
438                break;
439            }
440            case MotionEvent.ACTION_CANCEL: {
441                mActivePointerId = INVALID_POINTER;
442                mSwipeGestureType = GESTURE_NONE;
443                break;
444            }
445        }
446        return true;
447    }
448
449    private final Rect touchRect = new Rect();
450    private void onSecondaryPointerUp(MotionEvent ev) {
451        final int activePointerIndex = ev.getActionIndex();
452        final int pointerId = ev.getPointerId(activePointerIndex);
453        if (pointerId == mActivePointerId) {
454
455            int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? mNumActiveViews - 1
456                    : mNumActiveViews - 2;
457
458            View v = getViewAtRelativeIndex(activeViewIndex);
459            if (v == null) return;
460
461            // Our primary pointer has gone up -- let's see if we can find
462            // another pointer on the view. If so, then we should replace
463            // our primary pointer with this new pointer and adjust things
464            // so that the view doesn't jump
465            for (int index = 0; index < ev.getPointerCount(); index++) {
466                if (index != activePointerIndex) {
467
468                    float x = ev.getX(index);
469                    float y = ev.getY(index);
470
471                    touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
472                    if (touchRect.contains(Math.round(x), Math.round(y))) {
473                        float oldX = ev.getX(activePointerIndex);
474                        float oldY = ev.getY(activePointerIndex);
475
476                        // adjust our frame of reference to avoid a jump
477                        mInitialY += (y - oldY);
478                        mInitialX += (x - oldX);
479
480                        mActivePointerId = ev.getPointerId(index);
481                        if (mVelocityTracker != null) {
482                            mVelocityTracker.clear();
483                        }
484                        // ok, we're good, we found a new pointer which is touching the active view
485                        return;
486                    }
487                }
488            }
489            // if we made it this far, it means we didn't find a satisfactory new pointer :(,
490            // so end the gesture
491            handlePointerUp(ev);
492        }
493    }
494
495    private void handlePointerUp(MotionEvent ev) {
496        int pointerIndex = ev.findPointerIndex(mActivePointerId);
497        float newY = ev.getY(pointerIndex);
498        int deltaY = (int) (newY - mInitialY);
499
500        if (mVelocityTracker != null) {
501            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
502            mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
503        }
504
505        if (mVelocityTracker != null) {
506            mVelocityTracker.recycle();
507            mVelocityTracker = null;
508        }
509
510        if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
511                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
512            // Swipe threshold exceeded, swipe down
513            if (mStackMode == ITEMS_SLIDE_UP) {
514                showNext();
515            } else {
516                showPrevious();
517            }
518            mHighlight.bringToFront();
519        } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
520                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
521            // Swipe threshold exceeded, swipe up
522            if (mStackMode == ITEMS_SLIDE_UP) {
523                showPrevious();
524            } else {
525                showNext();
526            }
527
528            mHighlight.bringToFront();
529        } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
530            // Didn't swipe up far enough, snap back down
531            int duration;
532            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
533            if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
534                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
535            } else {
536                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
537            }
538
539            StackSlider animationSlider = new StackSlider(mStackSlider);
540            PropertyValuesHolder<Float> snapBackY =
541                    new PropertyValuesHolder<Float>("YProgress", finalYProgress);
542            PropertyValuesHolder<Float> snapBackX =
543                    new PropertyValuesHolder<Float>("XProgress", 0.0f);
544            ObjectAnimator pa = new ObjectAnimator(duration, animationSlider,
545                    snapBackX, snapBackY);
546            pa.setInterpolator(new LinearInterpolator());
547            pa.start();
548        } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
549            // Didn't swipe down far enough, snap back up
550            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
551            int duration;
552            if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
553                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
554            } else {
555                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
556            }
557
558            StackSlider animationSlider = new StackSlider(mStackSlider);
559            PropertyValuesHolder<Float> snapBackY =
560                    new PropertyValuesHolder<Float>("YProgress", finalYProgress);
561            PropertyValuesHolder<Float> snapBackX =
562                    new PropertyValuesHolder<Float>("XProgress", 0.0f);
563            ObjectAnimator pa = new ObjectAnimator(duration, animationSlider,
564                    snapBackX, snapBackY);
565            pa.start();
566        }
567
568        mActivePointerId = INVALID_POINTER;
569        mSwipeGestureType = GESTURE_NONE;
570    }
571
572    private class StackSlider {
573        View mView;
574        float mYProgress;
575        float mXProgress;
576
577        static final int NORMAL_MODE = 0;
578        static final int BEGINNING_OF_STACK_MODE = 1;
579        static final int END_OF_STACK_MODE = 2;
580
581        int mMode = NORMAL_MODE;
582
583        public StackSlider() {
584        }
585
586        public StackSlider(StackSlider copy) {
587            mView = copy.mView;
588            mYProgress = copy.mYProgress;
589            mXProgress = copy.mXProgress;
590            mMode = copy.mMode;
591        }
592
593        private float cubic(float r) {
594            return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
595        }
596
597        private float highlightAlphaInterpolator(float r) {
598            float pivot = 0.4f;
599            if (r < pivot) {
600                return 0.85f * cubic(r / pivot);
601            } else {
602                return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
603            }
604        }
605
606        private float viewAlphaInterpolator(float r) {
607            float pivot = 0.3f;
608            if (r > pivot) {
609                return (r - pivot) / (1 - pivot);
610            } else {
611                return 0;
612            }
613        }
614
615        private float rotationInterpolator(float r) {
616            float pivot = 0.2f;
617            if (r < pivot) {
618                return 0;
619            } else {
620                return (r - pivot) / (1 - pivot);
621            }
622        }
623
624        void setView(View v) {
625            mView = v;
626        }
627
628        public void setYProgress(float r) {
629            // enforce r between 0 and 1
630            r = Math.min(1.0f, r);
631            r = Math.max(0, r);
632
633            mYProgress = r;
634            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
635            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
636
637            int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
638
639            switch (mMode) {
640                case NORMAL_MODE:
641                    viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
642                    highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
643                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
644
645                    float alpha = viewAlphaInterpolator(1 - r);
646
647                    // We make sure that views which can't be seen (have 0 alpha) are also invisible
648                    // so that they don't interfere with click events.
649                    if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
650                        mView.setVisibility(VISIBLE);
651                    } else if (alpha == 0 && mView.getAlpha() != 0
652                            && mView.getVisibility() == VISIBLE) {
653                        mView.setVisibility(INVISIBLE);
654                    }
655
656                    mView.setAlpha(alpha);
657                    mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
658                    mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
659                    break;
660                case BEGINNING_OF_STACK_MODE:
661                    r = r * 0.2f;
662                    viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
663                    highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
664                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
665                    break;
666                case END_OF_STACK_MODE:
667                    r = (1-r) * 0.2f;
668                    viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
669                    highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
670                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
671                    break;
672            }
673        }
674
675        public void setXProgress(float r) {
676            // enforce r between 0 and 1
677            r = Math.min(2.0f, r);
678            r = Math.max(-2.0f, r);
679
680            mXProgress = r;
681
682            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
683            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
684
685            r *= 0.2f;
686            viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
687            highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
688        }
689
690        void setMode(int mode) {
691            mMode = mode;
692        }
693
694        float getDurationForNeutralPosition() {
695            return getDuration(false, 0);
696        }
697
698        float getDurationForOffscreenPosition() {
699            return getDuration(true, 0);
700        }
701
702        float getDurationForNeutralPosition(float velocity) {
703            return getDuration(false, velocity);
704        }
705
706        float getDurationForOffscreenPosition(float velocity) {
707            return getDuration(true, velocity);
708        }
709
710        private float getDuration(boolean invert, float velocity) {
711            if (mView != null) {
712                final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
713
714                float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
715                        Math.pow(viewLp.verticalOffset, 2));
716                float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
717                        Math.pow(0.4f * mSlideAmount, 2));
718
719                if (velocity == 0) {
720                    return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
721                } else {
722                    float duration = invert ? d / Math.abs(velocity) :
723                            (maxd - d) / Math.abs(velocity);
724                    if (duration < MINIMUM_ANIMATION_DURATION ||
725                            duration > DEFAULT_ANIMATION_DURATION) {
726                        return getDuration(invert, 0);
727                    } else {
728                        return duration;
729                    }
730                }
731            }
732            return 0;
733        }
734
735        public float getYProgress() {
736            return mYProgress;
737        }
738
739        public float getXProgress() {
740            return mXProgress;
741        }
742    }
743
744    @Override
745    public void onRemoteAdapterConnected() {
746        super.onRemoteAdapterConnected();
747        // On first run, we want to set the stack to the end.
748        if (mAdapter != null && mWhichChild == -1) {
749            mWhichChild = mAdapter.getCount() - 1;
750        }
751        setDisplayedChild(mWhichChild);
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