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