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 android.animation.ObjectAnimator;
     19 import android.animation.PropertyValuesHolder;
     20 import android.content.Context;
     21 import android.content.res.TypedArray;
     22 import android.graphics.Bitmap;
     23 import android.graphics.BlurMaskFilter;
     24 import android.graphics.Canvas;
     25 import android.graphics.Matrix;
     26 import android.graphics.Paint;
     27 import android.graphics.PorterDuff;
     28 import android.graphics.PorterDuffXfermode;
     29 import android.graphics.Rect;
     30 import android.graphics.RectF;
     31 import android.graphics.Region;
     32 import android.graphics.TableMaskFilter;
     33 import android.os.Bundle;
     34 import android.util.AttributeSet;
     35 import android.util.Log;
     36 import android.view.InputDevice;
     37 import android.view.MotionEvent;
     38 import android.view.VelocityTracker;
     39 import android.view.View;
     40 import android.view.ViewConfiguration;
     41 import android.view.ViewGroup;
     42 import android.view.accessibility.AccessibilityNodeInfo;
     43 import android.view.animation.LinearInterpolator;
     44 import android.widget.RemoteViews.RemoteView;
     45 
     46 import java.lang.ref.WeakReference;
     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                 if (d > maxd) {
   1055                     // Because mSlideAmount is updated in onLayout(), it is possible that d > maxd
   1056                     // if we get onLayout() right before this method is called.
   1057                     d = maxd;
   1058                 }
   1059 
   1060                 if (velocity == 0) {
   1061                     return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
   1062                 } else {
   1063                     float duration = invert ? d / Math.abs(velocity) :
   1064                             (maxd - d) / Math.abs(velocity);
   1065                     if (duration < MINIMUM_ANIMATION_DURATION ||
   1066                             duration > DEFAULT_ANIMATION_DURATION) {
   1067                         return getDuration(invert, 0);
   1068                     } else {
   1069                         return duration;
   1070                     }
   1071                 }
   1072             }
   1073             return 0;
   1074         }
   1075 
   1076         // Used for animations
   1077         @SuppressWarnings({"UnusedDeclaration"})
   1078         public float getYProgress() {
   1079             return mYProgress;
   1080         }
   1081 
   1082         // Used for animations
   1083         @SuppressWarnings({"UnusedDeclaration"})
   1084         public float getXProgress() {
   1085             return mXProgress;
   1086         }
   1087     }
   1088 
   1089     LayoutParams createOrReuseLayoutParams(View v) {
   1090         final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
   1091         if (currentLp instanceof LayoutParams) {
   1092             LayoutParams lp = (LayoutParams) currentLp;
   1093             lp.setHorizontalOffset(0);
   1094             lp.setVerticalOffset(0);
   1095             lp.width = 0;
   1096             lp.width = 0;
   1097             return lp;
   1098         }
   1099         return new LayoutParams(v);
   1100     }
   1101 
   1102     @Override
   1103     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
   1104         checkForAndHandleDataChanged();
   1105 
   1106         final int childCount = getChildCount();
   1107         for (int i = 0; i < childCount; i++) {
   1108             final View child = getChildAt(i);
   1109 
   1110             int childRight = mPaddingLeft + child.getMeasuredWidth();
   1111             int childBottom = mPaddingTop + child.getMeasuredHeight();
   1112             LayoutParams lp = (LayoutParams) child.getLayoutParams();
   1113 
   1114             child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
   1115                     childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
   1116 
   1117         }
   1118         onLayout();
   1119     }
   1120 
   1121     @Override
   1122     public void advance() {
   1123         long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
   1124 
   1125         if (mAdapter == null) return;
   1126         final int adapterCount = getCount();
   1127         if (adapterCount == 1 && mLoopViews) return;
   1128 
   1129         if (mSwipeGestureType == GESTURE_NONE &&
   1130                 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
   1131             showNext();
   1132         }
   1133     }
   1134 
   1135     private void measureChildren() {
   1136         final int count = getChildCount();
   1137 
   1138         final int measuredWidth = getMeasuredWidth();
   1139         final int measuredHeight = getMeasuredHeight();
   1140 
   1141         final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
   1142                 - mPaddingLeft - mPaddingRight;
   1143         final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
   1144                 - mPaddingTop - mPaddingBottom;
   1145 
   1146         int maxWidth = 0;
   1147         int maxHeight = 0;
   1148 
   1149         for (int i = 0; i < count; i++) {
   1150             final View child = getChildAt(i);
   1151             child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
   1152                     MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
   1153 
   1154             if (child != mHighlight && child != mClickFeedback) {
   1155                 final int childMeasuredWidth = child.getMeasuredWidth();
   1156                 final int childMeasuredHeight = child.getMeasuredHeight();
   1157                 if (childMeasuredWidth > maxWidth) {
   1158                     maxWidth = childMeasuredWidth;
   1159                 }
   1160                 if (childMeasuredHeight > maxHeight) {
   1161                     maxHeight = childMeasuredHeight;
   1162                 }
   1163             }
   1164         }
   1165 
   1166         mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
   1167         mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
   1168 
   1169         // If we have extra space, we try and spread the items out
   1170         if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
   1171             mNewPerspectiveShiftX = measuredWidth - maxWidth;
   1172         }
   1173 
   1174         if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
   1175             mNewPerspectiveShiftY = measuredHeight - maxHeight;
   1176         }
   1177     }
   1178 
   1179     @Override
   1180     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   1181         int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
   1182         int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
   1183         final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
   1184         final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
   1185 
   1186         boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
   1187 
   1188         // We need to deal with the case where our parent hasn't told us how
   1189         // big we should be. In this case we should
   1190         float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
   1191         if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
   1192             heightSpecSize = haveChildRefSize ?
   1193                     Math.round(mReferenceChildHeight * (1 + factorY)) +
   1194                     mPaddingTop + mPaddingBottom : 0;
   1195         } else if (heightSpecMode == MeasureSpec.AT_MOST) {
   1196             if (haveChildRefSize) {
   1197                 int height = Math.round(mReferenceChildHeight * (1 + factorY))
   1198                         + mPaddingTop + mPaddingBottom;
   1199                 if (height <= heightSpecSize) {
   1200                     heightSpecSize = height;
   1201                 } else {
   1202                     heightSpecSize |= MEASURED_STATE_TOO_SMALL;
   1203 
   1204                 }
   1205             } else {
   1206                 heightSpecSize = 0;
   1207             }
   1208         }
   1209 
   1210         float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
   1211         if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
   1212             widthSpecSize = haveChildRefSize ?
   1213                     Math.round(mReferenceChildWidth * (1 + factorX)) +
   1214                     mPaddingLeft + mPaddingRight : 0;
   1215         } else if (heightSpecMode == MeasureSpec.AT_MOST) {
   1216             if (haveChildRefSize) {
   1217                 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
   1218                 if (width <= widthSpecSize) {
   1219                     widthSpecSize = width;
   1220                 } else {
   1221                     widthSpecSize |= MEASURED_STATE_TOO_SMALL;
   1222                 }
   1223             } else {
   1224                 widthSpecSize = 0;
   1225             }
   1226         }
   1227         setMeasuredDimension(widthSpecSize, heightSpecSize);
   1228         measureChildren();
   1229     }
   1230 
   1231     @Override
   1232     public CharSequence getAccessibilityClassName() {
   1233         return StackView.class.getName();
   1234     }
   1235 
   1236     /** @hide */
   1237     @Override
   1238     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
   1239         super.onInitializeAccessibilityNodeInfoInternal(info);
   1240         info.setScrollable(getChildCount() > 1);
   1241         if (isEnabled()) {
   1242             if (getDisplayedChild() < getChildCount() - 1) {
   1243                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
   1244             }
   1245             if (getDisplayedChild() > 0) {
   1246                 info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
   1247             }
   1248         }
   1249     }
   1250 
   1251     /** @hide */
   1252     @Override
   1253     public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
   1254         if (super.performAccessibilityActionInternal(action, arguments)) {
   1255             return true;
   1256         }
   1257         if (!isEnabled()) {
   1258             return false;
   1259         }
   1260         switch (action) {
   1261             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
   1262                 if (getDisplayedChild() < getChildCount() - 1) {
   1263                     showNext();
   1264                     return true;
   1265                 }
   1266             } return false;
   1267             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
   1268                 if (getDisplayedChild() > 0) {
   1269                     showPrevious();
   1270                     return true;
   1271                 }
   1272             } return false;
   1273         }
   1274         return false;
   1275     }
   1276 
   1277     class LayoutParams extends ViewGroup.LayoutParams {
   1278         int horizontalOffset;
   1279         int verticalOffset;
   1280         View mView;
   1281         private final Rect parentRect = new Rect();
   1282         private final Rect invalidateRect = new Rect();
   1283         private final RectF invalidateRectf = new RectF();
   1284         private final Rect globalInvalidateRect = new Rect();
   1285 
   1286         LayoutParams(View view) {
   1287             super(0, 0);
   1288             width = 0;
   1289             height = 0;
   1290             horizontalOffset = 0;
   1291             verticalOffset = 0;
   1292             mView = view;
   1293         }
   1294 
   1295         LayoutParams(Context c, AttributeSet attrs) {
   1296             super(c, attrs);
   1297             horizontalOffset = 0;
   1298             verticalOffset = 0;
   1299             width = 0;
   1300             height = 0;
   1301         }
   1302 
   1303         void invalidateGlobalRegion(View v, Rect r) {
   1304             // We need to make a new rect here, so as not to modify the one passed
   1305             globalInvalidateRect.set(r);
   1306             globalInvalidateRect.union(0, 0, getWidth(), getHeight());
   1307             View p = v;
   1308             if (!(v.getParent() != null && v.getParent() instanceof View)) return;
   1309 
   1310             boolean firstPass = true;
   1311             parentRect.set(0, 0, 0, 0);
   1312             while (p.getParent() != null && p.getParent() instanceof View
   1313                     && !parentRect.contains(globalInvalidateRect)) {
   1314                 if (!firstPass) {
   1315                     globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
   1316                             - p.getScrollY());
   1317                 }
   1318                 firstPass = false;
   1319                 p = (View) p.getParent();
   1320                 parentRect.set(p.getScrollX(), p.getScrollY(),
   1321                         p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
   1322                 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
   1323                         globalInvalidateRect.right, globalInvalidateRect.bottom);
   1324             }
   1325 
   1326             p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
   1327                     globalInvalidateRect.right, globalInvalidateRect.bottom);
   1328         }
   1329 
   1330         Rect getInvalidateRect() {
   1331             return invalidateRect;
   1332         }
   1333 
   1334         void resetInvalidateRect() {
   1335             invalidateRect.set(0, 0, 0, 0);
   1336         }
   1337 
   1338         // This is public so that ObjectAnimator can access it
   1339         public void setVerticalOffset(int newVerticalOffset) {
   1340             setOffsets(horizontalOffset, newVerticalOffset);
   1341         }
   1342 
   1343         public void setHorizontalOffset(int newHorizontalOffset) {
   1344             setOffsets(newHorizontalOffset, verticalOffset);
   1345         }
   1346 
   1347         public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
   1348             int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
   1349             horizontalOffset = newHorizontalOffset;
   1350             int verticalOffsetDelta = newVerticalOffset - verticalOffset;
   1351             verticalOffset = newVerticalOffset;
   1352 
   1353             if (mView != null) {
   1354                 mView.requestLayout();
   1355                 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
   1356                 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
   1357                 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
   1358                 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
   1359 
   1360                 invalidateRectf.set(left, top, right, bottom);
   1361 
   1362                 float xoffset = -invalidateRectf.left;
   1363                 float yoffset = -invalidateRectf.top;
   1364                 invalidateRectf.offset(xoffset, yoffset);
   1365                 mView.getMatrix().mapRect(invalidateRectf);
   1366                 invalidateRectf.offset(-xoffset, -yoffset);
   1367 
   1368                 invalidateRect.set((int) Math.floor(invalidateRectf.left),
   1369                         (int) Math.floor(invalidateRectf.top),
   1370                         (int) Math.ceil(invalidateRectf.right),
   1371                         (int) Math.ceil(invalidateRectf.bottom));
   1372 
   1373                 invalidateGlobalRegion(mView, invalidateRect);
   1374             }
   1375         }
   1376     }
   1377 
   1378     private static class HolographicHelper {
   1379         private final Paint mHolographicPaint = new Paint();
   1380         private final Paint mErasePaint = new Paint();
   1381         private final Paint mBlurPaint = new Paint();
   1382         private static final int RES_OUT = 0;
   1383         private static final int CLICK_FEEDBACK = 1;
   1384         private float mDensity;
   1385         private BlurMaskFilter mSmallBlurMaskFilter;
   1386         private BlurMaskFilter mLargeBlurMaskFilter;
   1387         private final Canvas mCanvas = new Canvas();
   1388         private final Canvas mMaskCanvas = new Canvas();
   1389         private final int[] mTmpXY = new int[2];
   1390         private final Matrix mIdentityMatrix = new Matrix();
   1391 
   1392         HolographicHelper(Context context) {
   1393             mDensity = context.getResources().getDisplayMetrics().density;
   1394 
   1395             mHolographicPaint.setFilterBitmap(true);
   1396             mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
   1397             mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
   1398             mErasePaint.setFilterBitmap(true);
   1399 
   1400             mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
   1401             mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
   1402         }
   1403 
   1404         Bitmap createClickOutline(View v, int color) {
   1405             return createOutline(v, CLICK_FEEDBACK, color);
   1406         }
   1407 
   1408         Bitmap createResOutline(View v, int color) {
   1409             return createOutline(v, RES_OUT, color);
   1410         }
   1411 
   1412         Bitmap createOutline(View v, int type, int color) {
   1413             mHolographicPaint.setColor(color);
   1414             if (type == RES_OUT) {
   1415                 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
   1416             } else if (type == CLICK_FEEDBACK) {
   1417                 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
   1418             }
   1419 
   1420             if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
   1421                 return null;
   1422             }
   1423 
   1424             Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
   1425                     v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
   1426             mCanvas.setBitmap(bitmap);
   1427 
   1428             float rotationX = v.getRotationX();
   1429             float rotation = v.getRotation();
   1430             float translationY = v.getTranslationY();
   1431             float translationX = v.getTranslationX();
   1432             v.setRotationX(0);
   1433             v.setRotation(0);
   1434             v.setTranslationY(0);
   1435             v.setTranslationX(0);
   1436             v.draw(mCanvas);
   1437             v.setRotationX(rotationX);
   1438             v.setRotation(rotation);
   1439             v.setTranslationY(translationY);
   1440             v.setTranslationX(translationX);
   1441 
   1442             drawOutline(mCanvas, bitmap);
   1443             mCanvas.setBitmap(null);
   1444             return bitmap;
   1445         }
   1446 
   1447         void drawOutline(Canvas dest, Bitmap src) {
   1448             final int[] xy = mTmpXY;
   1449             Bitmap mask = src.extractAlpha(mBlurPaint, xy);
   1450             mMaskCanvas.setBitmap(mask);
   1451             mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
   1452             dest.drawColor(0, PorterDuff.Mode.CLEAR);
   1453             dest.setMatrix(mIdentityMatrix);
   1454             dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
   1455             mMaskCanvas.setBitmap(null);
   1456             mask.recycle();
   1457         }
   1458     }
   1459 }
   1460