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