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