StackView.java revision 5b53f9186e7812c93bc578d18e92cb123481fcbc
144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen/*
244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * Copyright (C) 2010 The Android Open Source Project
344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen *
444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * Licensed under the Apache License, Version 2.0 (the "License");
544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * you may not use this file except in compliance with the License.
644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * You may obtain a copy of the License at
744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen *
844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen *      http://www.apache.org/licenses/LICENSE-2.0
944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen *
1044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * Unless required by applicable law or agreed to in writing, software
1144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * distributed under the License is distributed on an "AS IS" BASIS,
1244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * See the License for the specific language governing permissions and
1444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * limitations under the License.
1544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen */
1644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
1744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenpackage android.widget;
1844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
1944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport java.util.WeakHashMap;
2044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
2144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.animation.PropertyAnimator;
2244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.content.Context;
2332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohenimport android.graphics.Bitmap;
2432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohenimport android.graphics.Canvas;
2532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohenimport android.graphics.Matrix;
2632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohenimport android.graphics.Paint;
2732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohenimport android.graphics.PorterDuff;
2832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohenimport android.graphics.PorterDuffXfermode;
2944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.graphics.Rect;
3044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.util.AttributeSet;
3144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.util.Log;
3244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.view.MotionEvent;
3344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.view.VelocityTracker;
3444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.view.View;
3544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.view.ViewConfiguration;
3644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.view.ViewGroup;
3744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenimport android.widget.RemoteViews.RemoteView;
3844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
3944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen@RemoteView
4044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen/**
4144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * A view that displays its children in a stack and allows users to discretely swipe
4244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen * through the children.
4344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen */
4444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohenpublic class StackView extends AdapterViewAnimator {
4544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private final String TAG = "StackView";
4644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
4744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    /**
4844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * Default animation parameters
4944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     */
505b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    private static final int DEFAULT_ANIMATION_DURATION = 400;
515b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    private static final int MINIMUM_ANIMATION_DURATION = 50;
5244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
5344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    /**
5444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * These specify the different gesture states
5544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     */
565b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    private static final int GESTURE_NONE = 0;
575b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    private static final int GESTURE_SLIDE_UP = 1;
585b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    private static final int GESTURE_SLIDE_DOWN = 2;
5944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
6044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    /**
6144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * Specifies how far you need to swipe (up or down) before it
6244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * will be consider a completed gesture when you lift your finger
6344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     */
645b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    private static final float SWIPE_THRESHOLD_RATIO = 0.35f;
655b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    private static final float SLIDE_UP_RATIO = 0.7f;
6644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
6744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private final WeakHashMap<View, Float> mRotations = new WeakHashMap<View, Float>();
6844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private final WeakHashMap<View, Integer>
6944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            mChildrenToApplyTransformsTo = new WeakHashMap<View, Integer>();
7044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
7144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    /**
7244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * Sentinel value for no current active pointer.
7344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * Used by {@link #mActivePointerId}.
7444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     */
7544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private static final int INVALID_POINTER = -1;
7644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
7744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    /**
7844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * These variables are all related to the current state of touch interaction
7944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * with the stack
8044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     */
8144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private float mInitialY;
8244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private float mInitialX;
8344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private int mActivePointerId;
8444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private int mYVelocity = 0;
8544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private int mSwipeGestureType = GESTURE_NONE;
8644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private int mViewHeight;
8744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private int mSwipeThreshold;
8844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private int mTouchSlop;
8944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private int mMaximumVelocity;
9044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private VelocityTracker mVelocityTracker;
9144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
9232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private ImageView mHighlight;
9332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private StackSlider mStackSlider;
9444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private boolean mFirstLayoutHappened = false;
9544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
9644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    // TODO: temp hack to get this thing started
9744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    int mIndex = 5;
9844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
9944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    public StackView(Context context) {
10044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        super(context);
10144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        initStackView();
10244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
10344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
10444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    public StackView(Context context, AttributeSet attrs) {
10544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        super(context, attrs);
10644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        initStackView();
10744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
10844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
10944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private void initStackView() {
11044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        configureViewAnimator(4, 2);
11144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        setStaticTransformationsEnabled(true);
11244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
11344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mTouchSlop = configuration.getScaledTouchSlop();// + 5;
11444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
11544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mActivePointerId = INVALID_POINTER;
11632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
11732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        mHighlight = new ImageView(getContext());
11832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        mHighlight.setLayoutParams(new LayoutParams(mHighlight));
11932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
12032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        mStackSlider = new StackSlider();
12132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
12232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        if (!sPaintsInitialized) {
1235b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            initializePaints();
12432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
12544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
12644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
12744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    /**
12844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
12944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     */
13044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    void animateViewForTransition(int fromIndex, int toIndex, View view) {
13144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (fromIndex == -1 && toIndex == 0) {
13244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Fade item in
13344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            if (view.getAlpha() == 1) {
13444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                view.setAlpha(0);
13544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
13644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            PropertyAnimator fadeIn = new PropertyAnimator(DEFAULT_ANIMATION_DURATION,
13744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    view, "alpha", view.getAlpha(), 1.0f);
13844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            fadeIn.start();
13944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        } else if (fromIndex == mNumActiveViews - 1 && toIndex == mNumActiveViews - 2) {
14044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Slide item in
14144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            view.setVisibility(VISIBLE);
14232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
14344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            LayoutParams lp = (LayoutParams) view.getLayoutParams();
14444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
1455b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            int largestDuration = Math.round(
14644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    (lp.verticalOffset*1.0f/-mViewHeight)*DEFAULT_ANIMATION_DURATION);
14744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            int duration = largestDuration;
14844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            if (mYVelocity != 0) {
14944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                duration = 1000*(0 - lp.verticalOffset)/Math.abs(mYVelocity);
15044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
15144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
15244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            duration = Math.min(duration, largestDuration);
15344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            duration = Math.max(duration, MINIMUM_ANIMATION_DURATION);
15444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
15532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator slideInY = new PropertyAnimator(duration, mStackSlider,
15632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "YProgress", mStackSlider.getYProgress(), 0);
15732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            slideInY.start();
15832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator slideInX = new PropertyAnimator(duration, mStackSlider,
15932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "XProgress", mStackSlider.getXProgress(), 0);
16032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            slideInX.start();
16144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
16244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        } else if (fromIndex == mNumActiveViews - 2 && toIndex == mNumActiveViews - 1) {
16344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Slide item out
16444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            LayoutParams lp = (LayoutParams) view.getLayoutParams();
16544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
1665b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            int largestDuration = Math.round(mStackSlider.getYProgress()*DEFAULT_ANIMATION_DURATION);
16744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            int duration = largestDuration;
16844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            if (mYVelocity != 0) {
16944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                duration = 1000*(lp.verticalOffset + mViewHeight)/Math.abs(mYVelocity);
17044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
17144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
17244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            duration = Math.min(duration, largestDuration);
17344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            duration = Math.max(duration, MINIMUM_ANIMATION_DURATION);
17444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
17532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator slideOutY = new PropertyAnimator(duration, mStackSlider,
17632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "YProgress", mStackSlider.getYProgress(), 1);
17732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            slideOutY.start();
17832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator slideOutX = new PropertyAnimator(duration, mStackSlider,
17932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "XProgress", mStackSlider.getXProgress(), 0);
18032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            slideOutX.start();
18144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
18244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        } else if (fromIndex == -1 && toIndex == mNumActiveViews - 1) {
18344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Make sure this view that is "waiting in the wings" is invisible
18444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            view.setAlpha(0.0f);
18544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        } else if (toIndex == -1) {
18644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Fade item out
18744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            PropertyAnimator fadeOut = new PropertyAnimator(DEFAULT_ANIMATION_DURATION,
18844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    view, "alpha", view.getAlpha(), 0);
18944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            fadeOut.start();
19044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
19144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
19244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
19344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    /**
19444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     * Apply any necessary tranforms for the child that is being added.
19544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen     */
19644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    void applyTransformForChildAtIndex(View child, int relativeIndex) {
19744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (!mRotations.containsKey(child)) {
1985b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            float rotation = (float) (Math.random()*26 - 13);
19944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            mRotations.put(child, rotation);
20044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
20144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
20244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        // Child has been removed
20344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (relativeIndex == -1) {
20444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            if (mRotations.containsKey(child)) {
20544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                mRotations.remove(child);
20644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
20744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            if (mChildrenToApplyTransformsTo.containsKey(child)) {
20844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                mChildrenToApplyTransformsTo.remove(child);
20944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
21044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
21144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
21244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        // if this view is already in the layout, we need to
21344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        // wait until layout has finished in order to set the
21444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        // pivot point of the rotation (requiring getMeasuredWidth/Height())
21544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mChildrenToApplyTransformsTo.put(child, relativeIndex);
21644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
21744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
21844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    @Override
21944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
22044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        super.onLayout(changed, left, top, right, bottom);
22144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
22244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (!mChildrenToApplyTransformsTo.isEmpty()) {
22344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            for (View child: mChildrenToApplyTransformsTo.keySet()) {
22444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                if (mRotations.containsKey(child)) {
22544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    child.setPivotX(child.getMeasuredWidth()/2);
22644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    child.setPivotY(child.getMeasuredHeight()/2);
22744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    child.setRotation(mRotations.get(child));
22844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                }
22944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
23044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            mChildrenToApplyTransformsTo.clear();
23144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
23244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
23344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (!mFirstLayoutHappened) {
2345b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            mViewHeight = Math.round(SLIDE_UP_RATIO*getMeasuredHeight());
2355b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO*mViewHeight);
23644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
23744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // TODO: Right now this walks all the way up the view hierarchy and disables
23844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // ClipChildren and ClipToPadding. We're probably going  to want to reset
23944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // these flags as well.
24044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            setClipChildren(false);
24144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            ViewGroup view = this;
24244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            while (view.getParent() != null && view.getParent() instanceof ViewGroup) {
24344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                view = (ViewGroup) view.getParent();
24444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                view.setClipChildren(false);
24544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                view.setClipToPadding(false);
24644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
24744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            mFirstLayoutHappened = true;
24844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
24944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
25044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
25144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    @Override
25244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    public boolean onInterceptTouchEvent(MotionEvent ev) {
25344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        int action = ev.getAction();
25444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        switch(action & MotionEvent.ACTION_MASK) {
25544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
25644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_DOWN: {
25744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                if (mActivePointerId == INVALID_POINTER) {
25844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    mInitialX = ev.getX();
25944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    mInitialY = ev.getY();
26044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    mActivePointerId = ev.getPointerId(0);
26144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                }
26244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                break;
26344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
26444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_MOVE: {
26544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                int pointerIndex = ev.findPointerIndex(mActivePointerId);
26644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                if (pointerIndex == INVALID_POINTER) {
26744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    // no data for our primary pointer, this shouldn't happen, log it
26844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    Log.d(TAG, "Error: No data for our primary pointer.");
26944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    return false;
27044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                }
27144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                float newY = ev.getY(pointerIndex);
27244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                float deltaY = newY - mInitialY;
27344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
27432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                beginGestureIfNeeded(deltaY);
27544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                break;
27644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
27744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_POINTER_UP: {
27844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                onSecondaryPointerUp(ev);
27944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                break;
28044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
28144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_UP:
28244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_CANCEL: {
28344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                mActivePointerId = INVALID_POINTER;
28444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                mSwipeGestureType = GESTURE_NONE;
28544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
28644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
28744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
28844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        return mSwipeGestureType != GESTURE_NONE;
28944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
29044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
29132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private void beginGestureIfNeeded(float deltaY) {
29232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
29332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mSwipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
29432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            cancelLongPress();
29532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            requestDisallowInterceptTouchEvent(true);
29632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
29732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            int activeIndex = mSwipeGestureType == GESTURE_SLIDE_DOWN ? mNumActiveViews - 1
29832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    : mNumActiveViews - 2;
29932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
30032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            View v = getViewAtRelativeIndex(activeIndex);
30132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            if (v != null) {
30232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                mHighlight.setImageBitmap(createOutline(v));
30332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                mHighlight.bringToFront();
30432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                v.bringToFront();
30532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                mStackSlider.setView(v);
30632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                if (mSwipeGestureType == GESTURE_SLIDE_DOWN)
30732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    v.setVisibility(VISIBLE);
30832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            }
30932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
31032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    }
31132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
31244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    @Override
31344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    public boolean onTouchEvent(MotionEvent ev) {
31444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        int action = ev.getAction();
31544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        int pointerIndex = ev.findPointerIndex(mActivePointerId);
31644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (pointerIndex == INVALID_POINTER) {
31744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // no data for our primary pointer, this shouldn't happen, log it
31844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            Log.d(TAG, "Error: No data for our primary pointer.");
31944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            return false;
32044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
32144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
32244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        float newY = ev.getY(pointerIndex);
32332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float newX = ev.getX(pointerIndex);
32444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        float deltaY = newY - mInitialY;
32532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float deltaX = newX - mInitialX;
32644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (mVelocityTracker == null) {
32744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            mVelocityTracker = VelocityTracker.obtain();
32844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
32944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mVelocityTracker.addMovement(ev);
33044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
33144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        switch (action & MotionEvent.ACTION_MASK) {
33244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_MOVE: {
33332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                beginGestureIfNeeded(deltaY);
33432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
33532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                float rx = 0.3f*deltaX/(mViewHeight*1.0f);
33632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
33732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    float r = (deltaY-mTouchSlop*1.0f)/mViewHeight*1.0f;
33832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    mStackSlider.setYProgress(1 - r);
33932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    mStackSlider.setXProgress(rx);
34032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    return true;
34132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
34232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    float r = -(deltaY + mTouchSlop*1.0f)/mViewHeight*1.0f;
34332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    mStackSlider.setYProgress(r);
34432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    mStackSlider.setXProgress(rx);
34532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    return true;
34644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                }
34744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
34844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                break;
34944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
35044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_UP: {
35144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                handlePointerUp(ev);
35244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                break;
35344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
35444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_POINTER_UP: {
35544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                onSecondaryPointerUp(ev);
35644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                break;
35744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
35844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            case MotionEvent.ACTION_CANCEL: {
35944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                mActivePointerId = INVALID_POINTER;
36044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                mSwipeGestureType = GESTURE_NONE;
36144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                break;
36244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
36344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
36444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        return true;
36544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
36644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
36744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private final Rect touchRect = new Rect();
36844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private void onSecondaryPointerUp(MotionEvent ev) {
36944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        final int activePointerIndex = ev.getActionIndex();
37044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        final int pointerId = ev.getPointerId(activePointerIndex);
37144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (pointerId == mActivePointerId) {
37244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
37344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? mNumActiveViews - 1
37444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    : mNumActiveViews - 2;
37544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
37644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            View v = getViewAtRelativeIndex(activeViewIndex);
37744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            if (v == null) return;
37844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
37944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Our primary pointer has gone up -- let's see if we can find
38044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // another pointer on the view. If so, then we should replace
38144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // our primary pointer with this new pointer and adjust things
38244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // so that the view doesn't jump
38344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            for (int index = 0; index < ev.getPointerCount(); index++) {
38444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                if (index != activePointerIndex) {
38544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
38644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    float x = ev.getX(index);
38744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    float y = ev.getY(index);
38844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
38944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    touchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
3905b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy                    if (touchRect.contains(Math.round(x), Math.round(y))) {
39144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        float oldX = ev.getX(activePointerIndex);
39244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        float oldY = ev.getY(activePointerIndex);
39344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
39444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        // adjust our frame of reference to avoid a jump
39544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        mInitialY += (y - oldY);
39644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        mInitialX += (x - oldX);
39744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
39844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        mActivePointerId = ev.getPointerId(index);
39944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        if (mVelocityTracker != null) {
40044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                            mVelocityTracker.clear();
40144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        }
40244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        // ok, we're good, we found a new pointer which is touching the active view
40344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                        return;
40444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                    }
40544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen                }
40644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            }
40744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // if we made it this far, it means we didn't find a satisfactory new pointer :(,
40844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // so end the
40944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            handlePointerUp(ev);
41044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
41144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
41244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
41344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    private void handlePointerUp(MotionEvent ev) {
41444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        int pointerIndex = ev.findPointerIndex(mActivePointerId);
41544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        float newY = ev.getY(pointerIndex);
41644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        int deltaY = (int) (newY - mInitialY);
41744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
41844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
41944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
42044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
42144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        if (mVelocityTracker != null) {
42244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            mVelocityTracker.recycle();
42344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            mVelocityTracker = null;
42444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
42544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
42632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN) {
42744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Swipe threshold exceeded, swipe down
42844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            showNext();
42932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mHighlight.bringToFront();
43032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP) {
43144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Swipe threshold exceeded, swipe up
43244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            showPrevious();
43332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mHighlight.bringToFront();
43432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
43544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Didn't swipe up far enough, snap back down
4365b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            int duration = Math.round(mStackSlider.getYProgress()*DEFAULT_ANIMATION_DURATION);
43732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
43832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator snapBackY = new PropertyAnimator(duration, mStackSlider,
43932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "YProgress", mStackSlider.getYProgress(), 0);
44032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            snapBackY.start();
44132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator snapBackX = new PropertyAnimator(duration, mStackSlider,
44232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "XProgress", mStackSlider.getXProgress(), 0);
44332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            snapBackX.start();
44432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
44544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen            // Didn't swipe down far enough, snap back up
4465b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            int duration = Math.round((1 -
44732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    mStackSlider.getYProgress())*DEFAULT_ANIMATION_DURATION);
44832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator snapBackY = new PropertyAnimator(duration, mStackSlider,
44932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "YProgress", mStackSlider.getYProgress(), 1);
45032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            snapBackY.start();
45132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            PropertyAnimator snapBackX = new PropertyAnimator(duration, mStackSlider,
45232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                    "XProgress", mStackSlider.getXProgress(), 0);
45332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            snapBackX.start();
45444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        }
45544729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
45644729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mActivePointerId = INVALID_POINTER;
45744729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        mSwipeGestureType = GESTURE_NONE;
45832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    }
45932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
46032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private class StackSlider {
46132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        View mView;
46232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float mYProgress;
46332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float mXProgress;
46432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
46532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        private float cubic(float r) {
46632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            return (float) (Math.pow(2*r-1, 3) + 1)/2.0f;
46732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
46832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
46932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        private float highlightAlphaInterpolator(float r) {
47032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            float pivot = 0.4f;
47132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            if (r < pivot) {
47232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                return 0.85f*cubic(r/pivot);
47332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            } else {
47432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                return 0.85f*cubic(1 - (r-pivot)/(1-pivot));
47532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            }
47632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
47732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
47832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        private float viewAlphaInterpolator(float r) {
47932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            float pivot = 0.3f;
48032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            if (r > pivot) {
48132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                return (r - pivot)/(1 - pivot);
48232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            } else {
48332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                return 0;
48432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            }
48532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
48632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
48732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        void setView(View v) {
48832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mView = v;
48932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
49032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
49132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        public void setYProgress(float r) {
49232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            // enforce r between 0 and 1
49332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            r = Math.min(1.0f, r);
49432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            r = Math.max(0, r);
49532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
49632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mYProgress = r;
49732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
49832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
49932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
50032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
5015b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            viewLp.setVerticalOffset(Math.round(-r*mViewHeight));
5025b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            highlightLp.setVerticalOffset(Math.round(-r*mViewHeight));
50332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mHighlight.setAlpha(highlightAlphaInterpolator(r));
50432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mView.setAlpha(viewAlphaInterpolator(1-r));
50532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
50632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
50732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        public void setXProgress(float r) {
50832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            // enforce r between 0 and 1
50932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            r = Math.min(1.0f, r);
51032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            r = Math.max(-1.0f, r);
51132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
51232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            mXProgress = r;
51332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
51432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
51532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
51632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
5175b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            viewLp.setHorizontalOffset(Math.round(r*mViewHeight));
5185b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy            highlightLp.setHorizontalOffset(Math.round(r*mViewHeight));
51932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
52032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
52132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float getYProgress() {
52232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            return mYProgress;
52332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
52432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
52532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float getXProgress() {
52632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen            return mXProgress;
52732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        }
52844729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
52944729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen
53044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    @Override
53144729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    public void onRemoteAdapterConnected() {
53244729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        super.onRemoteAdapterConnected();
53344729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen        setDisplayedChild(mIndex);
53444729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen    }
53532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
53632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private static final Paint sHolographicPaint = new Paint();
53732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private static final Paint sErasePaint = new Paint();
53832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private static boolean sPaintsInitialized = false;
53932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    private static final float STROKE_WIDTH = 3.0f;
54032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
5415b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    static void initializePaints() {
54232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        sHolographicPaint.setColor(0xff6699ff);
54332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        sHolographicPaint.setFilterBitmap(true);
54432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        sErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
54532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        sErasePaint.setFilterBitmap(true);
54632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        sPaintsInitialized = true;
54732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    }
54832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
54932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    static Bitmap createOutline(View v) {
55032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
55132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                Bitmap.Config.ARGB_8888);
55232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        Canvas canvas = new Canvas(bitmap);
55332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
55432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        canvas.concat(v.getMatrix());
55532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        v.draw(canvas);
55632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
55732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        Bitmap outlineBitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
55832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen                Bitmap.Config.ARGB_8888);
55932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        Canvas outlineCanvas = new Canvas(outlineBitmap);
5605b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy        drawOutline(outlineCanvas, bitmap);
56132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        bitmap.recycle();
56232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        return outlineBitmap;
56332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    }
56432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
5655b53f9186e7812c93bc578d18e92cb123481fcbcRomain Guy    static void drawOutline(Canvas dest, Bitmap src) {
56632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        dest.drawColor(0, PorterDuff.Mode.CLEAR);
56732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
56832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        Bitmap mask = src.extractAlpha();
56932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        Matrix id = new Matrix();
57032a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
57132a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        Matrix m = new Matrix();
57232a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float xScale = STROKE_WIDTH*2/(src.getWidth());
57332a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        float yScale = STROKE_WIDTH*2/(src.getHeight());
57432a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        m.preScale(1+xScale, 1+yScale, src.getWidth()/2, src.getHeight()/2);
57532a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        dest.drawBitmap(mask, m, sHolographicPaint);
57632a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen
57732a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        dest.drawBitmap(src, id, sErasePaint);
57832a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen        mask.recycle();
57932a42f1587db77b958d62c3de4f2734eb0a3b965Adam Cohen    }
58044729e3d1c01265858eec566c7b7c676c46a7916Adam Cohen}
581