• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.systemui;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ObjectAnimator;
22 import android.animation.Animator.AnimatorListener;
23 import android.animation.ValueAnimator;
24 import android.animation.ValueAnimator.AnimatorUpdateListener;
25 import android.graphics.RectF;
26 import android.util.Log;
27 import android.view.animation.LinearInterpolator;
28 import android.view.MotionEvent;
29 import android.view.VelocityTracker;
30 import android.view.View;
31 
32 public class SwipeHelper {
33     static final String TAG = "com.android.systemui.SwipeHelper";
34     private static final boolean DEBUG = false;
35     private static final boolean DEBUG_INVALIDATE = false;
36     private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
37     private static final boolean CONSTRAIN_SWIPE = true;
38     private static final boolean FADE_OUT_DURING_SWIPE = true;
39     private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
40 
41     public static final int X = 0;
42     public static final int Y = 1;
43 
44     private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
45 
46     private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
47     private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
48     private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
49     private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
50     private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
51 
52     public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
53                                                  // where fade starts
54     static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width
55                                               // beyond which alpha->0
56 
57     private float mPagingTouchSlop;
58     private Callback mCallback;
59     private int mSwipeDirection;
60     private VelocityTracker mVelocityTracker;
61 
62     private float mInitialTouchPos;
63     private boolean mDragging;
64     private View mCurrView;
65     private View mCurrAnimView;
66     private boolean mCanCurrViewBeDimissed;
67     private float mDensityScale;
68 
SwipeHelper(int swipeDirection, Callback callback, float densityScale, float pagingTouchSlop)69     public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
70             float pagingTouchSlop) {
71         mCallback = callback;
72         mSwipeDirection = swipeDirection;
73         mVelocityTracker = VelocityTracker.obtain();
74         mDensityScale = densityScale;
75         mPagingTouchSlop = pagingTouchSlop;
76     }
77 
setDensityScale(float densityScale)78     public void setDensityScale(float densityScale) {
79         mDensityScale = densityScale;
80     }
81 
setPagingTouchSlop(float pagingTouchSlop)82     public void setPagingTouchSlop(float pagingTouchSlop) {
83         mPagingTouchSlop = pagingTouchSlop;
84     }
85 
getPos(MotionEvent ev)86     private float getPos(MotionEvent ev) {
87         return mSwipeDirection == X ? ev.getX() : ev.getY();
88     }
89 
getTranslation(View v)90     private float getTranslation(View v) {
91         return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
92     }
93 
getVelocity(VelocityTracker vt)94     private float getVelocity(VelocityTracker vt) {
95         return mSwipeDirection == X ? vt.getXVelocity() :
96                 vt.getYVelocity();
97     }
98 
createTranslationAnimation(View v, float newPos)99     private ObjectAnimator createTranslationAnimation(View v, float newPos) {
100         ObjectAnimator anim = ObjectAnimator.ofFloat(v,
101                 mSwipeDirection == X ? "translationX" : "translationY", newPos);
102         return anim;
103     }
104 
getPerpendicularVelocity(VelocityTracker vt)105     private float getPerpendicularVelocity(VelocityTracker vt) {
106         return mSwipeDirection == X ? vt.getYVelocity() :
107                 vt.getXVelocity();
108     }
109 
setTranslation(View v, float translate)110     private void setTranslation(View v, float translate) {
111         if (mSwipeDirection == X) {
112             v.setTranslationX(translate);
113         } else {
114             v.setTranslationY(translate);
115         }
116     }
117 
getSize(View v)118     private float getSize(View v) {
119         return mSwipeDirection == X ? v.getMeasuredWidth() :
120                 v.getMeasuredHeight();
121     }
122 
getAlphaForOffset(View view)123     private float getAlphaForOffset(View view) {
124         float viewSize = getSize(view);
125         final float fadeSize = ALPHA_FADE_END * viewSize;
126         float result = 1.0f;
127         float pos = getTranslation(view);
128         if (pos >= viewSize * ALPHA_FADE_START) {
129             result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
130         } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
131             result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
132         }
133         // Make .03 alpha the minimum so you always see the item a bit-- slightly below
134         // .03, the item disappears entirely (as if alpha = 0) and that discontinuity looks
135         // a bit jarring
136         return Math.max(0.03f, result);
137     }
138 
139     // invalidate the view's own bounds all the way up the view hierarchy
invalidateGlobalRegion(View view)140     public static void invalidateGlobalRegion(View view) {
141         invalidateGlobalRegion(
142             view,
143             new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
144     }
145 
146     // invalidate a rectangle relative to the view's coordinate system all the way up the view
147     // hierarchy
invalidateGlobalRegion(View view, RectF childBounds)148     public static void invalidateGlobalRegion(View view, RectF childBounds) {
149         //childBounds.offset(view.getTranslationX(), view.getTranslationY());
150         if (DEBUG_INVALIDATE)
151             Log.v(TAG, "-------------");
152         while (view.getParent() != null && view.getParent() instanceof View) {
153             view = (View) view.getParent();
154             view.getMatrix().mapRect(childBounds);
155             view.invalidate((int) Math.floor(childBounds.left),
156                             (int) Math.floor(childBounds.top),
157                             (int) Math.ceil(childBounds.right),
158                             (int) Math.ceil(childBounds.bottom));
159             if (DEBUG_INVALIDATE) {
160                 Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
161                         + "," + (int) Math.floor(childBounds.top)
162                         + "," + (int) Math.ceil(childBounds.right)
163                         + "," + (int) Math.ceil(childBounds.bottom));
164             }
165         }
166     }
167 
onInterceptTouchEvent(MotionEvent ev)168     public boolean onInterceptTouchEvent(MotionEvent ev) {
169         final int action = ev.getAction();
170 
171         switch (action) {
172             case MotionEvent.ACTION_DOWN:
173                 mDragging = false;
174                 mCurrView = mCallback.getChildAtPosition(ev);
175                 mVelocityTracker.clear();
176                 if (mCurrView != null) {
177                     mCurrAnimView = mCallback.getChildContentView(mCurrView);
178                     mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
179                     mVelocityTracker.addMovement(ev);
180                     mInitialTouchPos = getPos(ev);
181                 }
182                 break;
183             case MotionEvent.ACTION_MOVE:
184                 if (mCurrView != null) {
185                     mVelocityTracker.addMovement(ev);
186                     float pos = getPos(ev);
187                     float delta = pos - mInitialTouchPos;
188                     if (Math.abs(delta) > mPagingTouchSlop) {
189                         mCallback.onBeginDrag(mCurrView);
190                         mDragging = true;
191                         mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
192                     }
193                 }
194                 break;
195             case MotionEvent.ACTION_UP:
196             case MotionEvent.ACTION_CANCEL:
197                 mDragging = false;
198                 mCurrView = null;
199                 mCurrAnimView = null;
200                 break;
201         }
202         return mDragging;
203     }
204 
205     /**
206      * @param view The view to be dismissed
207      * @param velocity The desired pixels/second speed at which the view should move
208      */
dismissChild(final View view, float velocity)209     public void dismissChild(final View view, float velocity) {
210         final View animView = mCallback.getChildContentView(view);
211         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
212         float newPos;
213 
214         if (velocity < 0
215                 || (velocity == 0 && getTranslation(animView) < 0)
216                 // if we use the Menu to dismiss an item in landscape, animate up
217                 || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
218             newPos = -getSize(animView);
219         } else {
220             newPos = getSize(animView);
221         }
222         int duration = MAX_ESCAPE_ANIMATION_DURATION;
223         if (velocity != 0) {
224             duration = Math.min(duration,
225                                 (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
226                                         .abs(velocity)));
227         } else {
228             duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
229         }
230 
231         animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
232         ObjectAnimator anim = createTranslationAnimation(animView, newPos);
233         anim.setInterpolator(sLinearInterpolator);
234         anim.setDuration(duration);
235         anim.addListener(new AnimatorListenerAdapter() {
236             public void onAnimationEnd(Animator animation) {
237                 mCallback.onChildDismissed(view);
238                 animView.setLayerType(View.LAYER_TYPE_NONE, null);
239             }
240         });
241         anim.addUpdateListener(new AnimatorUpdateListener() {
242             public void onAnimationUpdate(ValueAnimator animation) {
243                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
244                     animView.setAlpha(getAlphaForOffset(animView));
245                 }
246                 invalidateGlobalRegion(animView);
247             }
248         });
249         anim.start();
250     }
251 
snapChild(final View view, float velocity)252     public void snapChild(final View view, float velocity) {
253         final View animView = mCallback.getChildContentView(view);
254         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
255         ObjectAnimator anim = createTranslationAnimation(animView, 0);
256         int duration = SNAP_ANIM_LEN;
257         anim.setDuration(duration);
258         anim.addUpdateListener(new AnimatorUpdateListener() {
259             public void onAnimationUpdate(ValueAnimator animation) {
260                 if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
261                     animView.setAlpha(getAlphaForOffset(animView));
262                 }
263                 invalidateGlobalRegion(animView);
264             }
265         });
266         anim.start();
267     }
268 
onTouchEvent(MotionEvent ev)269     public boolean onTouchEvent(MotionEvent ev) {
270         if (!mDragging) {
271             return false;
272         }
273 
274         mVelocityTracker.addMovement(ev);
275         final int action = ev.getAction();
276         switch (action) {
277             case MotionEvent.ACTION_OUTSIDE:
278             case MotionEvent.ACTION_MOVE:
279                 if (mCurrView != null) {
280                     float delta = getPos(ev) - mInitialTouchPos;
281                     // don't let items that can't be dismissed be dragged more than
282                     // maxScrollDistance
283                     if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
284                         float size = getSize(mCurrAnimView);
285                         float maxScrollDistance = 0.15f * size;
286                         if (Math.abs(delta) >= size) {
287                             delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
288                         } else {
289                             delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
290                         }
291                     }
292                     setTranslation(mCurrAnimView, delta);
293                     if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
294                         mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
295                     }
296                     invalidateGlobalRegion(mCurrView);
297                 }
298                 break;
299             case MotionEvent.ACTION_UP:
300             case MotionEvent.ACTION_CANCEL:
301                 if (mCurrView != null) {
302                     float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
303                     mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
304                     float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
305                     float velocity = getVelocity(mVelocityTracker);
306                     float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
307 
308                     // Decide whether to dismiss the current view
309                     boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
310                             Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
311                     boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
312                             (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
313                             (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
314 
315                     boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
316                             (childSwipedFastEnough || childSwipedFarEnough);
317 
318                     if (dismissChild) {
319                         // flingadingy
320                         dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
321                     } else {
322                         // snappity
323                         mCallback.onDragCancelled(mCurrView);
324                         snapChild(mCurrView, velocity);
325                     }
326                 }
327                 break;
328         }
329         return true;
330     }
331 
332     public interface Callback {
getChildAtPosition(MotionEvent ev)333         View getChildAtPosition(MotionEvent ev);
334 
getChildContentView(View v)335         View getChildContentView(View v);
336 
canChildBeDismissed(View v)337         boolean canChildBeDismissed(View v);
338 
onBeginDrag(View v)339         void onBeginDrag(View v);
340 
onChildDismissed(View v)341         void onChildDismissed(View v);
342 
onDragCancelled(View v)343         void onDragCancelled(View v);
344     }
345 }
346