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