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