blob: f824a8e068a05616042a9e0f1fa1c8d388a91a10 [file] [log] [blame]
Michael Jurka07d40462011-07-19 10:54:38 -07001/*
2 * Copyright (C) 2011 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.systemui;
18
19import android.animation.Animator;
Chet Haase2f2022a2011-10-11 06:41:59 -070020import android.animation.AnimatorListenerAdapter;
Michael Jurka07d40462011-07-19 10:54:38 -070021import android.animation.ObjectAnimator;
22import android.animation.Animator.AnimatorListener;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.graphics.RectF;
Daniel Sandlerf7a19562012-04-04 14:04:21 -040026import android.os.Handler;
Michael Jurka07d40462011-07-19 10:54:38 -070027import android.util.Log;
Daniel Sandlerf7a19562012-04-04 14:04:21 -040028import android.view.accessibility.AccessibilityEvent;
Michael Jurka07d40462011-07-19 10:54:38 -070029import android.view.animation.LinearInterpolator;
30import android.view.MotionEvent;
31import android.view.VelocityTracker;
32import android.view.View;
Daniel Sandlerf7a19562012-04-04 14:04:21 -040033import android.view.ViewConfiguration;
Michael Jurka07d40462011-07-19 10:54:38 -070034
Daniel Sandler6a858c32012-03-12 14:38:58 -040035public class SwipeHelper implements Gefingerpoken {
Michael Jurka07d40462011-07-19 10:54:38 -070036 static final String TAG = "com.android.systemui.SwipeHelper";
Daniel Sandler96f48182011-08-17 09:50:35 -040037 private static final boolean DEBUG = false;
Michael Jurka07d40462011-07-19 10:54:38 -070038 private static final boolean DEBUG_INVALIDATE = false;
39 private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
Michael Jurka3cd0a592011-08-16 12:40:30 -070040 private static final boolean CONSTRAIN_SWIPE = true;
41 private static final boolean FADE_OUT_DURING_SWIPE = true;
42 private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
Michael Jurka07d40462011-07-19 10:54:38 -070043
44 public static final int X = 0;
45 public static final int Y = 1;
46
Chet Haase2f2022a2011-10-11 06:41:59 -070047 private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
48
Michael Jurka07d40462011-07-19 10:54:38 -070049 private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
Michael Jurka0e8063a2011-09-09 15:31:55 -070050 private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
51 private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
52 private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
53 private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
Michael Jurka07d40462011-07-19 10:54:38 -070054
Michael Jurka3cd0a592011-08-16 12:40:30 -070055 public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
Michael Jurka07d40462011-07-19 10:54:38 -070056 // where fade starts
57 static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width
58 // beyond which alpha->0
Michael Jurka4eaa9832012-02-29 15:51:49 -080059 private float mMinAlpha = 0f;
Michael Jurka07d40462011-07-19 10:54:38 -070060
61 private float mPagingTouchSlop;
62 private Callback mCallback;
Daniel Sandlerf7a19562012-04-04 14:04:21 -040063 private Handler mHandler;
Michael Jurka07d40462011-07-19 10:54:38 -070064 private int mSwipeDirection;
65 private VelocityTracker mVelocityTracker;
66
67 private float mInitialTouchPos;
68 private boolean mDragging;
69 private View mCurrView;
Michael Jurka3cd0a592011-08-16 12:40:30 -070070 private View mCurrAnimView;
71 private boolean mCanCurrViewBeDimissed;
Michael Jurka07d40462011-07-19 10:54:38 -070072 private float mDensityScale;
73
Daniel Sandlerf7a19562012-04-04 14:04:21 -040074 private boolean mLongPressSent;
75 private View.OnLongClickListener mLongPressListener;
76 private Runnable mWatchLongPress;
Daniel Sandler469e96e2012-05-04 15:56:19 -040077 private long mLongPressTimeout;
Daniel Sandlerf7a19562012-04-04 14:04:21 -040078
Michael Jurka07d40462011-07-19 10:54:38 -070079 public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
80 float pagingTouchSlop) {
81 mCallback = callback;
Daniel Sandlerf7a19562012-04-04 14:04:21 -040082 mHandler = new Handler();
Michael Jurka07d40462011-07-19 10:54:38 -070083 mSwipeDirection = swipeDirection;
84 mVelocityTracker = VelocityTracker.obtain();
85 mDensityScale = densityScale;
86 mPagingTouchSlop = pagingTouchSlop;
Daniel Sandler469e96e2012-05-04 15:56:19 -040087
88 mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press!
Michael Jurka07d40462011-07-19 10:54:38 -070089 }
90
Daniel Sandlerf7a19562012-04-04 14:04:21 -040091 public void setLongPressListener(View.OnLongClickListener listener) {
92 mLongPressListener = listener;
93 }
94
Michael Jurka07d40462011-07-19 10:54:38 -070095 public void setDensityScale(float densityScale) {
96 mDensityScale = densityScale;
97 }
98
99 public void setPagingTouchSlop(float pagingTouchSlop) {
100 mPagingTouchSlop = pagingTouchSlop;
101 }
102
103 private float getPos(MotionEvent ev) {
104 return mSwipeDirection == X ? ev.getX() : ev.getY();
105 }
106
Michael Jurka3cd0a592011-08-16 12:40:30 -0700107 private float getTranslation(View v) {
108 return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
Michael Jurka07d40462011-07-19 10:54:38 -0700109 }
110
111 private float getVelocity(VelocityTracker vt) {
112 return mSwipeDirection == X ? vt.getXVelocity() :
113 vt.getYVelocity();
114 }
115
116 private ObjectAnimator createTranslationAnimation(View v, float newPos) {
117 ObjectAnimator anim = ObjectAnimator.ofFloat(v,
118 mSwipeDirection == X ? "translationX" : "translationY", newPos);
119 return anim;
120 }
121
122 private float getPerpendicularVelocity(VelocityTracker vt) {
123 return mSwipeDirection == X ? vt.getYVelocity() :
124 vt.getXVelocity();
125 }
126
127 private void setTranslation(View v, float translate) {
128 if (mSwipeDirection == X) {
129 v.setTranslationX(translate);
130 } else {
131 v.setTranslationY(translate);
132 }
133 }
134
135 private float getSize(View v) {
136 return mSwipeDirection == X ? v.getMeasuredWidth() :
137 v.getMeasuredHeight();
138 }
139
Michael Jurka4eaa9832012-02-29 15:51:49 -0800140 public void setMinAlpha(float minAlpha) {
141 mMinAlpha = minAlpha;
142 }
143
Michael Jurka3cd0a592011-08-16 12:40:30 -0700144 private float getAlphaForOffset(View view) {
145 float viewSize = getSize(view);
146 final float fadeSize = ALPHA_FADE_END * viewSize;
Michael Jurka07d40462011-07-19 10:54:38 -0700147 float result = 1.0f;
Michael Jurka3cd0a592011-08-16 12:40:30 -0700148 float pos = getTranslation(view);
149 if (pos >= viewSize * ALPHA_FADE_START) {
150 result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
151 } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
152 result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
Michael Jurka07d40462011-07-19 10:54:38 -0700153 }
Michael Jurka4eaa9832012-02-29 15:51:49 -0800154 return Math.max(mMinAlpha, result);
Michael Jurka07d40462011-07-19 10:54:38 -0700155 }
156
Michael Jurka67b03702013-02-15 17:35:48 +0100157 private void updateAlphaFromOffset(View animView, boolean dismissable) {
158 if (FADE_OUT_DURING_SWIPE && dismissable) {
Michael Jurka499293f2013-03-06 18:08:45 +0100159 float alpha = getAlphaForOffset(animView);
160 if (alpha != 0f && alpha != 1f) {
161 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
162 } else {
163 animView.setLayerType(View.LAYER_TYPE_NONE, null);
164 }
Michael Jurka67b03702013-02-15 17:35:48 +0100165 animView.setAlpha(getAlphaForOffset(animView));
166 }
167 invalidateGlobalRegion(animView);
168 }
169
Daniel Sandlera375c942011-07-29 00:33:53 -0400170 // invalidate the view's own bounds all the way up the view hierarchy
171 public static void invalidateGlobalRegion(View view) {
172 invalidateGlobalRegion(
173 view,
174 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
175 }
176
177 // invalidate a rectangle relative to the view's coordinate system all the way up the view
178 // hierarchy
179 public static void invalidateGlobalRegion(View view, RectF childBounds) {
Daniel Sandler96f48182011-08-17 09:50:35 -0400180 //childBounds.offset(view.getTranslationX(), view.getTranslationY());
Michael Jurka07d40462011-07-19 10:54:38 -0700181 if (DEBUG_INVALIDATE)
182 Log.v(TAG, "-------------");
183 while (view.getParent() != null && view.getParent() instanceof View) {
184 view = (View) view.getParent();
185 view.getMatrix().mapRect(childBounds);
186 view.invalidate((int) Math.floor(childBounds.left),
187 (int) Math.floor(childBounds.top),
188 (int) Math.ceil(childBounds.right),
189 (int) Math.ceil(childBounds.bottom));
190 if (DEBUG_INVALIDATE) {
191 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
192 + "," + (int) Math.floor(childBounds.top)
193 + "," + (int) Math.ceil(childBounds.right)
194 + "," + (int) Math.ceil(childBounds.bottom));
195 }
196 }
197 }
198
Daniel Sandler469e96e2012-05-04 15:56:19 -0400199 public void removeLongPressCallback() {
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400200 if (mWatchLongPress != null) {
201 mHandler.removeCallbacks(mWatchLongPress);
Daniel Sandler491d3a92012-05-16 13:04:06 -0400202 mWatchLongPress = null;
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400203 }
204 }
205
Michael Jurka07d40462011-07-19 10:54:38 -0700206 public boolean onInterceptTouchEvent(MotionEvent ev) {
207 final int action = ev.getAction();
208
209 switch (action) {
210 case MotionEvent.ACTION_DOWN:
211 mDragging = false;
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400212 mLongPressSent = false;
Michael Jurka07d40462011-07-19 10:54:38 -0700213 mCurrView = mCallback.getChildAtPosition(ev);
214 mVelocityTracker.clear();
Michael Jurka21ce2d82011-09-02 15:28:06 -0700215 if (mCurrView != null) {
216 mCurrAnimView = mCallback.getChildContentView(mCurrView);
217 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
218 mVelocityTracker.addMovement(ev);
219 mInitialTouchPos = getPos(ev);
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400220
221 if (mLongPressListener != null) {
222 if (mWatchLongPress == null) {
223 mWatchLongPress = new Runnable() {
224 @Override
225 public void run() {
226 if (mCurrView != null && !mLongPressSent) {
227 mLongPressSent = true;
228 mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
229 mLongPressListener.onLongClick(mCurrView);
230 }
231 }
232 };
233 }
Daniel Sandler469e96e2012-05-04 15:56:19 -0400234 mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400235 }
236
Michael Jurka21ce2d82011-09-02 15:28:06 -0700237 }
Michael Jurka07d40462011-07-19 10:54:38 -0700238 break;
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400239
Michael Jurka07d40462011-07-19 10:54:38 -0700240 case MotionEvent.ACTION_MOVE:
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400241 if (mCurrView != null && !mLongPressSent) {
Michael Jurka07d40462011-07-19 10:54:38 -0700242 mVelocityTracker.addMovement(ev);
243 float pos = getPos(ev);
244 float delta = pos - mInitialTouchPos;
245 if (Math.abs(delta) > mPagingTouchSlop) {
246 mCallback.onBeginDrag(mCurrView);
247 mDragging = true;
Michael Jurka3cd0a592011-08-16 12:40:30 -0700248 mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400249
250 removeLongPressCallback();
Michael Jurka07d40462011-07-19 10:54:38 -0700251 }
252 }
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400253
Michael Jurka07d40462011-07-19 10:54:38 -0700254 break;
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400255
Michael Jurka07d40462011-07-19 10:54:38 -0700256 case MotionEvent.ACTION_UP:
Jeff Brown68ebcdf2011-09-12 14:12:17 -0700257 case MotionEvent.ACTION_CANCEL:
Michael Jurka07d40462011-07-19 10:54:38 -0700258 mDragging = false;
259 mCurrView = null;
Michael Jurka3cd0a592011-08-16 12:40:30 -0700260 mCurrAnimView = null;
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400261 mLongPressSent = false;
Daniel Sandler491d3a92012-05-16 13:04:06 -0400262 removeLongPressCallback();
Michael Jurka07d40462011-07-19 10:54:38 -0700263 break;
264 }
265 return mDragging;
266 }
267
Chet Haase2f2022a2011-10-11 06:41:59 -0700268 /**
269 * @param view The view to be dismissed
270 * @param velocity The desired pixels/second speed at which the view should move
271 */
Michael Jurka3cd0a592011-08-16 12:40:30 -0700272 public void dismissChild(final View view, float velocity) {
273 final View animView = mCallback.getChildContentView(view);
274 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
Michael Jurka07d40462011-07-19 10:54:38 -0700275 float newPos;
Michael Jurkac6461ca2011-09-02 12:12:15 -0700276
277 if (velocity < 0
278 || (velocity == 0 && getTranslation(animView) < 0)
279 // if we use the Menu to dismiss an item in landscape, animate up
280 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
Michael Jurka07d40462011-07-19 10:54:38 -0700281 newPos = -getSize(animView);
282 } else {
283 newPos = getSize(animView);
284 }
285 int duration = MAX_ESCAPE_ANIMATION_DURATION;
286 if (velocity != 0) {
287 duration = Math.min(duration,
Michael Jurka3cd0a592011-08-16 12:40:30 -0700288 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
Michael Jurka07d40462011-07-19 10:54:38 -0700289 .abs(velocity)));
Michael Jurka0e8063a2011-09-09 15:31:55 -0700290 } else {
291 duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
Michael Jurka07d40462011-07-19 10:54:38 -0700292 }
Michael Jurka0e8063a2011-09-09 15:31:55 -0700293
Chet Haase2f2022a2011-10-11 06:41:59 -0700294 animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
Michael Jurka07d40462011-07-19 10:54:38 -0700295 ObjectAnimator anim = createTranslationAnimation(animView, newPos);
Chet Haase2f2022a2011-10-11 06:41:59 -0700296 anim.setInterpolator(sLinearInterpolator);
Michael Jurka07d40462011-07-19 10:54:38 -0700297 anim.setDuration(duration);
Chet Haase2f2022a2011-10-11 06:41:59 -0700298 anim.addListener(new AnimatorListenerAdapter() {
Michael Jurka07d40462011-07-19 10:54:38 -0700299 public void onAnimationEnd(Animator animation) {
Michael Jurka3cd0a592011-08-16 12:40:30 -0700300 mCallback.onChildDismissed(view);
Chet Haase2f2022a2011-10-11 06:41:59 -0700301 animView.setLayerType(View.LAYER_TYPE_NONE, null);
Michael Jurka07d40462011-07-19 10:54:38 -0700302 }
303 });
304 anim.addUpdateListener(new AnimatorUpdateListener() {
305 public void onAnimationUpdate(ValueAnimator animation) {
Michael Jurka67b03702013-02-15 17:35:48 +0100306 updateAlphaFromOffset(animView, canAnimViewBeDismissed);
Michael Jurka07d40462011-07-19 10:54:38 -0700307 }
308 });
309 anim.start();
310 }
311
Michael Jurka3cd0a592011-08-16 12:40:30 -0700312 public void snapChild(final View view, float velocity) {
313 final View animView = mCallback.getChildContentView(view);
314 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
Michael Jurka07d40462011-07-19 10:54:38 -0700315 ObjectAnimator anim = createTranslationAnimation(animView, 0);
316 int duration = SNAP_ANIM_LEN;
317 anim.setDuration(duration);
318 anim.addUpdateListener(new AnimatorUpdateListener() {
319 public void onAnimationUpdate(ValueAnimator animation) {
Michael Jurka67b03702013-02-15 17:35:48 +0100320 updateAlphaFromOffset(animView, canAnimViewBeDismissed);
321 }
322 });
323 anim.addListener(new AnimatorListenerAdapter() {
324 public void onAnimationEnd(Animator animator) {
325 updateAlphaFromOffset(animView, canAnimViewBeDismissed);
Michael Jurka07d40462011-07-19 10:54:38 -0700326 }
327 });
328 anim.start();
329 }
330
331 public boolean onTouchEvent(MotionEvent ev) {
Daniel Sandlerf7a19562012-04-04 14:04:21 -0400332 if (mLongPressSent) {
333 return true;
334 }
335
Michael Jurka07d40462011-07-19 10:54:38 -0700336 if (!mDragging) {
Dianne Hackborn7f3b3792012-06-26 16:39:02 -0700337 // We are not doing anything, make sure the long press callback
338 // is not still ticking like a bomb waiting to go off.
339 removeLongPressCallback();
Michael Jurka07d40462011-07-19 10:54:38 -0700340 return false;
341 }
342
343 mVelocityTracker.addMovement(ev);
344 final int action = ev.getAction();
345 switch (action) {
346 case MotionEvent.ACTION_OUTSIDE:
347 case MotionEvent.ACTION_MOVE:
348 if (mCurrView != null) {
349 float delta = getPos(ev) - mInitialTouchPos;
350 // don't let items that can't be dismissed be dragged more than
351 // maxScrollDistance
352 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
Michael Jurka3cd0a592011-08-16 12:40:30 -0700353 float size = getSize(mCurrAnimView);
Michael Jurka07d40462011-07-19 10:54:38 -0700354 float maxScrollDistance = 0.15f * size;
355 if (Math.abs(delta) >= size) {
356 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
357 } else {
358 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
359 }
360 }
Michael Jurka3cd0a592011-08-16 12:40:30 -0700361 setTranslation(mCurrAnimView, delta);
Michael Jurka67b03702013-02-15 17:35:48 +0100362
363 updateAlphaFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
Michael Jurka07d40462011-07-19 10:54:38 -0700364 }
365 break;
366 case MotionEvent.ACTION_UP:
367 case MotionEvent.ACTION_CANCEL:
368 if (mCurrView != null) {
Daniel Sandler0761e4c2011-08-11 00:19:49 -0400369 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
Michael Jurka07d40462011-07-19 10:54:38 -0700370 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
371 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
372 float velocity = getVelocity(mVelocityTracker);
373 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
374
375 // Decide whether to dismiss the current view
376 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
Michael Jurka3cd0a592011-08-16 12:40:30 -0700377 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
Michael Jurka07d40462011-07-19 10:54:38 -0700378 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
379 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
Michael Jurka3cd0a592011-08-16 12:40:30 -0700380 (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
Michael Jurka07d40462011-07-19 10:54:38 -0700381
382 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
383 (childSwipedFastEnough || childSwipedFarEnough);
384
385 if (dismissChild) {
386 // flingadingy
387 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
388 } else {
389 // snappity
Peter Ng622a9762011-08-29 10:56:53 -0700390 mCallback.onDragCancelled(mCurrView);
Michael Jurka07d40462011-07-19 10:54:38 -0700391 snapChild(mCurrView, velocity);
392 }
393 }
394 break;
395 }
396 return true;
397 }
398
399 public interface Callback {
400 View getChildAtPosition(MotionEvent ev);
401
402 View getChildContentView(View v);
403
404 boolean canChildBeDismissed(View v);
405
406 void onBeginDrag(View v);
407
408 void onChildDismissed(View v);
Peter Ng622a9762011-08-29 10:56:53 -0700409
410 void onDragCancelled(View v);
Michael Jurka07d40462011-07-19 10:54:38 -0700411 }
412}