| /* Copyright (C) 2010 The Android Open Source Project | 
 |  * | 
 |  * Licensed under the Apache License, Version 2.0 (the "License"); | 
 |  * you may not use this file except in compliance with the License. | 
 |  * You may obtain a copy of the License at | 
 |  * | 
 |  *      http://www.apache.org/licenses/LICENSE-2.0 | 
 |  * | 
 |  * Unless required by applicable law or agreed to in writing, software | 
 |  * distributed under the License is distributed on an "AS IS" BASIS, | 
 |  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
 |  * See the License for the specific language governing permissions and | 
 |  * limitations under the License. | 
 |  */ | 
 |  | 
 | package android.widget; | 
 |  | 
 | import android.animation.ObjectAnimator; | 
 | import android.animation.PropertyValuesHolder; | 
 | import android.content.Context; | 
 | import android.content.res.TypedArray; | 
 | import android.graphics.Bitmap; | 
 | import android.graphics.BlurMaskFilter; | 
 | import android.graphics.Canvas; | 
 | import android.graphics.Matrix; | 
 | import android.graphics.Paint; | 
 | import android.graphics.PorterDuff; | 
 | import android.graphics.PorterDuffXfermode; | 
 | import android.graphics.Rect; | 
 | import android.graphics.RectF; | 
 | import android.graphics.TableMaskFilter; | 
 | import android.os.Bundle; | 
 | import android.util.AttributeSet; | 
 | import android.util.Log; | 
 | import android.view.InputDevice; | 
 | import android.view.MotionEvent; | 
 | import android.view.VelocityTracker; | 
 | import android.view.View; | 
 | import android.view.ViewConfiguration; | 
 | import android.view.ViewGroup; | 
 | import android.view.accessibility.AccessibilityNodeInfo; | 
 | import android.view.animation.LinearInterpolator; | 
 | import android.widget.RemoteViews.RemoteView; | 
 |  | 
 | import com.android.internal.R; | 
 |  | 
 | import java.lang.ref.WeakReference; | 
 |  | 
 | @RemoteView | 
 | /** | 
 |  * A view that displays its children in a stack and allows users to discretely swipe | 
 |  * through the children. | 
 |  */ | 
 | public class StackView extends AdapterViewAnimator { | 
 |     private final String TAG = "StackView"; | 
 |  | 
 |     /** | 
 |      * Default animation parameters | 
 |      */ | 
 |     private static final int DEFAULT_ANIMATION_DURATION = 400; | 
 |     private static final int MINIMUM_ANIMATION_DURATION = 50; | 
 |     private static final int STACK_RELAYOUT_DURATION = 100; | 
 |  | 
 |     /** | 
 |      * Parameters effecting the perspective visuals | 
 |      */ | 
 |     private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f; | 
 |     private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f; | 
 |  | 
 |     private float mPerspectiveShiftX; | 
 |     private float mPerspectiveShiftY; | 
 |     private float mNewPerspectiveShiftX; | 
 |     private float mNewPerspectiveShiftY; | 
 |  | 
 |     @SuppressWarnings({"FieldCanBeLocal"}) | 
 |     private static final float PERSPECTIVE_SCALE_FACTOR = 0f; | 
 |  | 
 |     /** | 
 |      * Represent the two possible stack modes, one where items slide up, and the other | 
 |      * where items slide down. The perspective is also inverted between these two modes. | 
 |      */ | 
 |     private static final int ITEMS_SLIDE_UP = 0; | 
 |     private static final int ITEMS_SLIDE_DOWN = 1; | 
 |  | 
 |     /** | 
 |      * These specify the different gesture states | 
 |      */ | 
 |     private static final int GESTURE_NONE = 0; | 
 |     private static final int GESTURE_SLIDE_UP = 1; | 
 |     private static final int GESTURE_SLIDE_DOWN = 2; | 
 |  | 
 |     /** | 
 |      * Specifies how far you need to swipe (up or down) before it | 
 |      * will be consider a completed gesture when you lift your finger | 
 |      */ | 
 |     private static final float SWIPE_THRESHOLD_RATIO = 0.2f; | 
 |  | 
 |     /** | 
 |      * Specifies the total distance, relative to the size of the stack, | 
 |      * that views will be slid, either up or down | 
 |      */ | 
 |     private static final float SLIDE_UP_RATIO = 0.7f; | 
 |  | 
 |     /** | 
 |      * Sentinel value for no current active pointer. | 
 |      * Used by {@link #mActivePointerId}. | 
 |      */ | 
 |     private static final int INVALID_POINTER = -1; | 
 |  | 
 |     /** | 
 |      * Number of active views in the stack. One fewer view is actually visible, as one is hidden. | 
 |      */ | 
 |     private static final int NUM_ACTIVE_VIEWS = 5; | 
 |  | 
 |     private static final int FRAME_PADDING = 4; | 
 |  | 
 |     private final Rect mTouchRect = new Rect(); | 
 |  | 
 |     private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000; | 
 |  | 
 |     private static final long MIN_TIME_BETWEEN_SCROLLS = 100; | 
 |  | 
 |     /** | 
 |      * These variables are all related to the current state of touch interaction | 
 |      * with the stack | 
 |      */ | 
 |     private float mInitialY; | 
 |     private float mInitialX; | 
 |     private int mActivePointerId; | 
 |     private int mYVelocity = 0; | 
 |     private int mSwipeGestureType = GESTURE_NONE; | 
 |     private int mSlideAmount; | 
 |     private int mSwipeThreshold; | 
 |     private int mTouchSlop; | 
 |     private int mMaximumVelocity; | 
 |     private VelocityTracker mVelocityTracker; | 
 |     private boolean mTransitionIsSetup = false; | 
 |     private int mResOutColor; | 
 |     private int mClickColor; | 
 |  | 
 |     private static HolographicHelper sHolographicHelper; | 
 |     private ImageView mHighlight; | 
 |     private ImageView mClickFeedback; | 
 |     private boolean mClickFeedbackIsValid = false; | 
 |     private StackSlider mStackSlider; | 
 |     private boolean mFirstLayoutHappened = false; | 
 |     private long mLastInteractionTime = 0; | 
 |     private long mLastScrollTime; | 
 |     private int mStackMode; | 
 |     private int mFramePadding; | 
 |     private final Rect stackInvalidateRect = new Rect(); | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     public StackView(Context context) { | 
 |         this(context, null); | 
 |     } | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     public StackView(Context context, AttributeSet attrs) { | 
 |         this(context, attrs, com.android.internal.R.attr.stackViewStyle); | 
 |     } | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     public StackView(Context context, AttributeSet attrs, int defStyleAttr) { | 
 |         this(context, attrs, defStyleAttr, 0); | 
 |     } | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { | 
 |         super(context, attrs, defStyleAttr, defStyleRes); | 
 |         final TypedArray a = context.obtainStyledAttributes( | 
 |                 attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes); | 
 |         saveAttributeDataForStyleable(context, com.android.internal.R.styleable.StackView, | 
 |                 attrs, a, defStyleAttr, defStyleRes); | 
 |  | 
 |         mResOutColor = a.getColor( | 
 |                 com.android.internal.R.styleable.StackView_resOutColor, 0); | 
 |         mClickColor = a.getColor( | 
 |                 com.android.internal.R.styleable.StackView_clickColor, 0); | 
 |  | 
 |         a.recycle(); | 
 |         initStackView(); | 
 |     } | 
 |  | 
 |     private void initStackView() { | 
 |         configureViewAnimator(NUM_ACTIVE_VIEWS, 1); | 
 |         setStaticTransformationsEnabled(true); | 
 |         final ViewConfiguration configuration = ViewConfiguration.get(getContext()); | 
 |         mTouchSlop = configuration.getScaledTouchSlop(); | 
 |         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); | 
 |         mActivePointerId = INVALID_POINTER; | 
 |  | 
 |         mHighlight = new ImageView(getContext()); | 
 |         mHighlight.setLayoutParams(new LayoutParams(mHighlight)); | 
 |         addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight)); | 
 |  | 
 |         mClickFeedback = new ImageView(getContext()); | 
 |         mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback)); | 
 |         addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback)); | 
 |         mClickFeedback.setVisibility(INVISIBLE); | 
 |  | 
 |         mStackSlider = new StackSlider(); | 
 |  | 
 |         if (sHolographicHelper == null) { | 
 |             sHolographicHelper = new HolographicHelper(mContext); | 
 |         } | 
 |         setClipChildren(false); | 
 |         setClipToPadding(false); | 
 |  | 
 |         // This sets the form of the StackView, which is currently to have the perspective-shifted | 
 |         // views above the active view, and have items slide down when sliding out. The opposite is | 
 |         // available by using ITEMS_SLIDE_UP. | 
 |         mStackMode = ITEMS_SLIDE_DOWN; | 
 |  | 
 |         // This is a flag to indicate the the stack is loading for the first time | 
 |         mWhichChild = -1; | 
 |  | 
 |         // Adjust the frame padding based on the density, since the highlight changes based | 
 |         // on the density | 
 |         final float density = mContext.getResources().getDisplayMetrics().density; | 
 |         mFramePadding = (int) Math.ceil(density * FRAME_PADDING); | 
 |     } | 
 |  | 
 |     /** | 
 |      * Animate the views between different relative indexes within the {@link AdapterViewAnimator} | 
 |      */ | 
 |     void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) { | 
 |         if (!animate) { | 
 |             ((StackFrame) view).cancelSliderAnimator(); | 
 |             view.setRotationX(0f); | 
 |             LayoutParams lp = (LayoutParams) view.getLayoutParams(); | 
 |             lp.setVerticalOffset(0); | 
 |             lp.setHorizontalOffset(0); | 
 |         } | 
 |  | 
 |         if (fromIndex == -1 && toIndex == getNumActiveViews() -1) { | 
 |             transformViewAtIndex(toIndex, view, false); | 
 |             view.setVisibility(VISIBLE); | 
 |             view.setAlpha(1.0f); | 
 |         } else if (fromIndex == 0 && toIndex == 1) { | 
 |             // Slide item in | 
 |             ((StackFrame) view).cancelSliderAnimator(); | 
 |             view.setVisibility(VISIBLE); | 
 |  | 
 |             int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity)); | 
 |             StackSlider animationSlider = new StackSlider(mStackSlider); | 
 |             animationSlider.setView(view); | 
 |  | 
 |             if (animate) { | 
 |                 PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f); | 
 |                 PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); | 
 |                 ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider, | 
 |                         slideInX, slideInY); | 
 |                 slideIn.setDuration(duration); | 
 |                 slideIn.setInterpolator(new LinearInterpolator()); | 
 |                 ((StackFrame) view).setSliderAnimator(slideIn); | 
 |                 slideIn.start(); | 
 |             } else { | 
 |                 animationSlider.setYProgress(0f); | 
 |                 animationSlider.setXProgress(0f); | 
 |             } | 
 |         } else if (fromIndex == 1 && toIndex == 0) { | 
 |             // Slide item out | 
 |             ((StackFrame) view).cancelSliderAnimator(); | 
 |             int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity)); | 
 |  | 
 |             StackSlider animationSlider = new StackSlider(mStackSlider); | 
 |             animationSlider.setView(view); | 
 |             if (animate) { | 
 |                 PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f); | 
 |                 PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); | 
 |                 ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider, | 
 |                         slideOutX, slideOutY); | 
 |                 slideOut.setDuration(duration); | 
 |                 slideOut.setInterpolator(new LinearInterpolator()); | 
 |                 ((StackFrame) view).setSliderAnimator(slideOut); | 
 |                 slideOut.start(); | 
 |             } else { | 
 |                 animationSlider.setYProgress(1.0f); | 
 |                 animationSlider.setXProgress(0f); | 
 |             } | 
 |         } else if (toIndex == 0) { | 
 |             // Make sure this view that is "waiting in the wings" is invisible | 
 |             view.setAlpha(0.0f); | 
 |             view.setVisibility(INVISIBLE); | 
 |         } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) { | 
 |             view.setVisibility(VISIBLE); | 
 |             view.setAlpha(1.0f); | 
 |             view.setRotationX(0f); | 
 |             LayoutParams lp = (LayoutParams) view.getLayoutParams(); | 
 |             lp.setVerticalOffset(0); | 
 |             lp.setHorizontalOffset(0); | 
 |         } else if (fromIndex == -1) { | 
 |             view.setAlpha(1.0f); | 
 |             view.setVisibility(VISIBLE); | 
 |         } else if (toIndex == -1) { | 
 |             if (animate) { | 
 |                 postDelayed(new Runnable() { | 
 |                     public void run() { | 
 |                         view.setAlpha(0); | 
 |                     } | 
 |                 }, STACK_RELAYOUT_DURATION); | 
 |             } else { | 
 |                 view.setAlpha(0f); | 
 |             } | 
 |         } | 
 |  | 
 |         // Implement the faked perspective | 
 |         if (toIndex != -1) { | 
 |             transformViewAtIndex(toIndex, view, animate); | 
 |         } | 
 |     } | 
 |  | 
 |     private void transformViewAtIndex(int index, final View view, boolean animate) { | 
 |         final float maxPerspectiveShiftY = mPerspectiveShiftY; | 
 |         final float maxPerspectiveShiftX = mPerspectiveShiftX; | 
 |  | 
 |         if (mStackMode == ITEMS_SLIDE_DOWN) { | 
 |             index = mMaxNumActiveViews - index - 1; | 
 |             if (index == mMaxNumActiveViews - 1) index--; | 
 |         } else { | 
 |             index--; | 
 |             if (index < 0) index++; | 
 |         } | 
 |  | 
 |         float r = (index * 1.0f) / (mMaxNumActiveViews - 2); | 
 |  | 
 |         final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r); | 
 |  | 
 |         float perspectiveTranslationY = r * maxPerspectiveShiftY; | 
 |         float scaleShiftCorrectionY = (scale - 1) * | 
 |                 (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f); | 
 |         final float transY = perspectiveTranslationY + scaleShiftCorrectionY; | 
 |  | 
 |         float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX; | 
 |         float scaleShiftCorrectionX =  (1 - scale) * | 
 |                 (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f); | 
 |         final float transX = perspectiveTranslationX + scaleShiftCorrectionX; | 
 |  | 
 |         // If this view is currently being animated for a certain position, we need to cancel | 
 |         // this animation so as not to interfere with the new transformation. | 
 |         if (view instanceof StackFrame) { | 
 |             ((StackFrame) view).cancelTransformAnimator(); | 
 |         } | 
 |  | 
 |         if (animate) { | 
 |             PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX); | 
 |             PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY); | 
 |             PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale); | 
 |             PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale); | 
 |  | 
 |             ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY, | 
 |                     translationY, translationX); | 
 |             oa.setDuration(STACK_RELAYOUT_DURATION); | 
 |             if (view instanceof StackFrame) { | 
 |                 ((StackFrame) view).setTransformAnimator(oa); | 
 |             } | 
 |             oa.start(); | 
 |         } else { | 
 |             view.setTranslationX(transX); | 
 |             view.setTranslationY(transY); | 
 |             view.setScaleX(scale); | 
 |             view.setScaleY(scale); | 
 |         } | 
 |     } | 
 |  | 
 |     private void setupStackSlider(View v, int mode) { | 
 |         mStackSlider.setMode(mode); | 
 |         if (v != null) { | 
 |             mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor)); | 
 |             mHighlight.setRotation(v.getRotation()); | 
 |             mHighlight.setTranslationY(v.getTranslationY()); | 
 |             mHighlight.setTranslationX(v.getTranslationX()); | 
 |             mHighlight.bringToFront(); | 
 |             v.bringToFront(); | 
 |             mStackSlider.setView(v); | 
 |  | 
 |             v.setVisibility(VISIBLE); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     @Override | 
 |     @android.view.RemotableViewMethod | 
 |     public void showNext() { | 
 |         if (mSwipeGestureType != GESTURE_NONE) return; | 
 |         if (!mTransitionIsSetup) { | 
 |             View v = getViewAtRelativeIndex(1); | 
 |             if (v != null) { | 
 |                 setupStackSlider(v, StackSlider.NORMAL_MODE); | 
 |                 mStackSlider.setYProgress(0); | 
 |                 mStackSlider.setXProgress(0); | 
 |             } | 
 |         } | 
 |         super.showNext(); | 
 |     } | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     @Override | 
 |     @android.view.RemotableViewMethod | 
 |     public void showPrevious() { | 
 |         if (mSwipeGestureType != GESTURE_NONE) return; | 
 |         if (!mTransitionIsSetup) { | 
 |             View v = getViewAtRelativeIndex(0); | 
 |             if (v != null) { | 
 |                 setupStackSlider(v, StackSlider.NORMAL_MODE); | 
 |                 mStackSlider.setYProgress(1); | 
 |                 mStackSlider.setXProgress(0); | 
 |             } | 
 |         } | 
 |         super.showPrevious(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     void showOnly(int childIndex, boolean animate) { | 
 |         super.showOnly(childIndex, animate); | 
 |  | 
 |         // Here we need to make sure that the z-order of the children is correct | 
 |         for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) { | 
 |             int index = modulo(i, getWindowSize()); | 
 |             ViewAndMetaData vm = mViewsMap.get(index); | 
 |             if (vm != null) { | 
 |                 View v = mViewsMap.get(index).view; | 
 |                 if (v != null) v.bringToFront(); | 
 |             } | 
 |         } | 
 |         if (mHighlight != null) { | 
 |             mHighlight.bringToFront(); | 
 |         } | 
 |         mTransitionIsSetup = false; | 
 |         mClickFeedbackIsValid = false; | 
 |     } | 
 |  | 
 |     void updateClickFeedback() { | 
 |         if (!mClickFeedbackIsValid) { | 
 |             View v = getViewAtRelativeIndex(1); | 
 |             if (v != null) { | 
 |                 mClickFeedback.setImageBitmap( | 
 |                         sHolographicHelper.createClickOutline(v, mClickColor)); | 
 |                 mClickFeedback.setTranslationX(v.getTranslationX()); | 
 |                 mClickFeedback.setTranslationY(v.getTranslationY()); | 
 |             } | 
 |             mClickFeedbackIsValid = true; | 
 |         } | 
 |     } | 
 |  | 
 |     @Override | 
 |     void showTapFeedback(View v) { | 
 |         updateClickFeedback(); | 
 |         mClickFeedback.setVisibility(VISIBLE); | 
 |         mClickFeedback.bringToFront(); | 
 |         invalidate(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     void hideTapFeedback(View v) { | 
 |         mClickFeedback.setVisibility(INVISIBLE); | 
 |         invalidate(); | 
 |     } | 
 |  | 
 |     private void updateChildTransforms() { | 
 |         for (int i = 0; i < getNumActiveViews(); i++) { | 
 |             View v = getViewAtRelativeIndex(i); | 
 |             if (v != null) { | 
 |                 transformViewAtIndex(i, v, false); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     private static class StackFrame extends FrameLayout { | 
 |         WeakReference<ObjectAnimator> transformAnimator; | 
 |         WeakReference<ObjectAnimator> sliderAnimator; | 
 |  | 
 |         public StackFrame(Context context) { | 
 |             super(context); | 
 |         } | 
 |  | 
 |         void setTransformAnimator(ObjectAnimator oa) { | 
 |             transformAnimator = new WeakReference<ObjectAnimator>(oa); | 
 |         } | 
 |  | 
 |         void setSliderAnimator(ObjectAnimator oa) { | 
 |             sliderAnimator = new WeakReference<ObjectAnimator>(oa); | 
 |         } | 
 |  | 
 |         boolean cancelTransformAnimator() { | 
 |             if (transformAnimator != null) { | 
 |                 ObjectAnimator oa = transformAnimator.get(); | 
 |                 if (oa != null) { | 
 |                     oa.cancel(); | 
 |                     return true; | 
 |                 } | 
 |             } | 
 |             return false; | 
 |         } | 
 |  | 
 |         boolean cancelSliderAnimator() { | 
 |             if (sliderAnimator != null) { | 
 |                 ObjectAnimator oa = sliderAnimator.get(); | 
 |                 if (oa != null) { | 
 |                     oa.cancel(); | 
 |                     return true; | 
 |                 } | 
 |             } | 
 |             return false; | 
 |         } | 
 |     } | 
 |  | 
 |     @Override | 
 |     FrameLayout getFrameForChild() { | 
 |         StackFrame fl = new StackFrame(mContext); | 
 |         fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding); | 
 |         return fl; | 
 |     } | 
 |  | 
 |     /** | 
 |      * Apply any necessary tranforms for the child that is being added. | 
 |      */ | 
 |     void applyTransformForChildAtIndex(View child, int relativeIndex) { | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected void dispatchDraw(Canvas canvas) { | 
 |         boolean expandClipRegion = false; | 
 |  | 
 |         canvas.getClipBounds(stackInvalidateRect); | 
 |         final int childCount = getChildCount(); | 
 |         for (int i = 0; i < childCount; i++) { | 
 |             final View child =  getChildAt(i); | 
 |             LayoutParams lp = (LayoutParams) child.getLayoutParams(); | 
 |             if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) || | 
 |                     child.getAlpha() == 0f || child.getVisibility() != VISIBLE) { | 
 |                 lp.resetInvalidateRect(); | 
 |             } | 
 |             Rect childInvalidateRect = lp.getInvalidateRect(); | 
 |             if (!childInvalidateRect.isEmpty()) { | 
 |                 expandClipRegion = true; | 
 |                 stackInvalidateRect.union(childInvalidateRect); | 
 |             } | 
 |         } | 
 |  | 
 |         // We only expand the clip bounds if necessary. | 
 |         if (expandClipRegion) { | 
 |             canvas.save(); | 
 |             canvas.clipRectUnion(stackInvalidateRect); | 
 |             super.dispatchDraw(canvas); | 
 |             canvas.restore(); | 
 |         } else { | 
 |             super.dispatchDraw(canvas); | 
 |         } | 
 |     } | 
 |  | 
 |     private void onLayout() { | 
 |         if (!mFirstLayoutHappened) { | 
 |             mFirstLayoutHappened = true; | 
 |             updateChildTransforms(); | 
 |         } | 
 |  | 
 |         final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight()); | 
 |         if (mSlideAmount != newSlideAmount) { | 
 |             mSlideAmount = newSlideAmount; | 
 |             mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount); | 
 |         } | 
 |  | 
 |         if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 || | 
 |                 Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) { | 
 |  | 
 |             mPerspectiveShiftY = mNewPerspectiveShiftY; | 
 |             mPerspectiveShiftX = mNewPerspectiveShiftX; | 
 |             updateChildTransforms(); | 
 |         } | 
 |     } | 
 |  | 
 |     @Override | 
 |     public boolean onGenericMotionEvent(MotionEvent event) { | 
 |         if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { | 
 |             switch (event.getAction()) { | 
 |                 case MotionEvent.ACTION_SCROLL: { | 
 |                     final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); | 
 |                     if (vscroll < 0) { | 
 |                         pacedScroll(false); | 
 |                         return true; | 
 |                     } else if (vscroll > 0) { | 
 |                         pacedScroll(true); | 
 |                         return true; | 
 |                     } | 
 |                 } | 
 |             } | 
 |         } | 
 |         return super.onGenericMotionEvent(event); | 
 |     } | 
 |  | 
 |     // This ensures that the frequency of stack flips caused by scrolls is capped | 
 |     private void pacedScroll(boolean up) { | 
 |         long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime; | 
 |         if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) { | 
 |             if (up) { | 
 |                 showPrevious(); | 
 |             } else { | 
 |                 showNext(); | 
 |             } | 
 |             mLastScrollTime = System.currentTimeMillis(); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     @Override | 
 |     public boolean onInterceptTouchEvent(MotionEvent ev) { | 
 |         int action = ev.getAction(); | 
 |         switch(action & MotionEvent.ACTION_MASK) { | 
 |             case MotionEvent.ACTION_DOWN: { | 
 |                 if (mActivePointerId == INVALID_POINTER) { | 
 |                     mInitialX = ev.getX(); | 
 |                     mInitialY = ev.getY(); | 
 |                     mActivePointerId = ev.getPointerId(0); | 
 |                 } | 
 |                 break; | 
 |             } | 
 |             case MotionEvent.ACTION_MOVE: { | 
 |                 int pointerIndex = ev.findPointerIndex(mActivePointerId); | 
 |                 if (pointerIndex == INVALID_POINTER) { | 
 |                     // no data for our primary pointer, this shouldn't happen, log it | 
 |                     Log.d(TAG, "Error: No data for our primary pointer."); | 
 |                     return false; | 
 |                 } | 
 |                 float newY = ev.getY(pointerIndex); | 
 |                 float deltaY = newY - mInitialY; | 
 |  | 
 |                 beginGestureIfNeeded(deltaY); | 
 |                 break; | 
 |             } | 
 |             case MotionEvent.ACTION_POINTER_UP: { | 
 |                 onSecondaryPointerUp(ev); | 
 |                 break; | 
 |             } | 
 |             case MotionEvent.ACTION_UP: | 
 |             case MotionEvent.ACTION_CANCEL: { | 
 |                 mActivePointerId = INVALID_POINTER; | 
 |                 mSwipeGestureType = GESTURE_NONE; | 
 |             } | 
 |         } | 
 |  | 
 |         return mSwipeGestureType != GESTURE_NONE; | 
 |     } | 
 |  | 
 |     private void beginGestureIfNeeded(float deltaY) { | 
 |         if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) { | 
 |             final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN; | 
 |             cancelLongPress(); | 
 |             requestDisallowInterceptTouchEvent(true); | 
 |  | 
 |             if (mAdapter == null) return; | 
 |             final int adapterCount = getCount(); | 
 |  | 
 |             int activeIndex; | 
 |             if (mStackMode == ITEMS_SLIDE_UP) { | 
 |                 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1; | 
 |             } else { | 
 |                 activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0; | 
 |             } | 
 |  | 
 |             boolean endOfStack = mLoopViews && adapterCount == 1 | 
 |                     && ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP) | 
 |                     || (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN)); | 
 |             boolean beginningOfStack = mLoopViews && adapterCount == 1 | 
 |                     && ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP) | 
 |                     || (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN)); | 
 |  | 
 |             int stackMode; | 
 |             if (mLoopViews && !beginningOfStack && !endOfStack) { | 
 |                 stackMode = StackSlider.NORMAL_MODE; | 
 |             } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) { | 
 |                 activeIndex++; | 
 |                 stackMode = StackSlider.BEGINNING_OF_STACK_MODE; | 
 |             } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) { | 
 |                 stackMode = StackSlider.END_OF_STACK_MODE; | 
 |             } else { | 
 |                 stackMode = StackSlider.NORMAL_MODE; | 
 |             } | 
 |  | 
 |             mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE; | 
 |  | 
 |             View v = getViewAtRelativeIndex(activeIndex); | 
 |             if (v == null) return; | 
 |  | 
 |             setupStackSlider(v, stackMode); | 
 |  | 
 |             // We only register this gesture if we've made it this far without a problem | 
 |             mSwipeGestureType = swipeGestureType; | 
 |             cancelHandleClick(); | 
 |         } | 
 |     } | 
 |  | 
 |     /** | 
 |      * {@inheritDoc} | 
 |      */ | 
 |     @Override | 
 |     public boolean onTouchEvent(MotionEvent ev) { | 
 |         super.onTouchEvent(ev); | 
 |  | 
 |         int action = ev.getAction(); | 
 |         int pointerIndex = ev.findPointerIndex(mActivePointerId); | 
 |         if (pointerIndex == INVALID_POINTER) { | 
 |             // no data for our primary pointer, this shouldn't happen, log it | 
 |             Log.d(TAG, "Error: No data for our primary pointer."); | 
 |             return false; | 
 |         } | 
 |  | 
 |         float newY = ev.getY(pointerIndex); | 
 |         float newX = ev.getX(pointerIndex); | 
 |         float deltaY = newY - mInitialY; | 
 |         float deltaX = newX - mInitialX; | 
 |         if (mVelocityTracker == null) { | 
 |             mVelocityTracker = VelocityTracker.obtain(); | 
 |         } | 
 |         mVelocityTracker.addMovement(ev); | 
 |  | 
 |         switch (action & MotionEvent.ACTION_MASK) { | 
 |             case MotionEvent.ACTION_MOVE: { | 
 |                 beginGestureIfNeeded(deltaY); | 
 |  | 
 |                 float rx = deltaX / (mSlideAmount * 1.0f); | 
 |                 if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { | 
 |                     float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f; | 
 |                     if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; | 
 |                     mStackSlider.setYProgress(1 - r); | 
 |                     mStackSlider.setXProgress(rx); | 
 |                     return true; | 
 |                 } else if (mSwipeGestureType == GESTURE_SLIDE_UP) { | 
 |                     float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f; | 
 |                     if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r; | 
 |                     mStackSlider.setYProgress(r); | 
 |                     mStackSlider.setXProgress(rx); | 
 |                     return true; | 
 |                 } | 
 |                 break; | 
 |             } | 
 |             case MotionEvent.ACTION_UP: { | 
 |                 handlePointerUp(ev); | 
 |                 break; | 
 |             } | 
 |             case MotionEvent.ACTION_POINTER_UP: { | 
 |                 onSecondaryPointerUp(ev); | 
 |                 break; | 
 |             } | 
 |             case MotionEvent.ACTION_CANCEL: { | 
 |                 mActivePointerId = INVALID_POINTER; | 
 |                 mSwipeGestureType = GESTURE_NONE; | 
 |                 break; | 
 |             } | 
 |         } | 
 |         return true; | 
 |     } | 
 |  | 
 |     private void onSecondaryPointerUp(MotionEvent ev) { | 
 |         final int activePointerIndex = ev.getActionIndex(); | 
 |         final int pointerId = ev.getPointerId(activePointerIndex); | 
 |         if (pointerId == mActivePointerId) { | 
 |  | 
 |             int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1; | 
 |  | 
 |             View v = getViewAtRelativeIndex(activeViewIndex); | 
 |             if (v == null) return; | 
 |  | 
 |             // Our primary pointer has gone up -- let's see if we can find | 
 |             // another pointer on the view. If so, then we should replace | 
 |             // our primary pointer with this new pointer and adjust things | 
 |             // so that the view doesn't jump | 
 |             for (int index = 0; index < ev.getPointerCount(); index++) { | 
 |                 if (index != activePointerIndex) { | 
 |  | 
 |                     float x = ev.getX(index); | 
 |                     float y = ev.getY(index); | 
 |  | 
 |                     mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom()); | 
 |                     if (mTouchRect.contains(Math.round(x), Math.round(y))) { | 
 |                         float oldX = ev.getX(activePointerIndex); | 
 |                         float oldY = ev.getY(activePointerIndex); | 
 |  | 
 |                         // adjust our frame of reference to avoid a jump | 
 |                         mInitialY += (y - oldY); | 
 |                         mInitialX += (x - oldX); | 
 |  | 
 |                         mActivePointerId = ev.getPointerId(index); | 
 |                         if (mVelocityTracker != null) { | 
 |                             mVelocityTracker.clear(); | 
 |                         } | 
 |                         // ok, we're good, we found a new pointer which is touching the active view | 
 |                         return; | 
 |                     } | 
 |                 } | 
 |             } | 
 |             // if we made it this far, it means we didn't find a satisfactory new pointer :(, | 
 |             // so end the gesture | 
 |             handlePointerUp(ev); | 
 |         } | 
 |     } | 
 |  | 
 |     private void handlePointerUp(MotionEvent ev) { | 
 |         int pointerIndex = ev.findPointerIndex(mActivePointerId); | 
 |         float newY = ev.getY(pointerIndex); | 
 |         int deltaY = (int) (newY - mInitialY); | 
 |         mLastInteractionTime = System.currentTimeMillis(); | 
 |  | 
 |         if (mVelocityTracker != null) { | 
 |             mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); | 
 |             mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); | 
 |         } | 
 |  | 
 |         if (mVelocityTracker != null) { | 
 |             mVelocityTracker.recycle(); | 
 |             mVelocityTracker = null; | 
 |         } | 
 |  | 
 |         if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN | 
 |                 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { | 
 |             // We reset the gesture variable, because otherwise we will ignore showPrevious() / | 
 |             // showNext(); | 
 |             mSwipeGestureType = GESTURE_NONE; | 
 |  | 
 |             // Swipe threshold exceeded, swipe down | 
 |             if (mStackMode == ITEMS_SLIDE_UP) { | 
 |                 showPrevious(); | 
 |             } else { | 
 |                 showNext(); | 
 |             } | 
 |             mHighlight.bringToFront(); | 
 |         } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP | 
 |                 && mStackSlider.mMode == StackSlider.NORMAL_MODE) { | 
 |             // We reset the gesture variable, because otherwise we will ignore showPrevious() / | 
 |             // showNext(); | 
 |             mSwipeGestureType = GESTURE_NONE; | 
 |  | 
 |             // Swipe threshold exceeded, swipe up | 
 |             if (mStackMode == ITEMS_SLIDE_UP) { | 
 |                 showNext(); | 
 |             } else { | 
 |                 showPrevious(); | 
 |             } | 
 |  | 
 |             mHighlight.bringToFront(); | 
 |         } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) { | 
 |             // Didn't swipe up far enough, snap back down | 
 |             int duration; | 
 |             float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0; | 
 |             if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) { | 
 |                 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); | 
 |             } else { | 
 |                 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); | 
 |             } | 
 |  | 
 |             StackSlider animationSlider = new StackSlider(mStackSlider); | 
 |             PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress); | 
 |             PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); | 
 |             ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, | 
 |                     snapBackX, snapBackY); | 
 |             pa.setDuration(duration); | 
 |             pa.setInterpolator(new LinearInterpolator()); | 
 |             pa.start(); | 
 |         } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) { | 
 |             // Didn't swipe down far enough, snap back up | 
 |             float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1; | 
 |             int duration; | 
 |             if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) { | 
 |                 duration = Math.round(mStackSlider.getDurationForNeutralPosition()); | 
 |             } else { | 
 |                 duration = Math.round(mStackSlider.getDurationForOffscreenPosition()); | 
 |             } | 
 |  | 
 |             StackSlider animationSlider = new StackSlider(mStackSlider); | 
 |             PropertyValuesHolder snapBackY = | 
 |                     PropertyValuesHolder.ofFloat("YProgress",finalYProgress); | 
 |             PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f); | 
 |             ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider, | 
 |                     snapBackX, snapBackY); | 
 |             pa.setDuration(duration); | 
 |             pa.start(); | 
 |         } | 
 |  | 
 |         mActivePointerId = INVALID_POINTER; | 
 |         mSwipeGestureType = GESTURE_NONE; | 
 |     } | 
 |  | 
 |     private class StackSlider { | 
 |         View mView; | 
 |         float mYProgress; | 
 |         float mXProgress; | 
 |  | 
 |         static final int NORMAL_MODE = 0; | 
 |         static final int BEGINNING_OF_STACK_MODE = 1; | 
 |         static final int END_OF_STACK_MODE = 2; | 
 |  | 
 |         int mMode = NORMAL_MODE; | 
 |  | 
 |         public StackSlider() { | 
 |         } | 
 |  | 
 |         public StackSlider(StackSlider copy) { | 
 |             mView = copy.mView; | 
 |             mYProgress = copy.mYProgress; | 
 |             mXProgress = copy.mXProgress; | 
 |             mMode = copy.mMode; | 
 |         } | 
 |  | 
 |         private float cubic(float r) { | 
 |             return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f; | 
 |         } | 
 |  | 
 |         private float highlightAlphaInterpolator(float r) { | 
 |             float pivot = 0.4f; | 
 |             if (r < pivot) { | 
 |                 return 0.85f * cubic(r / pivot); | 
 |             } else { | 
 |                 return 0.85f * cubic(1 - (r - pivot) / (1 - pivot)); | 
 |             } | 
 |         } | 
 |  | 
 |         private float viewAlphaInterpolator(float r) { | 
 |             float pivot = 0.3f; | 
 |             if (r > pivot) { | 
 |                 return (r - pivot) / (1 - pivot); | 
 |             } else { | 
 |                 return 0; | 
 |             } | 
 |         } | 
 |  | 
 |         private float rotationInterpolator(float r) { | 
 |             float pivot = 0.2f; | 
 |             if (r < pivot) { | 
 |                 return 0; | 
 |             } else { | 
 |                 return (r - pivot) / (1 - pivot); | 
 |             } | 
 |         } | 
 |  | 
 |         void setView(View v) { | 
 |             mView = v; | 
 |         } | 
 |  | 
 |         public void setYProgress(float r) { | 
 |             // enforce r between 0 and 1 | 
 |             r = Math.min(1.0f, r); | 
 |             r = Math.max(0, r); | 
 |  | 
 |             mYProgress = r; | 
 |             if (mView == null) return; | 
 |  | 
 |             final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); | 
 |             final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); | 
 |  | 
 |             int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1; | 
 |  | 
 |             // We need to prevent any clipping issues which may arise by setting a layer type. | 
 |             // This doesn't come for free however, so we only want to enable it when required. | 
 |             if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) { | 
 |                 if (mView.getLayerType() == LAYER_TYPE_NONE) { | 
 |                     mView.setLayerType(LAYER_TYPE_HARDWARE, null); | 
 |                 } | 
 |             } else { | 
 |                 if (mView.getLayerType() != LAYER_TYPE_NONE) { | 
 |                     mView.setLayerType(LAYER_TYPE_NONE, null); | 
 |                 } | 
 |             } | 
 |  | 
 |             switch (mMode) { | 
 |                 case NORMAL_MODE: | 
 |                     viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); | 
 |                     highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount)); | 
 |                     mHighlight.setAlpha(highlightAlphaInterpolator(r)); | 
 |  | 
 |                     float alpha = viewAlphaInterpolator(1 - r); | 
 |  | 
 |                     // We make sure that views which can't be seen (have 0 alpha) are also invisible | 
 |                     // so that they don't interfere with click events. | 
 |                     if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) { | 
 |                         mView.setVisibility(VISIBLE); | 
 |                     } else if (alpha == 0 && mView.getAlpha() != 0 | 
 |                             && mView.getVisibility() == VISIBLE) { | 
 |                         mView.setVisibility(INVISIBLE); | 
 |                     } | 
 |  | 
 |                     mView.setAlpha(alpha); | 
 |                     mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); | 
 |                     mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r)); | 
 |                     break; | 
 |                 case END_OF_STACK_MODE: | 
 |                     r = r * 0.2f; | 
 |                     viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); | 
 |                     highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount)); | 
 |                     mHighlight.setAlpha(highlightAlphaInterpolator(r)); | 
 |                     break; | 
 |                 case BEGINNING_OF_STACK_MODE: | 
 |                     r = (1-r) * 0.2f; | 
 |                     viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); | 
 |                     highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount)); | 
 |                     mHighlight.setAlpha(highlightAlphaInterpolator(r)); | 
 |                     break; | 
 |             } | 
 |         } | 
 |  | 
 |         public void setXProgress(float r) { | 
 |             // enforce r between 0 and 1 | 
 |             r = Math.min(2.0f, r); | 
 |             r = Math.max(-2.0f, r); | 
 |  | 
 |             mXProgress = r; | 
 |  | 
 |             if (mView == null) return; | 
 |             final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); | 
 |             final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams(); | 
 |  | 
 |             r *= 0.2f; | 
 |             viewLp.setHorizontalOffset(Math.round(r * mSlideAmount)); | 
 |             highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount)); | 
 |         } | 
 |  | 
 |         void setMode(int mode) { | 
 |             mMode = mode; | 
 |         } | 
 |  | 
 |         float getDurationForNeutralPosition() { | 
 |             return getDuration(false, 0); | 
 |         } | 
 |  | 
 |         float getDurationForOffscreenPosition() { | 
 |             return getDuration(true, 0); | 
 |         } | 
 |  | 
 |         float getDurationForNeutralPosition(float velocity) { | 
 |             return getDuration(false, velocity); | 
 |         } | 
 |  | 
 |         float getDurationForOffscreenPosition(float velocity) { | 
 |             return getDuration(true, velocity); | 
 |         } | 
 |  | 
 |         private float getDuration(boolean invert, float velocity) { | 
 |             if (mView != null) { | 
 |                 final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams(); | 
 |  | 
 |                 float d = (float) Math.hypot(viewLp.horizontalOffset, viewLp.verticalOffset); | 
 |                 float maxd = (float) Math.hypot(mSlideAmount, 0.4f * mSlideAmount); | 
 |                 if (d > maxd) { | 
 |                     // Because mSlideAmount is updated in onLayout(), it is possible that d > maxd | 
 |                     // if we get onLayout() right before this method is called. | 
 |                     d = maxd; | 
 |                 } | 
 |  | 
 |                 if (velocity == 0) { | 
 |                     return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION; | 
 |                 } else { | 
 |                     float duration = invert ? d / Math.abs(velocity) : | 
 |                             (maxd - d) / Math.abs(velocity); | 
 |                     if (duration < MINIMUM_ANIMATION_DURATION || | 
 |                             duration > DEFAULT_ANIMATION_DURATION) { | 
 |                         return getDuration(invert, 0); | 
 |                     } else { | 
 |                         return duration; | 
 |                     } | 
 |                 } | 
 |             } | 
 |             return 0; | 
 |         } | 
 |  | 
 |         // Used for animations | 
 |         @SuppressWarnings({"UnusedDeclaration"}) | 
 |         public float getYProgress() { | 
 |             return mYProgress; | 
 |         } | 
 |  | 
 |         // Used for animations | 
 |         @SuppressWarnings({"UnusedDeclaration"}) | 
 |         public float getXProgress() { | 
 |             return mXProgress; | 
 |         } | 
 |     } | 
 |  | 
 |     LayoutParams createOrReuseLayoutParams(View v) { | 
 |         final ViewGroup.LayoutParams currentLp = v.getLayoutParams(); | 
 |         if (currentLp instanceof LayoutParams) { | 
 |             LayoutParams lp = (LayoutParams) currentLp; | 
 |             lp.setHorizontalOffset(0); | 
 |             lp.setVerticalOffset(0); | 
 |             lp.width = 0; | 
 |             lp.width = 0; | 
 |             return lp; | 
 |         } | 
 |         return new LayoutParams(v); | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected void onLayout(boolean changed, int left, int top, int right, int bottom) { | 
 |         checkForAndHandleDataChanged(); | 
 |  | 
 |         final int childCount = getChildCount(); | 
 |         for (int i = 0; i < childCount; i++) { | 
 |             final View child = getChildAt(i); | 
 |  | 
 |             int childRight = mPaddingLeft + child.getMeasuredWidth(); | 
 |             int childBottom = mPaddingTop + child.getMeasuredHeight(); | 
 |             LayoutParams lp = (LayoutParams) child.getLayoutParams(); | 
 |  | 
 |             child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset, | 
 |                     childRight + lp.horizontalOffset, childBottom + lp.verticalOffset); | 
 |  | 
 |         } | 
 |         onLayout(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public void advance() { | 
 |         long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime; | 
 |  | 
 |         if (mAdapter == null) return; | 
 |         final int adapterCount = getCount(); | 
 |         if (adapterCount == 1 && mLoopViews) return; | 
 |  | 
 |         if (mSwipeGestureType == GESTURE_NONE && | 
 |                 timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) { | 
 |             showNext(); | 
 |         } | 
 |     } | 
 |  | 
 |     private void measureChildren() { | 
 |         final int count = getChildCount(); | 
 |  | 
 |         final int measuredWidth = getMeasuredWidth(); | 
 |         final int measuredHeight = getMeasuredHeight(); | 
 |  | 
 |         final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X)) | 
 |                 - mPaddingLeft - mPaddingRight; | 
 |         final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y)) | 
 |                 - mPaddingTop - mPaddingBottom; | 
 |  | 
 |         int maxWidth = 0; | 
 |         int maxHeight = 0; | 
 |  | 
 |         for (int i = 0; i < count; i++) { | 
 |             final View child = getChildAt(i); | 
 |             child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), | 
 |                     MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST)); | 
 |  | 
 |             if (child != mHighlight && child != mClickFeedback) { | 
 |                 final int childMeasuredWidth = child.getMeasuredWidth(); | 
 |                 final int childMeasuredHeight = child.getMeasuredHeight(); | 
 |                 if (childMeasuredWidth > maxWidth) { | 
 |                     maxWidth = childMeasuredWidth; | 
 |                 } | 
 |                 if (childMeasuredHeight > maxHeight) { | 
 |                     maxHeight = childMeasuredHeight; | 
 |                 } | 
 |             } | 
 |         } | 
 |  | 
 |         mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth; | 
 |         mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight; | 
 |  | 
 |         // If we have extra space, we try and spread the items out | 
 |         if (maxWidth > 0 && count > 0 && maxWidth < childWidth) { | 
 |             mNewPerspectiveShiftX = measuredWidth - maxWidth; | 
 |         } | 
 |  | 
 |         if (maxHeight > 0 && count > 0 && maxHeight < childHeight) { | 
 |             mNewPerspectiveShiftY = measuredHeight - maxHeight; | 
 |         } | 
 |     } | 
 |  | 
 |     @Override | 
 |     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { | 
 |         int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); | 
 |         int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); | 
 |         final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); | 
 |         final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); | 
 |  | 
 |         boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1); | 
 |  | 
 |         // We need to deal with the case where our parent hasn't told us how | 
 |         // big we should be. In this case we should | 
 |         float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y); | 
 |         if (heightSpecMode == MeasureSpec.UNSPECIFIED) { | 
 |             heightSpecSize = haveChildRefSize ? | 
 |                     Math.round(mReferenceChildHeight * (1 + factorY)) + | 
 |                     mPaddingTop + mPaddingBottom : 0; | 
 |         } else if (heightSpecMode == MeasureSpec.AT_MOST) { | 
 |             if (haveChildRefSize) { | 
 |                 int height = Math.round(mReferenceChildHeight * (1 + factorY)) | 
 |                         + mPaddingTop + mPaddingBottom; | 
 |                 if (height <= heightSpecSize) { | 
 |                     heightSpecSize = height; | 
 |                 } else { | 
 |                     heightSpecSize |= MEASURED_STATE_TOO_SMALL; | 
 |  | 
 |                 } | 
 |             } else { | 
 |                 heightSpecSize = 0; | 
 |             } | 
 |         } | 
 |  | 
 |         float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X); | 
 |         if (widthSpecMode == MeasureSpec.UNSPECIFIED) { | 
 |             widthSpecSize = haveChildRefSize ? | 
 |                     Math.round(mReferenceChildWidth * (1 + factorX)) + | 
 |                     mPaddingLeft + mPaddingRight : 0; | 
 |         } else if (heightSpecMode == MeasureSpec.AT_MOST) { | 
 |             if (haveChildRefSize) { | 
 |                 int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight; | 
 |                 if (width <= widthSpecSize) { | 
 |                     widthSpecSize = width; | 
 |                 } else { | 
 |                     widthSpecSize |= MEASURED_STATE_TOO_SMALL; | 
 |                 } | 
 |             } else { | 
 |                 widthSpecSize = 0; | 
 |             } | 
 |         } | 
 |         setMeasuredDimension(widthSpecSize, heightSpecSize); | 
 |         measureChildren(); | 
 |     } | 
 |  | 
 |     @Override | 
 |     public CharSequence getAccessibilityClassName() { | 
 |         return StackView.class.getName(); | 
 |     } | 
 |  | 
 |     /** @hide */ | 
 |     @Override | 
 |     public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { | 
 |         super.onInitializeAccessibilityNodeInfoInternal(info); | 
 |         info.setScrollable(getChildCount() > 1); | 
 |         if (isEnabled()) { | 
 |             if (getDisplayedChild() < getChildCount() - 1) { | 
 |                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); | 
 |                 if (mStackMode == ITEMS_SLIDE_UP) { | 
 |                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_DOWN); | 
 |                 } else { | 
 |                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_UP); | 
 |                 } | 
 |             } | 
 |             if (getDisplayedChild() > 0) { | 
 |                 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); | 
 |                 if (mStackMode == ITEMS_SLIDE_UP) { | 
 |                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_UP); | 
 |                 } else { | 
 |                     info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_PAGE_DOWN); | 
 |                 } | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     private boolean goForward() { | 
 |         if (getDisplayedChild() < getChildCount() - 1) { | 
 |             showNext(); | 
 |             return true; | 
 |         } | 
 |         return false; | 
 |     } | 
 |  | 
 |     private boolean goBackward() { | 
 |         if (getDisplayedChild() > 0) { | 
 |             showPrevious(); | 
 |             return true; | 
 |         } | 
 |         return false; | 
 |     } | 
 |  | 
 |     /** @hide */ | 
 |     @Override | 
 |     public boolean performAccessibilityActionInternal(int action, Bundle arguments) { | 
 |         if (super.performAccessibilityActionInternal(action, arguments)) { | 
 |             return true; | 
 |         } | 
 |         if (!isEnabled()) { | 
 |             return false; | 
 |         } | 
 |         switch (action) { | 
 |             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { | 
 |                 return goForward(); | 
 |             } | 
 |             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { | 
 |                 return goBackward(); | 
 |             } | 
 |             case R.id.accessibilityActionPageUp: { | 
 |                 if (mStackMode == ITEMS_SLIDE_UP) { | 
 |                     return goBackward(); | 
 |                 } else { | 
 |                     return goForward(); | 
 |                 } | 
 |             } | 
 |             case R.id.accessibilityActionPageDown: { | 
 |                 if (mStackMode == ITEMS_SLIDE_UP) { | 
 |                     return goForward(); | 
 |                 } else { | 
 |                     return goBackward(); | 
 |                 } | 
 |             } | 
 |         } | 
 |         return false; | 
 |     } | 
 |  | 
 |     class LayoutParams extends ViewGroup.LayoutParams { | 
 |         int horizontalOffset; | 
 |         int verticalOffset; | 
 |         View mView; | 
 |         private final Rect parentRect = new Rect(); | 
 |         private final Rect invalidateRect = new Rect(); | 
 |         private final RectF invalidateRectf = new RectF(); | 
 |         private final Rect globalInvalidateRect = new Rect(); | 
 |  | 
 |         LayoutParams(View view) { | 
 |             super(0, 0); | 
 |             width = 0; | 
 |             height = 0; | 
 |             horizontalOffset = 0; | 
 |             verticalOffset = 0; | 
 |             mView = view; | 
 |         } | 
 |  | 
 |         LayoutParams(Context c, AttributeSet attrs) { | 
 |             super(c, attrs); | 
 |             horizontalOffset = 0; | 
 |             verticalOffset = 0; | 
 |             width = 0; | 
 |             height = 0; | 
 |         } | 
 |  | 
 |         void invalidateGlobalRegion(View v, Rect r) { | 
 |             // We need to make a new rect here, so as not to modify the one passed | 
 |             globalInvalidateRect.set(r); | 
 |             globalInvalidateRect.union(0, 0, getWidth(), getHeight()); | 
 |             View p = v; | 
 |             if (!(v.getParent() != null && v.getParent() instanceof View)) return; | 
 |  | 
 |             boolean firstPass = true; | 
 |             parentRect.set(0, 0, 0, 0); | 
 |             while (p.getParent() != null && p.getParent() instanceof View | 
 |                     && !parentRect.contains(globalInvalidateRect)) { | 
 |                 if (!firstPass) { | 
 |                     globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop() | 
 |                             - p.getScrollY()); | 
 |                 } | 
 |                 firstPass = false; | 
 |                 p = (View) p.getParent(); | 
 |                 parentRect.set(p.getScrollX(), p.getScrollY(), | 
 |                         p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY()); | 
 |                 p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, | 
 |                         globalInvalidateRect.right, globalInvalidateRect.bottom); | 
 |             } | 
 |  | 
 |             p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top, | 
 |                     globalInvalidateRect.right, globalInvalidateRect.bottom); | 
 |         } | 
 |  | 
 |         Rect getInvalidateRect() { | 
 |             return invalidateRect; | 
 |         } | 
 |  | 
 |         void resetInvalidateRect() { | 
 |             invalidateRect.set(0, 0, 0, 0); | 
 |         } | 
 |  | 
 |         // This is public so that ObjectAnimator can access it | 
 |         public void setVerticalOffset(int newVerticalOffset) { | 
 |             setOffsets(horizontalOffset, newVerticalOffset); | 
 |         } | 
 |  | 
 |         public void setHorizontalOffset(int newHorizontalOffset) { | 
 |             setOffsets(newHorizontalOffset, verticalOffset); | 
 |         } | 
 |  | 
 |         public void setOffsets(int newHorizontalOffset, int newVerticalOffset) { | 
 |             int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset; | 
 |             horizontalOffset = newHorizontalOffset; | 
 |             int verticalOffsetDelta = newVerticalOffset - verticalOffset; | 
 |             verticalOffset = newVerticalOffset; | 
 |  | 
 |             if (mView != null) { | 
 |                 mView.requestLayout(); | 
 |                 int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft()); | 
 |                 int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight()); | 
 |                 int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop()); | 
 |                 int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom()); | 
 |  | 
 |                 invalidateRectf.set(left, top, right, bottom); | 
 |  | 
 |                 float xoffset = -invalidateRectf.left; | 
 |                 float yoffset = -invalidateRectf.top; | 
 |                 invalidateRectf.offset(xoffset, yoffset); | 
 |                 mView.getMatrix().mapRect(invalidateRectf); | 
 |                 invalidateRectf.offset(-xoffset, -yoffset); | 
 |  | 
 |                 invalidateRect.set((int) Math.floor(invalidateRectf.left), | 
 |                         (int) Math.floor(invalidateRectf.top), | 
 |                         (int) Math.ceil(invalidateRectf.right), | 
 |                         (int) Math.ceil(invalidateRectf.bottom)); | 
 |  | 
 |                 invalidateGlobalRegion(mView, invalidateRect); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     private static class HolographicHelper { | 
 |         private final Paint mHolographicPaint = new Paint(); | 
 |         private final Paint mErasePaint = new Paint(); | 
 |         private final Paint mBlurPaint = new Paint(); | 
 |         private static final int RES_OUT = 0; | 
 |         private static final int CLICK_FEEDBACK = 1; | 
 |         private float mDensity; | 
 |         private BlurMaskFilter mSmallBlurMaskFilter; | 
 |         private BlurMaskFilter mLargeBlurMaskFilter; | 
 |         private final Canvas mCanvas = new Canvas(); | 
 |         private final Canvas mMaskCanvas = new Canvas(); | 
 |         private final int[] mTmpXY = new int[2]; | 
 |         private final Matrix mIdentityMatrix = new Matrix(); | 
 |  | 
 |         HolographicHelper(Context context) { | 
 |             mDensity = context.getResources().getDisplayMetrics().density; | 
 |  | 
 |             mHolographicPaint.setFilterBitmap(true); | 
 |             mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30)); | 
 |             mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); | 
 |             mErasePaint.setFilterBitmap(true); | 
 |  | 
 |             mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL); | 
 |             mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL); | 
 |         } | 
 |  | 
 |         Bitmap createClickOutline(View v, int color) { | 
 |             return createOutline(v, CLICK_FEEDBACK, color); | 
 |         } | 
 |  | 
 |         Bitmap createResOutline(View v, int color) { | 
 |             return createOutline(v, RES_OUT, color); | 
 |         } | 
 |  | 
 |         Bitmap createOutline(View v, int type, int color) { | 
 |             mHolographicPaint.setColor(color); | 
 |             if (type == RES_OUT) { | 
 |                 mBlurPaint.setMaskFilter(mSmallBlurMaskFilter); | 
 |             } else if (type == CLICK_FEEDBACK) { | 
 |                 mBlurPaint.setMaskFilter(mLargeBlurMaskFilter); | 
 |             } | 
 |  | 
 |             if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) { | 
 |                 return null; | 
 |             } | 
 |  | 
 |             Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(), | 
 |                     v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888); | 
 |             mCanvas.setBitmap(bitmap); | 
 |  | 
 |             float rotationX = v.getRotationX(); | 
 |             float rotation = v.getRotation(); | 
 |             float translationY = v.getTranslationY(); | 
 |             float translationX = v.getTranslationX(); | 
 |             v.setRotationX(0); | 
 |             v.setRotation(0); | 
 |             v.setTranslationY(0); | 
 |             v.setTranslationX(0); | 
 |             v.draw(mCanvas); | 
 |             v.setRotationX(rotationX); | 
 |             v.setRotation(rotation); | 
 |             v.setTranslationY(translationY); | 
 |             v.setTranslationX(translationX); | 
 |  | 
 |             drawOutline(mCanvas, bitmap); | 
 |             mCanvas.setBitmap(null); | 
 |             return bitmap; | 
 |         } | 
 |  | 
 |         void drawOutline(Canvas dest, Bitmap src) { | 
 |             final int[] xy = mTmpXY; | 
 |             Bitmap mask = src.extractAlpha(mBlurPaint, xy); | 
 |             mMaskCanvas.setBitmap(mask); | 
 |             mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint); | 
 |             dest.drawColor(0, PorterDuff.Mode.CLEAR); | 
 |             dest.setMatrix(mIdentityMatrix); | 
 |             dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint); | 
 |             mMaskCanvas.setBitmap(null); | 
 |             mask.recycle(); | 
 |         } | 
 |     } | 
 | } |