StackView.java revision 0e2de6d7187ef67ec00a2f2544450caa4a239c39
1/* Copyright (C) 2010 The Android Open Source Project
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16package android.widget;
17
18import java.lang.ref.WeakReference;
19
20import android.animation.ObjectAnimator;
21import android.animation.PropertyValuesHolder;
22import android.content.Context;
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.Region;
33import android.graphics.TableMaskFilter;
34import android.util.AttributeSet;
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.animation.LinearInterpolator;
42import android.widget.RemoteViews.RemoteView;
43
44@RemoteView
45/**
46 * A view that displays its children in a stack and allows users to discretely swipe
47 * through the children.
48 */
49public class StackView extends AdapterViewAnimator {
50    private final String TAG = "StackView";
51
52    /**
53     * Default animation parameters
54     */
55    private static final int DEFAULT_ANIMATION_DURATION = 400;
56    private static final int FADE_IN_ANIMATION_DURATION = 800;
57    private static final int MINIMUM_ANIMATION_DURATION = 50;
58    private static final int STACK_RELAYOUT_DURATION = 100;
59
60    /**
61     * Parameters effecting the perspective visuals
62     */
63    private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
64    private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
65
66    private float mPerspectiveShiftX;
67    private float mPerspectiveShiftY;
68    private float mNewPerspectiveShiftX;
69    private float mNewPerspectiveShiftY;
70
71    @SuppressWarnings({"FieldCanBeLocal"})
72    private static final float PERSPECTIVE_SCALE_FACTOR = 0.f;
73
74    /**
75     * Represent the two possible stack modes, one where items slide up, and the other
76     * where items slide down. The perspective is also inverted between these two modes.
77     */
78    private static final int ITEMS_SLIDE_UP = 0;
79    private static final int ITEMS_SLIDE_DOWN = 1;
80
81    /**
82     * These specify the different gesture states
83     */
84    private static final int GESTURE_NONE = 0;
85    private static final int GESTURE_SLIDE_UP = 1;
86    private static final int GESTURE_SLIDE_DOWN = 2;
87
88    /**
89     * Specifies how far you need to swipe (up or down) before it
90     * will be consider a completed gesture when you lift your finger
91     */
92    private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
93
94    /**
95     * Specifies the total distance, relative to the size of the stack,
96     * that views will be slid, either up or down
97     */
98    private static final float SLIDE_UP_RATIO = 0.7f;
99
100    /**
101     * Sentinel value for no current active pointer.
102     * Used by {@link #mActivePointerId}.
103     */
104    private static final int INVALID_POINTER = -1;
105
106    /**
107     * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
108     */
109    private static final int NUM_ACTIVE_VIEWS = 5;
110
111    private static final int FRAME_PADDING = 4;
112
113    private final Rect mTouchRect = new Rect();
114
115    private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
116
117    /**
118     * These variables are all related to the current state of touch interaction
119     * with the stack
120     */
121    private float mInitialY;
122    private float mInitialX;
123    private int mActivePointerId;
124    private int mYVelocity = 0;
125    private int mSwipeGestureType = GESTURE_NONE;
126    private int mSlideAmount;
127    private int mSwipeThreshold;
128    private int mTouchSlop;
129    private int mMaximumVelocity;
130    private VelocityTracker mVelocityTracker;
131    private boolean mTransitionIsSetup = false;
132
133    private static HolographicHelper sHolographicHelper;
134    private ImageView mHighlight;
135    private ImageView mClickFeedback;
136    private boolean mClickFeedbackIsValid = false;
137    private StackSlider mStackSlider;
138    private boolean mFirstLayoutHappened = false;
139    private long mLastInteractionTime = 0;
140    private int mStackMode;
141    private int mFramePadding;
142    private final Rect stackInvalidateRect = new Rect();
143
144    public StackView(Context context) {
145        super(context);
146        initStackView();
147    }
148
149    public StackView(Context context, AttributeSet attrs) {
150        super(context, attrs);
151        initStackView();
152    }
153
154    private void initStackView() {
155        configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
156        setStaticTransformationsEnabled(true);
157        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
158        mTouchSlop = configuration.getScaledTouchSlop();
159        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
160        mActivePointerId = INVALID_POINTER;
161
162        mHighlight = new ImageView(getContext());
163        mHighlight.setLayoutParams(new LayoutParams(mHighlight));
164        addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
165
166        mClickFeedback = new ImageView(getContext());
167        mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
168        addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
169        mClickFeedback.setVisibility(INVISIBLE);
170
171        mStackSlider = new StackSlider();
172
173        if (sHolographicHelper == null) {
174            sHolographicHelper = new HolographicHelper(mContext);
175        }
176        setClipChildren(false);
177        setClipToPadding(false);
178
179        // This sets the form of the StackView, which is currently to have the perspective-shifted
180        // views above the active view, and have items slide down when sliding out. The opposite is
181        // available by using ITEMS_SLIDE_UP.
182        mStackMode = ITEMS_SLIDE_DOWN;
183
184        // This is a flag to indicate the the stack is loading for the first time
185        mWhichChild = -1;
186
187        // Adjust the frame padding based on the density, since the highlight changes based
188        // on the density
189        final float density = mContext.getResources().getDisplayMetrics().density;
190        mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
191    }
192
193    /**
194     * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
195     */
196    void animateViewForTransition(int fromIndex, int toIndex, final View view) {
197        ObjectAnimator alphaOa = null;
198        ObjectAnimator oldAlphaOa = null;
199
200        // If there is currently an alpha animation on this view, we need
201        // to know about it, and may need to cancel it so as not to interfere with
202        // a new alpha animation.
203        Object tag = view.getTag(com.android.internal.R.id.viewAlphaAnimation);
204        if (tag instanceof WeakReference<?>) {
205            Object obj = ((WeakReference<?>) tag).get();
206            if (obj instanceof ObjectAnimator) {
207                oldAlphaOa = (ObjectAnimator) obj;
208            }
209        }
210
211        if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
212            // Fade item in
213            if (view.getAlpha() == 1) {
214                view.setAlpha(0);
215            }
216            transformViewAtIndex(toIndex, view, false);
217            view.setVisibility(VISIBLE);
218
219            alphaOa = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 1.0f);
220            alphaOa.setDuration(FADE_IN_ANIMATION_DURATION);
221            if (oldAlphaOa != null) oldAlphaOa.cancel();
222            alphaOa.start();
223            view.setTagInternal(com.android.internal.R.id.viewAlphaAnimation,
224                    new WeakReference<ObjectAnimator>(alphaOa));
225        } else if (fromIndex == 0 && toIndex == 1) {
226            // Slide item in
227            view.setVisibility(VISIBLE);
228
229            int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
230
231            StackSlider animationSlider = new StackSlider(mStackSlider);
232            animationSlider.setView(view);
233            PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
234            PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
235            ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
236                    slideInX, slideInY);
237            slideIn.setDuration(duration);
238            slideIn.setInterpolator(new LinearInterpolator());
239            slideIn.start();
240        } else if (fromIndex == 1 && toIndex == 0) {
241            // Slide item out
242            int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
243
244            StackSlider animationSlider = new StackSlider(mStackSlider);
245            animationSlider.setView(view);
246            PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
247            PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
248            ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
249                    slideOutX, slideOutY);
250            slideOut.setDuration(duration);
251            slideOut.setInterpolator(new LinearInterpolator());
252            slideOut.start();
253        } else if (toIndex == 0) {
254            // Make sure this view that is "waiting in the wings" is invisible
255            view.setAlpha(0.0f);
256            view.setVisibility(INVISIBLE);
257        } else if (fromIndex == 0 && toIndex > 1) {
258            view.setVisibility(VISIBLE);
259            view.setAlpha(1.0f);
260        } else if (fromIndex == -1) {
261            view.setAlpha(1.0f);
262            view.setVisibility(VISIBLE);
263        } else if (toIndex == -1) {
264            // Fade item out
265            alphaOa = ObjectAnimator.ofFloat(view, "alpha", view.getAlpha(), 0.0f);
266            alphaOa.setDuration(STACK_RELAYOUT_DURATION);
267            if (oldAlphaOa != null) oldAlphaOa.cancel();
268            alphaOa.start();
269            view.setTagInternal(com.android.internal.R.id.viewAlphaAnimation,
270                    new WeakReference<ObjectAnimator>(alphaOa));
271        }
272
273        // Implement the faked perspective
274        if (toIndex != -1) {
275            transformViewAtIndex(toIndex, view, true);
276        }
277    }
278
279    private void transformViewAtIndex(int index, final View view, boolean animate) {
280        final float maxPerspectiveShiftY = mPerspectiveShiftY;
281        final float maxPerspectiveShiftX = mPerspectiveShiftX;
282
283        if (mStackMode == ITEMS_SLIDE_DOWN) {
284            index = mMaxNumActiveViews - index - 1;
285            if (index == mMaxNumActiveViews - 1) index--;
286        } else {
287            index--;
288            if (index < 0) index++;
289        }
290
291        float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
292
293        final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
294
295        float perspectiveTranslationY = r * maxPerspectiveShiftY;
296        float scaleShiftCorrectionY = (scale - 1) *
297                (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
298        final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
299
300        float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
301        float scaleShiftCorrectionX =  (1 - scale) *
302                (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
303        final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
304
305        // If this view is currently being animated for a certain position, we need to cancel
306        // this animation so as not to interfere with the new transformation.
307        Object tag = view.getTag(com.android.internal.R.id.viewAnimation);
308        if (tag instanceof WeakReference<?>) {
309            Object obj = ((WeakReference<?>) tag).get();
310            if (obj instanceof ObjectAnimator) {
311                ((ObjectAnimator) obj).cancel();
312            }
313        }
314
315        if (animate) {
316            PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
317            PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
318            PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
319            PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
320
321            ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
322                    translationY, translationX);
323            oa.setDuration(STACK_RELAYOUT_DURATION);
324            view.setTagInternal(com.android.internal.R.id.viewAnimation,
325                    new WeakReference<ObjectAnimator>(oa));
326            oa.start();
327        } else {
328            view.setTranslationX(transX);
329            view.setTranslationY(transY);
330            view.setScaleX(scale);
331            view.setScaleY(scale);
332        }
333    }
334
335    private void setupStackSlider(View v, int mode) {
336        mStackSlider.setMode(mode);
337        if (v != null) {
338            mHighlight.setImageBitmap(sHolographicHelper.createOutline(v));
339            mHighlight.setRotation(v.getRotation());
340            mHighlight.setTranslationY(v.getTranslationY());
341            mHighlight.setTranslationX(v.getTranslationX());
342            mHighlight.bringToFront();
343            v.bringToFront();
344            mStackSlider.setView(v);
345
346            v.setVisibility(VISIBLE);
347        }
348    }
349
350    /**
351     * {@inheritDoc}
352     */
353    @Override
354    @android.view.RemotableViewMethod
355    public void showNext() {
356        if (mSwipeGestureType != GESTURE_NONE) return;
357        if (!mTransitionIsSetup) {
358            View v = getViewAtRelativeIndex(1);
359            if (v != null) {
360                setupStackSlider(v, StackSlider.NORMAL_MODE);
361                mStackSlider.setYProgress(0);
362                mStackSlider.setXProgress(0);
363            }
364        }
365        super.showNext();
366    }
367
368    /**
369     * {@inheritDoc}
370     */
371    @Override
372    @android.view.RemotableViewMethod
373    public void showPrevious() {
374        if (mSwipeGestureType != GESTURE_NONE) return;
375        if (!mTransitionIsSetup) {
376            View v = getViewAtRelativeIndex(0);
377            if (v != null) {
378                setupStackSlider(v, StackSlider.NORMAL_MODE);
379                mStackSlider.setYProgress(1);
380                mStackSlider.setXProgress(0);
381            }
382        }
383        super.showPrevious();
384    }
385
386    @Override
387    void showOnly(int childIndex, boolean animate, boolean onLayout) {
388        super.showOnly(childIndex, animate, onLayout);
389
390        // Here we need to make sure that the z-order of the children is correct
391        for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
392            int index = modulo(i, getWindowSize());
393            ViewAndIndex vi = mViewsMap.get(index);
394            if (vi != null) {
395                View v = mViewsMap.get(index).view;
396                if (v != null) v.bringToFront();
397            }
398        }
399        mTransitionIsSetup = false;
400        mClickFeedbackIsValid = false;
401    }
402
403    void updateClickFeedback() {
404        if (!mClickFeedbackIsValid) {
405            View v = getViewAtRelativeIndex(1);
406            if (v != null) {
407                mClickFeedback.setImageBitmap(sHolographicHelper.createOutline(v,
408                        HolographicHelper.CLICK_FEEDBACK));
409                mClickFeedback.setTranslationX(v.getTranslationX());
410                mClickFeedback.setTranslationY(v.getTranslationY());
411            }
412            mClickFeedbackIsValid = true;
413        }
414    }
415
416    @Override
417    void showTapFeedback(View v) {
418        updateClickFeedback();
419        mClickFeedback.setVisibility(VISIBLE);
420        mClickFeedback.bringToFront();
421        invalidate();
422    }
423
424    @Override
425    void hideTapFeedback(View v) {
426        mClickFeedback.setVisibility(INVISIBLE);
427        invalidate();
428    }
429
430    private void updateChildTransforms() {
431        for (int i = 0; i < getNumActiveViews(); i++) {
432            View v = getViewAtRelativeIndex(i);
433            if (v != null) {
434                transformViewAtIndex(i, v, false);
435            }
436        }
437    }
438
439    @Override
440    FrameLayout getFrameForChild() {
441        FrameLayout fl = new FrameLayout(mContext);
442        fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
443        return fl;
444    }
445
446    /**
447     * Apply any necessary tranforms for the child that is being added.
448     */
449    void applyTransformForChildAtIndex(View child, int relativeIndex) {
450    }
451
452    @Override
453    protected void dispatchDraw(Canvas canvas) {
454        canvas.getClipBounds(stackInvalidateRect);
455        final int childCount = getChildCount();
456        for (int i = 0; i < childCount; i++) {
457            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
458            stackInvalidateRect.union(lp.getInvalidateRect());
459            lp.resetInvalidateRect();
460        }
461        canvas.save(Canvas.CLIP_SAVE_FLAG);
462        canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
463        super.dispatchDraw(canvas);
464        canvas.restore();
465    }
466
467    private void onLayout() {
468        if (!mFirstLayoutHappened) {
469            mSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
470            updateChildTransforms();
471            mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * mSlideAmount);
472            mFirstLayoutHappened = true;
473        }
474
475        if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
476                Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
477            mPerspectiveShiftY = mNewPerspectiveShiftY;
478            mPerspectiveShiftX = mNewPerspectiveShiftX;
479            updateChildTransforms();
480        }
481    }
482
483    /**
484     * {@inheritDoc}
485     */
486    @Override
487    public boolean onInterceptTouchEvent(MotionEvent ev) {
488        int action = ev.getAction();
489        switch(action & MotionEvent.ACTION_MASK) {
490            case MotionEvent.ACTION_DOWN: {
491                if (mActivePointerId == INVALID_POINTER) {
492                    mInitialX = ev.getX();
493                    mInitialY = ev.getY();
494                    mActivePointerId = ev.getPointerId(0);
495                }
496                break;
497            }
498            case MotionEvent.ACTION_MOVE: {
499                int pointerIndex = ev.findPointerIndex(mActivePointerId);
500                if (pointerIndex == INVALID_POINTER) {
501                    // no data for our primary pointer, this shouldn't happen, log it
502                    Log.d(TAG, "Error: No data for our primary pointer.");
503                    return false;
504                }
505                float newY = ev.getY(pointerIndex);
506                float deltaY = newY - mInitialY;
507
508                beginGestureIfNeeded(deltaY);
509                break;
510            }
511            case MotionEvent.ACTION_POINTER_UP: {
512                onSecondaryPointerUp(ev);
513                break;
514            }
515            case MotionEvent.ACTION_UP:
516            case MotionEvent.ACTION_CANCEL: {
517                mActivePointerId = INVALID_POINTER;
518                mSwipeGestureType = GESTURE_NONE;
519            }
520        }
521
522        return mSwipeGestureType != GESTURE_NONE;
523    }
524
525    private void beginGestureIfNeeded(float deltaY) {
526        if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
527            final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
528            cancelLongPress();
529            requestDisallowInterceptTouchEvent(true);
530
531            if (mAdapter == null) return;
532            final int adapterCount = mAdapter.getCount();
533
534            int activeIndex;
535            if (mStackMode == ITEMS_SLIDE_UP) {
536                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
537            } else {
538                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
539            }
540
541            boolean endOfStack = mLoopViews && adapterCount == 1 &&
542                ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP) ||
543                 (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
544            boolean beginningOfStack = mLoopViews && adapterCount == 1 &&
545                ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP) ||
546                 (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
547
548            int stackMode;
549            if (mLoopViews && !beginningOfStack && !endOfStack) {
550                stackMode = StackSlider.NORMAL_MODE;
551            } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
552                activeIndex++;
553                stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
554            } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
555                stackMode = StackSlider.END_OF_STACK_MODE;
556            } else {
557                stackMode = StackSlider.NORMAL_MODE;
558            }
559
560            mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
561
562            View v = getViewAtRelativeIndex(activeIndex);
563            if (v == null) return;
564
565            setupStackSlider(v, stackMode);
566
567            // We only register this gesture if we've made it this far without a problem
568            mSwipeGestureType = swipeGestureType;
569            cancelHandleClick();
570        }
571    }
572
573    /**
574     * {@inheritDoc}
575     */
576    @Override
577    public boolean onTouchEvent(MotionEvent ev) {
578        super.onTouchEvent(ev);
579
580        int action = ev.getAction();
581        int pointerIndex = ev.findPointerIndex(mActivePointerId);
582        if (pointerIndex == INVALID_POINTER) {
583            // no data for our primary pointer, this shouldn't happen, log it
584            Log.d(TAG, "Error: No data for our primary pointer.");
585            return false;
586        }
587
588        float newY = ev.getY(pointerIndex);
589        float newX = ev.getX(pointerIndex);
590        float deltaY = newY - mInitialY;
591        float deltaX = newX - mInitialX;
592        if (mVelocityTracker == null) {
593            mVelocityTracker = VelocityTracker.obtain();
594        }
595        mVelocityTracker.addMovement(ev);
596
597        switch (action & MotionEvent.ACTION_MASK) {
598            case MotionEvent.ACTION_MOVE: {
599                beginGestureIfNeeded(deltaY);
600
601                float rx = deltaX / (mSlideAmount * 1.0f);
602                if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
603                    float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
604                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
605                    mStackSlider.setYProgress(1 - r);
606                    mStackSlider.setXProgress(rx);
607                    return true;
608                } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
609                    float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
610                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
611                    mStackSlider.setYProgress(r);
612                    mStackSlider.setXProgress(rx);
613                    return true;
614                }
615                break;
616            }
617            case MotionEvent.ACTION_UP: {
618                handlePointerUp(ev);
619                break;
620            }
621            case MotionEvent.ACTION_POINTER_UP: {
622                onSecondaryPointerUp(ev);
623                break;
624            }
625            case MotionEvent.ACTION_CANCEL: {
626                mActivePointerId = INVALID_POINTER;
627                mSwipeGestureType = GESTURE_NONE;
628                break;
629            }
630        }
631        return true;
632    }
633
634    private void onSecondaryPointerUp(MotionEvent ev) {
635        final int activePointerIndex = ev.getActionIndex();
636        final int pointerId = ev.getPointerId(activePointerIndex);
637        if (pointerId == mActivePointerId) {
638
639            int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
640
641            View v = getViewAtRelativeIndex(activeViewIndex);
642            if (v == null) return;
643
644            // Our primary pointer has gone up -- let's see if we can find
645            // another pointer on the view. If so, then we should replace
646            // our primary pointer with this new pointer and adjust things
647            // so that the view doesn't jump
648            for (int index = 0; index < ev.getPointerCount(); index++) {
649                if (index != activePointerIndex) {
650
651                    float x = ev.getX(index);
652                    float y = ev.getY(index);
653
654                    mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
655                    if (mTouchRect.contains(Math.round(x), Math.round(y))) {
656                        float oldX = ev.getX(activePointerIndex);
657                        float oldY = ev.getY(activePointerIndex);
658
659                        // adjust our frame of reference to avoid a jump
660                        mInitialY += (y - oldY);
661                        mInitialX += (x - oldX);
662
663                        mActivePointerId = ev.getPointerId(index);
664                        if (mVelocityTracker != null) {
665                            mVelocityTracker.clear();
666                        }
667                        // ok, we're good, we found a new pointer which is touching the active view
668                        return;
669                    }
670                }
671            }
672            // if we made it this far, it means we didn't find a satisfactory new pointer :(,
673            // so end the gesture
674            handlePointerUp(ev);
675        }
676    }
677
678    private void handlePointerUp(MotionEvent ev) {
679        int pointerIndex = ev.findPointerIndex(mActivePointerId);
680        float newY = ev.getY(pointerIndex);
681        int deltaY = (int) (newY - mInitialY);
682        mLastInteractionTime = System.currentTimeMillis();
683
684        if (mVelocityTracker != null) {
685            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
686            mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
687        }
688
689        if (mVelocityTracker != null) {
690            mVelocityTracker.recycle();
691            mVelocityTracker = null;
692        }
693
694        if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
695                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
696            // We reset the gesture variable, because otherwise we will ignore showPrevious() /
697            // showNext();
698            mSwipeGestureType = GESTURE_NONE;
699
700            // Swipe threshold exceeded, swipe down
701            if (mStackMode == ITEMS_SLIDE_UP) {
702                showPrevious();
703            } else {
704                showNext();
705            }
706            mHighlight.bringToFront();
707        } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
708                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
709            // We reset the gesture variable, because otherwise we will ignore showPrevious() /
710            // showNext();
711            mSwipeGestureType = GESTURE_NONE;
712
713            // Swipe threshold exceeded, swipe up
714            if (mStackMode == ITEMS_SLIDE_UP) {
715                showNext();
716            } else {
717                showPrevious();
718            }
719
720            mHighlight.bringToFront();
721        } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
722            // Didn't swipe up far enough, snap back down
723            int duration;
724            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
725            if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
726                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
727            } else {
728                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
729            }
730
731            StackSlider animationSlider = new StackSlider(mStackSlider);
732            PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
733            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
734            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
735                    snapBackX, snapBackY);
736            pa.setDuration(duration);
737            pa.setInterpolator(new LinearInterpolator());
738            pa.start();
739        } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
740            // Didn't swipe down far enough, snap back up
741            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
742            int duration;
743            if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
744                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
745            } else {
746                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
747            }
748
749            StackSlider animationSlider = new StackSlider(mStackSlider);
750            PropertyValuesHolder snapBackY =
751                    PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
752            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
753            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
754                    snapBackX, snapBackY);
755            pa.setDuration(duration);
756            pa.start();
757        }
758
759        mActivePointerId = INVALID_POINTER;
760        mSwipeGestureType = GESTURE_NONE;
761    }
762
763    private class StackSlider {
764        View mView;
765        float mYProgress;
766        float mXProgress;
767
768        static final int NORMAL_MODE = 0;
769        static final int BEGINNING_OF_STACK_MODE = 1;
770        static final int END_OF_STACK_MODE = 2;
771
772        int mMode = NORMAL_MODE;
773
774        public StackSlider() {
775        }
776
777        public StackSlider(StackSlider copy) {
778            mView = copy.mView;
779            mYProgress = copy.mYProgress;
780            mXProgress = copy.mXProgress;
781            mMode = copy.mMode;
782        }
783
784        private float cubic(float r) {
785            return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
786        }
787
788        private float highlightAlphaInterpolator(float r) {
789            float pivot = 0.4f;
790            if (r < pivot) {
791                return 0.85f * cubic(r / pivot);
792            } else {
793                return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
794            }
795        }
796
797        private float viewAlphaInterpolator(float r) {
798            float pivot = 0.3f;
799            if (r > pivot) {
800                return (r - pivot) / (1 - pivot);
801            } else {
802                return 0;
803            }
804        }
805
806        private float rotationInterpolator(float r) {
807            float pivot = 0.2f;
808            if (r < pivot) {
809                return 0;
810            } else {
811                return (r - pivot) / (1 - pivot);
812            }
813        }
814
815        void setView(View v) {
816            mView = v;
817        }
818
819        public void setYProgress(float r) {
820            // enforce r between 0 and 1
821            r = Math.min(1.0f, r);
822            r = Math.max(0, r);
823
824            mYProgress = r;
825            if (mView == null) return;
826
827            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
828            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
829
830            int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
831
832            // We need to prevent any clipping issues which may arise by setting a layer type.
833            // This doesn't come for free however, so we only want to enable it when required.
834            if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
835                if (mView.getLayerType() == LAYER_TYPE_NONE) {
836                    mView.setLayerType(LAYER_TYPE_HARDWARE, null);
837                }
838            } else {
839                if (mView.getLayerType() != LAYER_TYPE_NONE) {
840                    mView.setLayerType(LAYER_TYPE_NONE, null);
841                }
842            }
843
844            switch (mMode) {
845                case NORMAL_MODE:
846                    viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
847                    highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
848                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
849
850                    float alpha = viewAlphaInterpolator(1 - r);
851
852                    // We make sure that views which can't be seen (have 0 alpha) are also invisible
853                    // so that they don't interfere with click events.
854                    if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
855                        mView.setVisibility(VISIBLE);
856                    } else if (alpha == 0 && mView.getAlpha() != 0
857                            && mView.getVisibility() == VISIBLE) {
858                        mView.setVisibility(INVISIBLE);
859                    }
860
861                    mView.setAlpha(alpha);
862                    mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
863                    mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
864                    break;
865                case END_OF_STACK_MODE:
866                    r = r * 0.2f;
867                    viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
868                    highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
869                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
870                    break;
871                case BEGINNING_OF_STACK_MODE:
872                    r = (1-r) * 0.2f;
873                    viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
874                    highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
875                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
876                    break;
877            }
878        }
879
880        public void setXProgress(float r) {
881            // enforce r between 0 and 1
882            r = Math.min(2.0f, r);
883            r = Math.max(-2.0f, r);
884
885            mXProgress = r;
886
887            if (mView == null) return;
888            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
889            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
890
891            r *= 0.2f;
892            viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
893            highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
894        }
895
896        void setMode(int mode) {
897            mMode = mode;
898        }
899
900        float getDurationForNeutralPosition() {
901            return getDuration(false, 0);
902        }
903
904        float getDurationForOffscreenPosition() {
905            return getDuration(true, 0);
906        }
907
908        float getDurationForNeutralPosition(float velocity) {
909            return getDuration(false, velocity);
910        }
911
912        float getDurationForOffscreenPosition(float velocity) {
913            return getDuration(true, velocity);
914        }
915
916        private float getDuration(boolean invert, float velocity) {
917            if (mView != null) {
918                final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
919
920                float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
921                        Math.pow(viewLp.verticalOffset, 2));
922                float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
923                        Math.pow(0.4f * mSlideAmount, 2));
924
925                if (velocity == 0) {
926                    return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
927                } else {
928                    float duration = invert ? d / Math.abs(velocity) :
929                            (maxd - d) / Math.abs(velocity);
930                    if (duration < MINIMUM_ANIMATION_DURATION ||
931                            duration > DEFAULT_ANIMATION_DURATION) {
932                        return getDuration(invert, 0);
933                    } else {
934                        return duration;
935                    }
936                }
937            }
938            return 0;
939        }
940
941        // Used for animations
942        @SuppressWarnings({"UnusedDeclaration"})
943        public float getYProgress() {
944            return mYProgress;
945        }
946
947        // Used for animations
948        @SuppressWarnings({"UnusedDeclaration"})
949        public float getXProgress() {
950            return mXProgress;
951        }
952    }
953
954    /**
955     * {@inheritDoc}
956     */
957    @Override
958    public void onRemoteAdapterConnected() {
959        super.onRemoteAdapterConnected();
960        // On first run, we want to set the stack to the end.
961        if (mWhichChild == -1) {
962            mWhichChild = 0;
963        }
964        setDisplayedChild(mWhichChild);
965    }
966
967    LayoutParams createOrReuseLayoutParams(View v) {
968        final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
969        if (currentLp instanceof LayoutParams) {
970            LayoutParams lp = (LayoutParams) currentLp;
971            lp.setHorizontalOffset(0);
972            lp.setVerticalOffset(0);
973            lp.width = 0;
974            lp.width = 0;
975            return lp;
976        }
977        return new LayoutParams(v);
978    }
979
980    @Override
981    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
982        boolean dataChanged = mDataChanged;
983        if (dataChanged) {
984            handleDataChanged();
985
986            // if the data changes, mWhichChild might be out of the bounds of the adapter
987            // in this case, we reset mWhichChild to the beginning
988            if (mWhichChild >= mAdapter.getCount())
989                mWhichChild = 0;
990
991            showOnly(mWhichChild, true, true);
992            refreshChildren();
993        }
994
995        final int childCount = getChildCount();
996        for (int i = 0; i < childCount; i++) {
997            final View child = getChildAt(i);
998
999            int childRight = mPaddingLeft + child.getMeasuredWidth();
1000            int childBottom = mPaddingTop + child.getMeasuredHeight();
1001            LayoutParams lp = (LayoutParams) child.getLayoutParams();
1002
1003            child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
1004                    childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
1005
1006        }
1007
1008        mDataChanged = false;
1009        onLayout();
1010    }
1011
1012    @Override
1013    public void advance() {
1014        long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
1015
1016        if (mAdapter == null) return;
1017        final int adapterCount = mAdapter.getCount();
1018        if (adapterCount == 1 && mLoopViews) return;
1019
1020        if (mSwipeGestureType == GESTURE_NONE &&
1021                timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
1022            showNext();
1023        }
1024    }
1025
1026    private void measureChildren() {
1027        final int count = getChildCount();
1028
1029        final int measuredWidth = getMeasuredWidth();
1030        final int measuredHeight = getMeasuredHeight();
1031
1032        final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
1033                - mPaddingLeft - mPaddingRight;
1034        final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
1035                - mPaddingTop - mPaddingBottom;
1036
1037        int maxWidth = 0;
1038        int maxHeight = 0;
1039
1040        for (int i = 0; i < count; i++) {
1041            final View child = getChildAt(i);
1042            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
1043                    MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
1044
1045            if (child != mHighlight && child != mClickFeedback) {
1046                final int childMeasuredWidth = child.getMeasuredWidth();
1047                final int childMeasuredHeight = child.getMeasuredHeight();
1048                if (childMeasuredWidth > maxWidth) {
1049                    maxWidth = childMeasuredWidth;
1050                }
1051                if (childMeasuredHeight > maxHeight) {
1052                    maxHeight = childMeasuredHeight;
1053                }
1054            }
1055        }
1056
1057        mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
1058        mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
1059        if (maxWidth > 0 && maxWidth < childWidth) {
1060            mNewPerspectiveShiftX = measuredWidth - maxWidth;
1061        }
1062
1063        if (maxHeight > 0 && maxHeight < childHeight) {
1064            mNewPerspectiveShiftY = measuredHeight - maxHeight;
1065        }
1066    }
1067
1068    @Override
1069    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1070        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
1071        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
1072        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
1073        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
1074
1075        boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
1076
1077        // We need to deal with the case where our parent hasn't told us how
1078        // big we should be. In this case we should
1079        float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
1080        if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
1081            heightSpecSize = haveChildRefSize ?
1082                    Math.round(mReferenceChildHeight * (1 + factorY)) +
1083                    mPaddingTop + mPaddingBottom : 0;
1084        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
1085            if (haveChildRefSize) {
1086                int height = Math.round(mReferenceChildHeight * (1 + factorY))
1087                        + mPaddingTop + mPaddingBottom;
1088                if (height <= heightSpecSize) {
1089                    heightSpecSize = height;
1090                } else {
1091                    heightSpecSize |= MEASURED_STATE_TOO_SMALL;
1092                }
1093            } else {
1094                heightSpecSize = 0;
1095            }
1096        }
1097
1098        float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
1099        if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
1100            widthSpecSize = haveChildRefSize ?
1101                    Math.round(mReferenceChildWidth * (1 + factorX)) +
1102                    mPaddingLeft + mPaddingRight : 0;
1103        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
1104            if (haveChildRefSize) {
1105                int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
1106                if (width <= widthSpecSize) {
1107                    widthSpecSize = width;
1108                } else {
1109                    widthSpecSize |= MEASURED_STATE_TOO_SMALL;
1110                }
1111            } else {
1112                widthSpecSize = 0;
1113            }
1114        }
1115
1116        setMeasuredDimension(widthSpecSize, heightSpecSize);
1117        measureChildren();
1118    }
1119
1120    class LayoutParams extends ViewGroup.LayoutParams {
1121        int horizontalOffset;
1122        int verticalOffset;
1123        View mView;
1124        private final Rect parentRect = new Rect();
1125        private final Rect invalidateRect = new Rect();
1126        private final RectF invalidateRectf = new RectF();
1127        private final Rect globalInvalidateRect = new Rect();
1128
1129        LayoutParams(View view) {
1130            super(0, 0);
1131            width = 0;
1132            height = 0;
1133            horizontalOffset = 0;
1134            verticalOffset = 0;
1135            mView = view;
1136        }
1137
1138        LayoutParams(Context c, AttributeSet attrs) {
1139            super(c, attrs);
1140            horizontalOffset = 0;
1141            verticalOffset = 0;
1142            width = 0;
1143            height = 0;
1144        }
1145
1146        void invalidateGlobalRegion(View v, Rect r) {
1147            // We need to make a new rect here, so as not to modify the one passed
1148            globalInvalidateRect.set(r);
1149            View p = v;
1150            if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1151
1152            boolean firstPass = true;
1153            parentRect.set(0, 0, 0, 0);
1154            int depth = 0;
1155            while (p.getParent() != null && p.getParent() instanceof View
1156                    && !parentRect.contains(globalInvalidateRect)) {
1157                if (!firstPass) {
1158                    globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1159                            - p.getScrollY());
1160                    depth++;
1161                }
1162                firstPass = false;
1163                p = (View) p.getParent();
1164                parentRect.set(p.getScrollX(), p.getScrollY(),
1165                               p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1166
1167            }
1168
1169            p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1170                    globalInvalidateRect.right, globalInvalidateRect.bottom);
1171        }
1172
1173        Rect getInvalidateRect() {
1174            return invalidateRect;
1175        }
1176
1177        void resetInvalidateRect() {
1178            invalidateRect.set(0, 0, 0, 0);
1179        }
1180
1181        // This is public so that ObjectAnimator can access it
1182        public void setVerticalOffset(int newVerticalOffset) {
1183            setOffsets(horizontalOffset, newVerticalOffset);
1184        }
1185
1186        public void setHorizontalOffset(int newHorizontalOffset) {
1187            setOffsets(newHorizontalOffset, verticalOffset);
1188        }
1189
1190        public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1191            int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1192            horizontalOffset = newHorizontalOffset;
1193            int verticalOffsetDelta = newVerticalOffset - verticalOffset;
1194            verticalOffset = newVerticalOffset;
1195
1196            if (mView != null) {
1197                mView.requestLayout();
1198                int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1199                int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1200                int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1201                int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
1202
1203                invalidateRectf.set(left, top, right, bottom);
1204
1205                float xoffset = -invalidateRectf.left;
1206                float yoffset = -invalidateRectf.top;
1207                invalidateRectf.offset(xoffset, yoffset);
1208                mView.getMatrix().mapRect(invalidateRectf);
1209                invalidateRectf.offset(-xoffset, -yoffset);
1210
1211                invalidateRect.set((int) Math.floor(invalidateRectf.left),
1212                        (int) Math.floor(invalidateRectf.top),
1213                        (int) Math.ceil(invalidateRectf.right),
1214                        (int) Math.ceil(invalidateRectf.bottom));
1215
1216                invalidateGlobalRegion(mView, invalidateRect);
1217            }
1218        }
1219    }
1220
1221    private static class HolographicHelper {
1222        private final Paint mHolographicPaint = new Paint();
1223        private final Paint mErasePaint = new Paint();
1224        private final Paint mBlurPaint = new Paint();
1225        private static final int RES_OUT = 0;
1226        private static final int CLICK_FEEDBACK = 1;
1227        private float mDensity;
1228        private BlurMaskFilter mSmallBlurMaskFilter;
1229        private BlurMaskFilter mLargeBlurMaskFilter;
1230        private final Canvas mCanvas = new Canvas();
1231        private final Canvas mMaskCanvas = new Canvas();
1232        private final int[] mTmpXY = new int[2];
1233        private final Matrix mIdentityMatrix = new Matrix();
1234
1235        HolographicHelper(Context context) {
1236            mDensity = context.getResources().getDisplayMetrics().density;
1237
1238            mHolographicPaint.setFilterBitmap(true);
1239            mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
1240            mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1241            mErasePaint.setFilterBitmap(true);
1242
1243            mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1244            mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
1245        }
1246
1247        Bitmap createOutline(View v) {
1248            return createOutline(v, RES_OUT);
1249        }
1250
1251        Bitmap createOutline(View v, int type) {
1252            if (type == RES_OUT) {
1253                mHolographicPaint.setColor(0xff6699ff);
1254                mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
1255            } else if (type == CLICK_FEEDBACK) {
1256                mHolographicPaint.setColor(0x886699ff);
1257                mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
1258            }
1259
1260            if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1261                return null;
1262            }
1263
1264            Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
1265                    Bitmap.Config.ARGB_8888);
1266            mCanvas.setBitmap(bitmap);
1267
1268            float rotationX = v.getRotationX();
1269            float rotation = v.getRotation();
1270            float translationY = v.getTranslationY();
1271            float translationX = v.getTranslationX();
1272            v.setRotationX(0);
1273            v.setRotation(0);
1274            v.setTranslationY(0);
1275            v.setTranslationX(0);
1276            v.draw(mCanvas);
1277            v.setRotationX(rotationX);
1278            v.setRotation(rotation);
1279            v.setTranslationY(translationY);
1280            v.setTranslationX(translationX);
1281
1282            drawOutline(mCanvas, bitmap);
1283            return bitmap;
1284        }
1285
1286        void drawOutline(Canvas dest, Bitmap src) {
1287            final int[] xy = mTmpXY;
1288            Bitmap mask = src.extractAlpha(mBlurPaint, xy);
1289            mMaskCanvas.setBitmap(mask);
1290            mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
1291            dest.drawColor(0, PorterDuff.Mode.CLEAR);
1292            dest.setMatrix(mIdentityMatrix);
1293            dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
1294            mask.recycle();
1295        }
1296    }
1297}