SwipeDismissLayout.java revision ca6234e084a71e0c968cff404620298bcd971fcc
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.widget;
18
19import android.animation.TimeInterpolator;
20import android.content.Context;
21import android.util.AttributeSet;
22import android.util.Log;
23import android.view.MotionEvent;
24import android.view.VelocityTracker;
25import android.view.View;
26import android.view.ViewConfiguration;
27import android.view.ViewGroup;
28import android.view.animation.AccelerateInterpolator;
29import android.view.animation.DecelerateInterpolator;
30import android.widget.FrameLayout;
31
32/**
33 * Special layout that finishes its activity when swiped away.
34 */
35public class SwipeDismissLayout extends FrameLayout {
36    private static final String TAG = "SwipeDismissLayout";
37
38    private static final float TRANSLATION_MIN_ALPHA = 0.5f;
39
40    public interface OnDismissedListener {
41        void onDismissed(SwipeDismissLayout layout);
42    }
43
44    public interface OnSwipeProgressChangedListener {
45        /**
46         * Called when the layout has been swiped and the position of the window should change.
47         *
48         * @param progress A number in [-1, 1] representing how far to the left
49         * or right the window has been swiped. Negative values are swipes
50         * left, and positives are right.
51         * @param translate A number in [-w, w], where w is the width of the
52         * layout. This is equivalent to progress * layout.getWidth().
53         */
54        void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate);
55
56        void onSwipeCancelled(SwipeDismissLayout layout);
57    }
58
59    // Cached ViewConfiguration and system-wide constant values
60    private int mSlop;
61    private int mMinFlingVelocity;
62    private int mMaxFlingVelocity;
63    private long mAnimationTime;
64    private TimeInterpolator mCancelInterpolator;
65    private TimeInterpolator mDismissInterpolator;
66
67    // Transient properties
68    private int mActiveTouchId;
69    private float mDownX;
70    private float mDownY;
71    private boolean mSwiping;
72    private boolean mDismissed;
73    private boolean mDiscardIntercept;
74    private VelocityTracker mVelocityTracker;
75    private float mTranslationX;
76
77    private OnDismissedListener mDismissedListener;
78    private OnSwipeProgressChangedListener mProgressListener;
79
80    public SwipeDismissLayout(Context context) {
81        super(context);
82        init(context);
83    }
84
85    public SwipeDismissLayout(Context context, AttributeSet attrs) {
86        super(context, attrs);
87        init(context);
88    }
89
90    public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
91        super(context, attrs, defStyle);
92        init(context);
93    }
94
95    private void init(Context context) {
96        ViewConfiguration vc = ViewConfiguration.get(getContext());
97        mSlop = vc.getScaledTouchSlop();
98        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity() * 16;
99        mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
100        mAnimationTime = getContext().getResources().getInteger(
101                android.R.integer.config_shortAnimTime);
102        mCancelInterpolator = new DecelerateInterpolator(1.5f);
103        mDismissInterpolator = new AccelerateInterpolator(1.5f);
104    }
105
106    public void setOnDismissedListener(OnDismissedListener listener) {
107        mDismissedListener = listener;
108    }
109
110    public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
111        mProgressListener = listener;
112    }
113
114    @Override
115    public boolean onInterceptTouchEvent(MotionEvent ev) {
116        // offset because the view is translated during swipe
117        ev.offsetLocation(mTranslationX, 0);
118
119        switch (ev.getActionMasked()) {
120            case MotionEvent.ACTION_DOWN:
121                resetMembers();
122                mDownX = ev.getRawX();
123                mDownY = ev.getRawY();
124                mActiveTouchId = ev.getPointerId(0);
125                mVelocityTracker = VelocityTracker.obtain();
126                mVelocityTracker.addMovement(ev);
127                break;
128
129            case MotionEvent.ACTION_CANCEL:
130            case MotionEvent.ACTION_UP:
131                resetMembers();
132                break;
133
134            case MotionEvent.ACTION_MOVE:
135                if (mVelocityTracker == null || mDiscardIntercept) {
136                    break;
137                }
138
139                int pointerIndex = ev.findPointerIndex(mActiveTouchId);
140                float dx = ev.getRawX() - mDownX;
141                float x = ev.getX(pointerIndex);
142                float y = ev.getY(pointerIndex);
143                if (dx != 0 && canScroll(this, false, dx, x, y)) {
144                    mDiscardIntercept = true;
145                    break;
146                }
147                updateSwiping(ev);
148                break;
149        }
150
151        return !mDiscardIntercept && mSwiping;
152    }
153
154    @Override
155    public boolean onTouchEvent(MotionEvent ev) {
156        if (mVelocityTracker == null) {
157            return super.onTouchEvent(ev);
158        }
159        switch (ev.getActionMasked()) {
160            case MotionEvent.ACTION_UP:
161                updateDismiss(ev);
162                if (mDismissed) {
163                    dismiss();
164                } else if (mSwiping) {
165                    cancel();
166                }
167                resetMembers();
168                break;
169
170            case MotionEvent.ACTION_CANCEL:
171                cancel();
172                resetMembers();
173                break;
174
175            case MotionEvent.ACTION_MOVE:
176                mVelocityTracker.addMovement(ev);
177                updateSwiping(ev);
178                updateDismiss(ev);
179                if (mSwiping) {
180                    setProgress(ev.getRawX() - mDownX);
181                    break;
182                }
183        }
184        return true;
185    }
186
187    private void setProgress(float deltaX) {
188        mTranslationX = deltaX;
189        if (mProgressListener != null) {
190            mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX);
191        }
192    }
193
194    private void dismiss() {
195        if (mDismissedListener != null) {
196            mDismissedListener.onDismissed(this);
197        }
198    }
199
200    protected void cancel() {
201        if (mProgressListener != null) {
202            mProgressListener.onSwipeCancelled(this);
203        }
204    }
205
206    /**
207     * Resets internal members when canceling.
208     */
209    private void resetMembers() {
210        if (mVelocityTracker != null) {
211            mVelocityTracker.recycle();
212        }
213        mVelocityTracker = null;
214        mTranslationX = 0;
215        mDownX = 0;
216        mDownY = 0;
217        mSwiping = false;
218        mDismissed = false;
219        mDiscardIntercept = false;
220    }
221
222    private void updateSwiping(MotionEvent ev) {
223        if (!mSwiping) {
224            float deltaX = ev.getRawX() - mDownX;
225            float deltaY = ev.getRawY() - mDownY;
226            mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2;
227        }
228    }
229
230    private void updateDismiss(MotionEvent ev) {
231        if (!mDismissed) {
232            mVelocityTracker.addMovement(ev);
233            mVelocityTracker.computeCurrentVelocity(1000);
234
235            float deltaX = ev.getRawX() - mDownX;
236            float velocityX = mVelocityTracker.getXVelocity();
237            float absVelocityX = Math.abs(velocityX);
238            float absVelocityY = Math.abs(mVelocityTracker.getYVelocity());
239
240            if (deltaX > getWidth() / 2) {
241                mDismissed = true;
242            } else if (absVelocityX >= mMinFlingVelocity
243                    && absVelocityX <= mMaxFlingVelocity
244                    && absVelocityY < absVelocityX / 2
245                    && velocityX > 0
246                    && deltaX > 0) {
247                mDismissed = true;
248            }
249        }
250    }
251
252    /**
253     * Tests scrollability within child views of v in the direction of dx.
254     *
255     * @param v View to test for horizontal scrollability
256     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
257     *               or just its children (false).
258     * @param dx Delta scrolled in pixels. Only the sign of this is used.
259     * @param x X coordinate of the active touch point
260     * @param y Y coordinate of the active touch point
261     * @return true if child views of v can be scrolled by delta of dx.
262     */
263    protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) {
264        if (v instanceof ViewGroup) {
265            final ViewGroup group = (ViewGroup) v;
266            final int scrollX = v.getScrollX();
267            final int scrollY = v.getScrollY();
268            final int count = group.getChildCount();
269            for (int i = count - 1; i >= 0; i--) {
270                final View child = group.getChildAt(i);
271                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
272                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
273                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
274                                y + scrollY - child.getTop())) {
275                    return true;
276                }
277            }
278        }
279
280        return checkV && v.canScrollHorizontally((int) -dx);
281    }
282}
283