blob: 6cfbc1b2676858a60587ec2f51e74e153b9bfe4c [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;
20import android.animation.ObjectAnimator;
21import android.animation.Animator.AnimatorListener;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.graphics.RectF;
25import android.util.Log;
26import android.view.animation.LinearInterpolator;
27import android.view.MotionEvent;
28import android.view.VelocityTracker;
29import android.view.View;
30
31public class SwipeHelper {
32 static final String TAG = "com.android.systemui.SwipeHelper";
Daniel Sandler96f48182011-08-17 09:50:35 -040033 private static final boolean DEBUG = false;
Michael Jurka07d40462011-07-19 10:54:38 -070034 private static final boolean DEBUG_INVALIDATE = false;
35 private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
Michael Jurka3cd0a592011-08-16 12:40:30 -070036 private static final boolean CONSTRAIN_SWIPE = true;
37 private static final boolean FADE_OUT_DURING_SWIPE = true;
38 private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
Michael Jurka07d40462011-07-19 10:54:38 -070039
40 public static final int X = 0;
41 public static final int Y = 1;
42
Michael Jurka07d40462011-07-19 10:54:38 -070043 private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
44 private int MAX_ESCAPE_ANIMATION_DURATION = 500; // ms
Daniel Sandler0761e4c2011-08-11 00:19:49 -040045 private int MAX_DISMISS_VELOCITY = 1000; // dp/sec
Michael Jurka07d40462011-07-19 10:54:38 -070046 private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms
47
Michael Jurka3cd0a592011-08-16 12:40:30 -070048 public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
Michael Jurka07d40462011-07-19 10:54:38 -070049 // where fade starts
50 static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width
51 // beyond which alpha->0
52
53 private float mPagingTouchSlop;
54 private Callback mCallback;
55 private int mSwipeDirection;
56 private VelocityTracker mVelocityTracker;
57
58 private float mInitialTouchPos;
59 private boolean mDragging;
60 private View mCurrView;
Michael Jurka3cd0a592011-08-16 12:40:30 -070061 private View mCurrAnimView;
62 private boolean mCanCurrViewBeDimissed;
Michael Jurka07d40462011-07-19 10:54:38 -070063 private float mDensityScale;
64
65 public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
66 float pagingTouchSlop) {
67 mCallback = callback;
68 mSwipeDirection = swipeDirection;
69 mVelocityTracker = VelocityTracker.obtain();
70 mDensityScale = densityScale;
71 mPagingTouchSlop = pagingTouchSlop;
72 }
73
74 public void setDensityScale(float densityScale) {
75 mDensityScale = densityScale;
76 }
77
78 public void setPagingTouchSlop(float pagingTouchSlop) {
79 mPagingTouchSlop = pagingTouchSlop;
80 }
81
82 private float getPos(MotionEvent ev) {
83 return mSwipeDirection == X ? ev.getX() : ev.getY();
84 }
85
Michael Jurka3cd0a592011-08-16 12:40:30 -070086 private float getTranslation(View v) {
87 return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
Michael Jurka07d40462011-07-19 10:54:38 -070088 }
89
90 private float getVelocity(VelocityTracker vt) {
91 return mSwipeDirection == X ? vt.getXVelocity() :
92 vt.getYVelocity();
93 }
94
95 private ObjectAnimator createTranslationAnimation(View v, float newPos) {
96 ObjectAnimator anim = ObjectAnimator.ofFloat(v,
97 mSwipeDirection == X ? "translationX" : "translationY", newPos);
98 return anim;
99 }
100
101 private float getPerpendicularVelocity(VelocityTracker vt) {
102 return mSwipeDirection == X ? vt.getYVelocity() :
103 vt.getXVelocity();
104 }
105
106 private void setTranslation(View v, float translate) {
107 if (mSwipeDirection == X) {
108 v.setTranslationX(translate);
109 } else {
110 v.setTranslationY(translate);
111 }
112 }
113
114 private float getSize(View v) {
115 return mSwipeDirection == X ? v.getMeasuredWidth() :
116 v.getMeasuredHeight();
117 }
118
Michael Jurka3cd0a592011-08-16 12:40:30 -0700119 private float getAlphaForOffset(View view) {
120 float viewSize = getSize(view);
121 final float fadeSize = ALPHA_FADE_END * viewSize;
Michael Jurka07d40462011-07-19 10:54:38 -0700122 float result = 1.0f;
Michael Jurka3cd0a592011-08-16 12:40:30 -0700123 float pos = getTranslation(view);
124 if (pos >= viewSize * ALPHA_FADE_START) {
125 result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
126 } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
127 result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
Michael Jurka07d40462011-07-19 10:54:38 -0700128 }
129 return result;
130 }
131
Daniel Sandlera375c942011-07-29 00:33:53 -0400132 // invalidate the view's own bounds all the way up the view hierarchy
133 public static void invalidateGlobalRegion(View view) {
134 invalidateGlobalRegion(
135 view,
136 new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
137 }
138
139 // invalidate a rectangle relative to the view's coordinate system all the way up the view
140 // hierarchy
141 public static void invalidateGlobalRegion(View view, RectF childBounds) {
Daniel Sandler96f48182011-08-17 09:50:35 -0400142 //childBounds.offset(view.getTranslationX(), view.getTranslationY());
Michael Jurka07d40462011-07-19 10:54:38 -0700143 if (DEBUG_INVALIDATE)
144 Log.v(TAG, "-------------");
145 while (view.getParent() != null && view.getParent() instanceof View) {
146 view = (View) view.getParent();
147 view.getMatrix().mapRect(childBounds);
148 view.invalidate((int) Math.floor(childBounds.left),
149 (int) Math.floor(childBounds.top),
150 (int) Math.ceil(childBounds.right),
151 (int) Math.ceil(childBounds.bottom));
152 if (DEBUG_INVALIDATE) {
153 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
154 + "," + (int) Math.floor(childBounds.top)
155 + "," + (int) Math.ceil(childBounds.right)
156 + "," + (int) Math.ceil(childBounds.bottom));
157 }
158 }
159 }
160
161 public boolean onInterceptTouchEvent(MotionEvent ev) {
162 final int action = ev.getAction();
163
164 switch (action) {
165 case MotionEvent.ACTION_DOWN:
166 mDragging = false;
167 mCurrView = mCallback.getChildAtPosition(ev);
Michael Jurka3cd0a592011-08-16 12:40:30 -0700168 mCurrAnimView = mCallback.getChildContentView(mCurrView);
169 mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
Michael Jurka07d40462011-07-19 10:54:38 -0700170 mVelocityTracker.clear();
171 mVelocityTracker.addMovement(ev);
172 mInitialTouchPos = getPos(ev);
173 break;
174 case MotionEvent.ACTION_MOVE:
175 if (mCurrView != null) {
176 mVelocityTracker.addMovement(ev);
177 float pos = getPos(ev);
178 float delta = pos - mInitialTouchPos;
179 if (Math.abs(delta) > mPagingTouchSlop) {
180 mCallback.onBeginDrag(mCurrView);
181 mDragging = true;
Michael Jurka3cd0a592011-08-16 12:40:30 -0700182 mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
Michael Jurka07d40462011-07-19 10:54:38 -0700183 }
184 }
185 break;
186 case MotionEvent.ACTION_UP:
187 mDragging = false;
188 mCurrView = null;
Michael Jurka3cd0a592011-08-16 12:40:30 -0700189 mCurrAnimView = null;
Michael Jurka07d40462011-07-19 10:54:38 -0700190 break;
191 }
192 return mDragging;
193 }
194
Michael Jurka3cd0a592011-08-16 12:40:30 -0700195 public void dismissChild(final View view, float velocity) {
196 final View animView = mCallback.getChildContentView(view);
197 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
Michael Jurka07d40462011-07-19 10:54:38 -0700198 float newPos;
Michael Jurka3cd0a592011-08-16 12:40:30 -0700199 if (velocity < 0 || (velocity == 0 && getTranslation(animView) < 0)) {
Michael Jurka07d40462011-07-19 10:54:38 -0700200 newPos = -getSize(animView);
201 } else {
202 newPos = getSize(animView);
203 }
204 int duration = MAX_ESCAPE_ANIMATION_DURATION;
205 if (velocity != 0) {
206 duration = Math.min(duration,
Michael Jurka3cd0a592011-08-16 12:40:30 -0700207 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
Michael Jurka07d40462011-07-19 10:54:38 -0700208 .abs(velocity)));
209 }
210 ObjectAnimator anim = createTranslationAnimation(animView, newPos);
211 anim.setInterpolator(new LinearInterpolator());
212 anim.setDuration(duration);
213 anim.addListener(new AnimatorListener() {
214 public void onAnimationStart(Animator animation) {
215 }
216
217 public void onAnimationRepeat(Animator animation) {
218 }
219
220 public void onAnimationEnd(Animator animation) {
Michael Jurka3cd0a592011-08-16 12:40:30 -0700221 mCallback.onChildDismissed(view);
Michael Jurka07d40462011-07-19 10:54:38 -0700222 }
223
224 public void onAnimationCancel(Animator animation) {
Michael Jurka3cd0a592011-08-16 12:40:30 -0700225 mCallback.onChildDismissed(view);
Michael Jurka07d40462011-07-19 10:54:38 -0700226 }
227 });
228 anim.addUpdateListener(new AnimatorUpdateListener() {
229 public void onAnimationUpdate(ValueAnimator animation) {
Michael Jurka3cd0a592011-08-16 12:40:30 -0700230 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
231 animView.setAlpha(getAlphaForOffset(animView));
Michael Jurka07d40462011-07-19 10:54:38 -0700232 }
233 invalidateGlobalRegion(animView);
234 }
235 });
236 anim.start();
237 }
238
Michael Jurka3cd0a592011-08-16 12:40:30 -0700239 public void snapChild(final View view, float velocity) {
240 final View animView = mCallback.getChildContentView(view);
241 final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
Michael Jurka07d40462011-07-19 10:54:38 -0700242 ObjectAnimator anim = createTranslationAnimation(animView, 0);
243 int duration = SNAP_ANIM_LEN;
244 anim.setDuration(duration);
245 anim.addUpdateListener(new AnimatorUpdateListener() {
246 public void onAnimationUpdate(ValueAnimator animation) {
Michael Jurka3cd0a592011-08-16 12:40:30 -0700247 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
248 animView.setAlpha(getAlphaForOffset(animView));
Michael Jurka07d40462011-07-19 10:54:38 -0700249 }
250 invalidateGlobalRegion(animView);
251 }
252 });
253 anim.start();
254 }
255
256 public boolean onTouchEvent(MotionEvent ev) {
257 if (!mDragging) {
258 return false;
259 }
260
261 mVelocityTracker.addMovement(ev);
262 final int action = ev.getAction();
263 switch (action) {
264 case MotionEvent.ACTION_OUTSIDE:
265 case MotionEvent.ACTION_MOVE:
266 if (mCurrView != null) {
267 float delta = getPos(ev) - mInitialTouchPos;
268 // don't let items that can't be dismissed be dragged more than
269 // maxScrollDistance
270 if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
Michael Jurka3cd0a592011-08-16 12:40:30 -0700271 float size = getSize(mCurrAnimView);
Michael Jurka07d40462011-07-19 10:54:38 -0700272 float maxScrollDistance = 0.15f * size;
273 if (Math.abs(delta) >= size) {
274 delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
275 } else {
276 delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
277 }
278 }
Michael Jurka3cd0a592011-08-16 12:40:30 -0700279 setTranslation(mCurrAnimView, delta);
280 if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
281 mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
Michael Jurka07d40462011-07-19 10:54:38 -0700282 }
283 invalidateGlobalRegion(mCurrView);
284 }
285 break;
286 case MotionEvent.ACTION_UP:
287 case MotionEvent.ACTION_CANCEL:
288 if (mCurrView != null) {
Daniel Sandler0761e4c2011-08-11 00:19:49 -0400289 float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
Michael Jurka07d40462011-07-19 10:54:38 -0700290 mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
291 float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
292 float velocity = getVelocity(mVelocityTracker);
293 float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
294
295 // Decide whether to dismiss the current view
296 boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
Michael Jurka3cd0a592011-08-16 12:40:30 -0700297 Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
Michael Jurka07d40462011-07-19 10:54:38 -0700298 boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
299 (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
Michael Jurka3cd0a592011-08-16 12:40:30 -0700300 (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
Michael Jurka07d40462011-07-19 10:54:38 -0700301
302 boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
303 (childSwipedFastEnough || childSwipedFarEnough);
304
305 if (dismissChild) {
306 // flingadingy
307 dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
308 } else {
309 // snappity
310 snapChild(mCurrView, velocity);
311 }
312 }
313 break;
314 }
315 return true;
316 }
317
318 public interface Callback {
319 View getChildAtPosition(MotionEvent ev);
320
321 View getChildContentView(View v);
322
323 boolean canChildBeDismissed(View v);
324
325 void onBeginDrag(View v);
326
327 void onChildDismissed(View v);
328 }
329}