blob: d617c0591c3908c50169dcad6c39e7eac57a6426 [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;
Filip Gruszczynski3f8dd142014-10-21 10:11:27 -070020import android.app.Activity;
Will Haldean Brownca6234e2014-02-12 10:23:41 -080021import android.content.Context;
22import android.util.AttributeSet;
23import android.util.Log;
24import android.view.MotionEvent;
25import android.view.VelocityTracker;
26import android.view.View;
27import android.view.ViewConfiguration;
28import android.view.ViewGroup;
Filip Gruszczynski6eafa902014-11-14 14:24:37 -080029import android.view.ViewTreeObserver;
Will Haldean Brownca6234e2014-02-12 10:23:41 -080030import android.view.animation.AccelerateInterpolator;
31import android.view.animation.DecelerateInterpolator;
32import android.widget.FrameLayout;
33
34/**
35 * Special layout that finishes its activity when swiped away.
36 */
37public class SwipeDismissLayout extends FrameLayout {
38 private static final String TAG = "SwipeDismissLayout";
39
Mindy Pereira56e533a2014-05-20 10:19:03 -070040 private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f;
Will Haldean Brownca6234e2014-02-12 10:23:41 -080041
42 public interface OnDismissedListener {
43 void onDismissed(SwipeDismissLayout layout);
44 }
45
46 public interface OnSwipeProgressChangedListener {
47 /**
48 * Called when the layout has been swiped and the position of the window should change.
49 *
Mark Renouf6c5c48a2014-06-18 10:43:29 -040050 * @param progress A number in [0, 1] representing how far to the
51 * right the window has been swiped
52 * @param translate A number in [0, w], where w is the width of the
Will Haldean Brownca6234e2014-02-12 10:23:41 -080053 * layout. This is equivalent to progress * layout.getWidth().
54 */
55 void onSwipeProgressChanged(SwipeDismissLayout layout, float progress, float translate);
56
57 void onSwipeCancelled(SwipeDismissLayout layout);
58 }
59
60 // Cached ViewConfiguration and system-wide constant values
61 private int mSlop;
62 private int mMinFlingVelocity;
63 private int mMaxFlingVelocity;
64 private long mAnimationTime;
65 private TimeInterpolator mCancelInterpolator;
66 private TimeInterpolator mDismissInterpolator;
67
68 // Transient properties
69 private int mActiveTouchId;
70 private float mDownX;
71 private float mDownY;
72 private boolean mSwiping;
73 private boolean mDismissed;
74 private boolean mDiscardIntercept;
75 private VelocityTracker mVelocityTracker;
76 private float mTranslationX;
77
78 private OnDismissedListener mDismissedListener;
79 private OnSwipeProgressChangedListener mProgressListener;
Filip Gruszczynski6eafa902014-11-14 14:24:37 -080080 private ViewTreeObserver.OnEnterAnimationCompleteListener mOnEnterAnimationCompleteListener =
81 new ViewTreeObserver.OnEnterAnimationCompleteListener() {
82 @Override
83 public void onEnterAnimationComplete() {
84 // SwipeDismissLayout assumes that the host Activity is translucent
85 // and temporarily disables translucency when it is fully visible.
86 // As soon as the user starts swiping, we will re-enable
87 // translucency.
88 if (getContext() instanceof Activity) {
89 ((Activity) getContext()).convertFromTranslucent();
90 }
91 }
92 };
Will Haldean Brownca6234e2014-02-12 10:23:41 -080093
Mark Renouf11b14692014-05-01 14:18:40 -040094 private float mLastX;
95
Will Haldean Brownca6234e2014-02-12 10:23:41 -080096 public SwipeDismissLayout(Context context) {
97 super(context);
98 init(context);
99 }
100
101 public SwipeDismissLayout(Context context, AttributeSet attrs) {
102 super(context, attrs);
103 init(context);
104 }
105
106 public SwipeDismissLayout(Context context, AttributeSet attrs, int defStyle) {
107 super(context, attrs, defStyle);
108 init(context);
109 }
110
111 private void init(Context context) {
112 ViewConfiguration vc = ViewConfiguration.get(getContext());
113 mSlop = vc.getScaledTouchSlop();
Mark Renouf11b14692014-05-01 14:18:40 -0400114 mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800115 mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
116 mAnimationTime = getContext().getResources().getInteger(
117 android.R.integer.config_shortAnimTime);
118 mCancelInterpolator = new DecelerateInterpolator(1.5f);
119 mDismissInterpolator = new AccelerateInterpolator(1.5f);
120 }
121
122 public void setOnDismissedListener(OnDismissedListener listener) {
123 mDismissedListener = listener;
124 }
125
126 public void setOnSwipeProgressChangedListener(OnSwipeProgressChangedListener listener) {
127 mProgressListener = listener;
128 }
129
130 @Override
Filip Gruszczynski6eafa902014-11-14 14:24:37 -0800131 protected void onAttachedToWindow() {
132 super.onAttachedToWindow();
133 if (getContext() instanceof Activity) {
134 getViewTreeObserver().addOnEnterAnimationCompleteListener(
135 mOnEnterAnimationCompleteListener);
136 }
137 }
138
139 @Override
140 protected void onDetachedFromWindow() {
141 super.onDetachedFromWindow();
142 if (getContext() instanceof Activity) {
143 getViewTreeObserver().removeOnEnterAnimationCompleteListener(
144 mOnEnterAnimationCompleteListener);
145 }
146 }
147
148 @Override
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800149 public boolean onInterceptTouchEvent(MotionEvent ev) {
150 // offset because the view is translated during swipe
151 ev.offsetLocation(mTranslationX, 0);
152
153 switch (ev.getActionMasked()) {
154 case MotionEvent.ACTION_DOWN:
155 resetMembers();
156 mDownX = ev.getRawX();
157 mDownY = ev.getRawY();
158 mActiveTouchId = ev.getPointerId(0);
159 mVelocityTracker = VelocityTracker.obtain();
160 mVelocityTracker.addMovement(ev);
161 break;
162
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800163 case MotionEvent.ACTION_POINTER_DOWN:
164 int actionIndex = ev.getActionIndex();
165 mActiveTouchId = ev.getPointerId(actionIndex);
166 break;
167 case MotionEvent.ACTION_POINTER_UP:
168 actionIndex = ev.getActionIndex();
169 int pointerId = ev.getPointerId(actionIndex);
170 if (pointerId == mActiveTouchId) {
171 // This was our active pointer going up. Choose a new active pointer.
172 int newActionIndex = actionIndex == 0 ? 1 : 0;
173 mActiveTouchId = ev.getPointerId(newActionIndex);
174 }
175 break;
176
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800177 case MotionEvent.ACTION_CANCEL:
178 case MotionEvent.ACTION_UP:
179 resetMembers();
180 break;
181
182 case MotionEvent.ACTION_MOVE:
183 if (mVelocityTracker == null || mDiscardIntercept) {
184 break;
185 }
186
187 int pointerIndex = ev.findPointerIndex(mActiveTouchId);
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800188 if (pointerIndex == -1) {
189 Log.e(TAG, "Invalid pointer index: ignoring.");
190 mDiscardIntercept = true;
191 break;
192 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800193 float dx = ev.getRawX() - mDownX;
194 float x = ev.getX(pointerIndex);
195 float y = ev.getY(pointerIndex);
196 if (dx != 0 && canScroll(this, false, dx, x, y)) {
197 mDiscardIntercept = true;
198 break;
199 }
200 updateSwiping(ev);
201 break;
202 }
203
204 return !mDiscardIntercept && mSwiping;
205 }
206
207 @Override
208 public boolean onTouchEvent(MotionEvent ev) {
209 if (mVelocityTracker == null) {
210 return super.onTouchEvent(ev);
211 }
212 switch (ev.getActionMasked()) {
213 case MotionEvent.ACTION_UP:
214 updateDismiss(ev);
215 if (mDismissed) {
216 dismiss();
217 } else if (mSwiping) {
218 cancel();
219 }
220 resetMembers();
221 break;
222
223 case MotionEvent.ACTION_CANCEL:
224 cancel();
225 resetMembers();
226 break;
227
228 case MotionEvent.ACTION_MOVE:
229 mVelocityTracker.addMovement(ev);
Mark Renouf11b14692014-05-01 14:18:40 -0400230 mLastX = ev.getRawX();
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800231 updateSwiping(ev);
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800232 if (mSwiping) {
Filip Gruszczynski625ec482014-10-23 10:56:24 -0700233 if (getContext() instanceof Activity) {
234 ((Activity) getContext()).convertToTranslucent(null, null);
235 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800236 setProgress(ev.getRawX() - mDownX);
237 break;
238 }
239 }
240 return true;
241 }
242
243 private void setProgress(float deltaX) {
244 mTranslationX = deltaX;
Mark Renouf6c5c48a2014-06-18 10:43:29 -0400245 if (mProgressListener != null && deltaX >= 0) {
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800246 mProgressListener.onSwipeProgressChanged(this, deltaX / getWidth(), deltaX);
247 }
248 }
249
250 private void dismiss() {
251 if (mDismissedListener != null) {
252 mDismissedListener.onDismissed(this);
253 }
254 }
255
256 protected void cancel() {
Filip Gruszczynski625ec482014-10-23 10:56:24 -0700257 if (getContext() instanceof Activity) {
258 ((Activity) getContext()).convertFromTranslucent();
259 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800260 if (mProgressListener != null) {
261 mProgressListener.onSwipeCancelled(this);
262 }
263 }
264
265 /**
266 * Resets internal members when canceling.
267 */
268 private void resetMembers() {
269 if (mVelocityTracker != null) {
270 mVelocityTracker.recycle();
271 }
272 mVelocityTracker = null;
273 mTranslationX = 0;
274 mDownX = 0;
275 mDownY = 0;
276 mSwiping = false;
277 mDismissed = false;
278 mDiscardIntercept = false;
279 }
280
281 private void updateSwiping(MotionEvent ev) {
282 if (!mSwiping) {
283 float deltaX = ev.getRawX() - mDownX;
284 float deltaY = ev.getRawY() - mDownY;
Mindy Pereira072c6032014-05-06 15:26:23 -0700285 if ((deltaX * deltaX) + (deltaY * deltaY) > mSlop * mSlop) {
286 mSwiping = deltaX > mSlop * 2 && Math.abs(deltaY) < mSlop * 2;
287 } else {
288 mSwiping = false;
289 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800290 }
291 }
292
293 private void updateDismiss(MotionEvent ev) {
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800294 float deltaX = ev.getRawX() - mDownX;
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800295 if (!mDismissed) {
296 mVelocityTracker.addMovement(ev);
297 mVelocityTracker.computeCurrentVelocity(1000);
298
Mindy Pereira072c6032014-05-06 15:26:23 -0700299 if (deltaX > (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO) &&
Mark Renouf11b14692014-05-01 14:18:40 -0400300 ev.getRawX() >= mLastX) {
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800301 mDismissed = true;
302 }
303 }
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800304 // Check if the user tried to undo this.
305 if (mDismissed && mSwiping) {
306 // Check if the user's finger is actually back
Mindy Pereira072c6032014-05-06 15:26:23 -0700307 if (deltaX < (getWidth() * DISMISS_MIN_DRAG_WIDTH_RATIO)) {
Justin Kohdf4ee5c2014-03-05 19:34:58 -0800308 mDismissed = false;
309 }
310 }
Will Haldean Brownca6234e2014-02-12 10:23:41 -0800311 }
312
313 /**
314 * Tests scrollability within child views of v in the direction of dx.
315 *
316 * @param v View to test for horizontal scrollability
317 * @param checkV Whether the view v passed should itself be checked for scrollability (true),
318 * or just its children (false).
319 * @param dx Delta scrolled in pixels. Only the sign of this is used.
320 * @param x X coordinate of the active touch point
321 * @param y Y coordinate of the active touch point
322 * @return true if child views of v can be scrolled by delta of dx.
323 */
324 protected boolean canScroll(View v, boolean checkV, float dx, float x, float y) {
325 if (v instanceof ViewGroup) {
326 final ViewGroup group = (ViewGroup) v;
327 final int scrollX = v.getScrollX();
328 final int scrollY = v.getScrollY();
329 final int count = group.getChildCount();
330 for (int i = count - 1; i >= 0; i--) {
331 final View child = group.getChildAt(i);
332 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
333 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
334 canScroll(child, true, dx, x + scrollX - child.getLeft(),
335 y + scrollY - child.getTop())) {
336 return true;
337 }
338 }
339 }
340
341 return checkV && v.canScrollHorizontally((int) -dx);
342 }
343}