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