content: /* 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 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.ACTION_SCROLL_FORWARD);
              }
              if (getDisplayedChild() > 0) {
                  info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
              }
          }
      }
  
      /** @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: {
                  if (getDisplayedChild() < getChildCount() - 1) {
                      showNext();
                      return true;
                  }
              } return false;
              case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
                  if (getDisplayedChild() > 0) {
                      showPrevious();
                      return true;
                  }
              } return false;
          }
          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();
          }
      }
  }
  