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