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