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