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