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