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