blob: 97b1634f99cc7cd563e4c5af9c73b7c784cc9931 [file] [log] [blame]
Will Haldean Brownca6234e2014-02-12 10:23:41 -08001/*
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
Mindy Pereira56e533a2014-05-20 10:19:03 -070038 private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f;
Will Haldean Brownca6234e2014-02-12 10:23:41 -080039
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 *
Mark Renouf6c5c48a2014-06-18 10:43:29 -040048 * @param progress A number in [0, 1] representing how far to the
49 * right the window has been swiped
50 * @param translate A number in [0, w], where w is the width of the
Will Haldean Brownca6234e2014-02-12 10:23:41 -080051 * layout. This is equivalent to progress * layout.getWidth().
52 */
53 void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate);
54
55 void onSwipeCancelled(SwipeDismissLayout layout);
56 }
57
58 // Cached ViewConfiguration and system-wide constant values
59 private int mSlop;
60 private int mMinFlingVelocity;
61 private int mMaxFlingVelocity;
62 private long mAnimationTime;
63 private TimeInterpolator mCancelInterpolator;
64 private TimeInterpolator mDismissInterpolator;
65
66 // Transient properties
67 private int mActiveTouchId;
68 private float mDownX;
69 private float mDownY;
70 private boolean mSwiping;
71 private boolean mDismissed;
72 private boolean mDiscardIntercept;
73 private VelocityTracker mVelocityTracker;
74 private float mTranslationX;
75
76 private OnDismissedListener mDismissedListener;
77 private OnSwipeProgressChangedListener mProgressListener;
78
Mark Renouf11b14692014-05-01 14:18:40 -040079 private float mLastX;
80
Will Haldean Brownca6234e2014-02-12 10:23:41 -080081 public SwipeDismissLayout(Context context) {
82 super(context);
83 init(context);
84 }
85
86 public SwipeDismissLayout(Context context, AttributeSet attrs) {
87 super(context, attrs);
88 init(context);
89 }
90
91 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
92 super(context, attrs, defStyle);
93 init(context);
94 }
95
96 private void init(Context context) {
97 ViewConfiguration vc = ViewConfiguration.get(getContext());
98 mSlop = vc.getScaledTouchSlop();
Mark Renouf11b14692014-05-01 14:18:40 -040099 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800100 mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
101 mAnimationTime = getContext().getResources().getInteger(
102 android.R.integer.config_shortAnimTime);
103 mCancelInterpolator = new DecelerateInterpolator(1.5f);
104 mDismissInterpolator = new AccelerateInterpolator(1.5f);
105 }
106
107 public void setOnDismissedListener(OnDismissedListener listener) {
108 mDismissedListener = listener;
109 }
110
111 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
112 mProgressListener = listener;
113 }
114
115 @Override
116 public boolean onInterceptTouchEvent(MotionEvent ev) {
117 // offset because the view is translated during swipe
118 ev.offsetLocation(mTranslationX, 0);
119
120 switch (ev.getActionMasked()) {
121 case MotionEvent.ACTION_DOWN:
122 resetMembers();
123 mDownX = ev.getRawX();
124 mDownY = ev.getRawY();
125 mActiveTouchId = ev.getPointerId(0);
126 mVelocityTracker = VelocityTracker.obtain();
127 mVelocityTracker.addMovement(ev);
128 break;
129
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800130 case MotionEvent.ACTION_POINTER_DOWN:
131 int actionIndex = ev.getActionIndex();
132 mActiveTouchId = ev.getPointerId(actionIndex);
133 break;
134 case MotionEvent.ACTION_POINTER_UP:
135 actionIndex = ev.getActionIndex();
136 int pointerId = ev.getPointerId(actionIndex);
137 if (pointerId == mActiveTouchId) {
138 // This was our active pointer going up. Choose a new active pointer.
139 int newActionIndex = actionIndex == 0 ? 1 : 0;
140 mActiveTouchId = ev.getPointerId(newActionIndex);
141 }
142 break;
143
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800144 case MotionEvent.ACTION_CANCEL:
145 case MotionEvent.ACTION_UP:
146 resetMembers();
147 break;
148
149 case MotionEvent.ACTION_MOVE:
150 if (mVelocityTracker == null || mDiscardIntercept) {
151 break;
152 }
153
154 int pointerIndex = ev.findPointerIndex(mActiveTouchId);
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800155 if (pointerIndex == -1) {
156 Log.e(TAG, "Invalid pointer index: ignoring.");
157 mDiscardIntercept = true;
158 break;
159 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800160 float dx = ev.getRawX() - mDownX;
161 float x = ev.getX(pointerIndex);
162 float y = ev.getY(pointerIndex);
163 if (dx != 0 && canScroll(this, false, dx, x, y)) {
164 mDiscardIntercept = true;
165 break;
166 }
167 updateSwiping(ev);
168 break;
169 }
170
171 return !mDiscardIntercept && mSwiping;
172 }
173
174 @Override
175 public boolean onTouchEvent(MotionEvent ev) {
176 if (mVelocityTracker == null) {
177 return super.onTouchEvent(ev);
178 }
179 switch (ev.getActionMasked()) {
180 case MotionEvent.ACTION_UP:
181 updateDismiss(ev);
182 if (mDismissed) {
183 dismiss();
184 } else if (mSwiping) {
185 cancel();
186 }
187 resetMembers();
188 break;
189
190 case MotionEvent.ACTION_CANCEL:
191 cancel();
192 resetMembers();
193 break;
194
195 case MotionEvent.ACTION_MOVE:
196 mVelocityTracker.addMovement(ev);
Mark Renouf11b14692014-05-01 14:18:40 -0400197 mLastX = ev.getRawX();
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800198 updateSwiping(ev);
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800199 if (mSwiping) {
200 setProgress(ev.getRawX() - mDownX);
201 break;
202 }
203 }
204 return true;
205 }
206
207 private void setProgress(float deltaX) {
208 mTranslationX = deltaX;
Mark Renouf6c5c48a2014-06-18 10:43:29 -0400209 if (mProgressListener != null && deltaX >= 0) {
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800210 mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX);
211 }
212 }
213
214 private void dismiss() {
215 if (mDismissedListener != null) {
216 mDismissedListener.onDismissed(this);
217 }
218 }
219
220 protected void cancel() {
221 if (mProgressListener != null) {
222 mProgressListener.onSwipeCancelled(this);
223 }
224 }
225
226 /**
227 * Resets internal members when canceling.
228 */
229 private void resetMembers() {
230 if (mVelocityTracker != null) {
231 mVelocityTracker.recycle();
232 }
233 mVelocityTracker = null;
234 mTranslationX = 0;
235 mDownX = 0;
236 mDownY = 0;
237 mSwiping = false;
238 mDismissed = false;
239 mDiscardIntercept = false;
240 }
241
242 private void updateSwiping(MotionEvent ev) {
243 if (!mSwiping) {
244 float deltaX = ev.getRawX() - mDownX;
245 float deltaY = ev.getRawY() - mDownY;
Mindy Pereira072c6032014-05-06 15:26:23 -0700246 if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) {
247 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2;
248 } else {
249 mSwiping = false;
250 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800251 }
252 }
253
254 private void updateDismiss(MotionEvent ev) {
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800255 float deltaX = ev.getRawX() - mDownX;
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800256 if (!mDismissed) {
257 mVelocityTracker.addMovement(ev);
258 mVelocityTracker.computeCurrentVelocity(1000);
259
Mindy Pereira072c6032014-05-06 15:26:23 -0700260 if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
Mark Renouf11b14692014-05-01 14:18:40 -0400261 ev.getRawX() >= mLastX) {
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800262 mDismissed = true;
263 }
264 }
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800265 // Check if the user tried to undo this.
266 if (mDismissed && mSwiping) {
267 // Check if the user's finger is actually back
Mindy Pereira072c6032014-05-06 15:26:23 -0700268 if (deltaX < (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO)) {
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800269 mDismissed = false;
270 }
271 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800272 }
273
274 /**
275 * Tests scrollability within child views of v in the direction of dx.
276 *
277 * @param v View to test for horizontal scrollability
278 * @param checkV Whether the view v passed should itself be checked for scrollability (true),
279 * or just its children (false).
280 * @param dx Delta scrolled in pixels. Only the sign of this is used.
281 * @param x X coordinate of the active touch point
282 * @param y Y coordinate of the active touch point
283 * @return true if child views of v can be scrolled by delta of dx.
284 */
285 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) {
286 if (v instanceof ViewGroup) {
287 final ViewGroup group = (ViewGroup) v;
288 final int scrollX = v.getScrollX();
289 final int scrollY = v.getScrollY();
290 final int count = group.getChildCount();
291 for (int i = count - 1; i >= 0; i--) {
292 final View child = group.getChildAt(i);
293 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
294 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
295 canScroll(child, true, dx, x + scrollX - child.getLeft(),
296 y + scrollY - child.getTop())) {
297 return true;
298 }
299 }
300 }
301
302 return checkV && v.canScrollHorizontally((int) -dx);
303 }
304}