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