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