SwipeHelper.java revision 622a97646d316ca753c577752ac9010415e9a472
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
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";
33    private static final boolean DEBUG = false;
34    private static final boolean DEBUG_INVALIDATE = false;
35    private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
36    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;
39
40    public static final int X = 0;
41    public static final int Y = 1;
42
43    private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
44    private int MAX_ESCAPE_ANIMATION_DURATION = 500; // ms
45    private int MAX_DISMISS_VELOCITY = 1000; // dp/sec
46    private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms
47
48    public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
49                                                 // 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;
61    private View mCurrAnimView;
62    private boolean mCanCurrViewBeDimissed;
63    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
86    private float getTranslation(View v) {
87        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
88    }
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
119    private float getAlphaForOffset(View view) {
120        float viewSize = getSize(view);
121        final float fadeSize = ALPHA_FADE_END * viewSize;
122        float result = 1.0f;
123        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;
128        }
129        return result;
130    }
131
132    // 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) {
142        //childBounds.offset(view.getTranslationX(), view.getTranslationY());
143        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);
168                mCurrAnimView = mCallback.getChildContentView(mCurrView);
169                mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
170                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;
182                        mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
183                    }
184                }
185                break;
186            case MotionEvent.ACTION_UP:
187                mDragging = false;
188                mCurrView = null;
189                mCurrAnimView = null;
190                break;
191        }
192        return mDragging;
193    }
194
195    public void dismissChild(final View view, float velocity) {
196        final View animView = mCallback.getChildContentView(view);
197        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
198        float newPos;
199        if (velocity < 0 || (velocity == 0 && getTranslation(animView) < 0)) {
200            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,
207                                (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
208                                        .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) {
221                mCallback.onChildDismissed(view);
222            }
223
224            public void onAnimationCancel(Animator animation) {
225                mCallback.onChildDismissed(view);
226            }
227        });
228        anim.addUpdateListener(new AnimatorUpdateListener() {
229            public void onAnimationUpdate(ValueAnimator animation) {
230                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
231                    animView.setAlpha(getAlphaForOffset(animView));
232                }
233                invalidateGlobalRegion(animView);
234            }
235        });
236        anim.start();
237    }
238
239    public void snapChild(final View view, float velocity) {
240        final View animView = mCallback.getChildContentView(view);
241        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
242        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) {
247                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
248                    animView.setAlpha(getAlphaForOffset(animView));
249                }
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)) {
271                        float size = getSize(mCurrAnimView);
272                        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                    }
279                    setTranslation(mCurrAnimView, delta);
280                    if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
281                        mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
282                    }
283                    invalidateGlobalRegion(mCurrView);
284                }
285                break;
286            case MotionEvent.ACTION_UP:
287            case MotionEvent.ACTION_CANCEL:
288                if (mCurrView != null) {
289                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
290                    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 &&
297                            Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
298                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
299                            (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
300                            (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
301
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                        mCallback.onDragCancelled(mCurrView);
311                        snapChild(mCurrView, velocity);
312                    }
313                }
314                break;
315        }
316        return true;
317    }
318
319    public interface Callback {
320        View getChildAtPosition(MotionEvent ev);
321
322        View getChildContentView(View v);
323
324        boolean canChildBeDismissed(View v);
325
326        void onBeginDrag(View v);
327
328        void onChildDismissed(View v);
329
330        void onDragCancelled(View v);
331    }
332}
333