SwipeHelper.java revision a375c94fca986d76d21f8cb9a3eb29f1ef88c4a8
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 = true;
34    private static final boolean DEBUG_INVALIDATE = false;
35    private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
36
37    public static final int X = 0;
38    public static final int Y = 1;
39
40    private boolean CONSTRAIN_SWIPE = true;
41    private boolean FADE_OUT_DURING_SWIPE = true;
42    private boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
43
44    private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
45    private int MAX_ESCAPE_ANIMATION_DURATION = 500; // ms
46    private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms
47
48    public static float ALPHA_FADE_START = 0.8f; // 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 float mDensityScale;
62
63    public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
64            float pagingTouchSlop) {
65        mCallback = callback;
66        mSwipeDirection = swipeDirection;
67        mVelocityTracker = VelocityTracker.obtain();
68        mDensityScale = densityScale;
69        mPagingTouchSlop = pagingTouchSlop;
70    }
71
72    public void setDensityScale(float densityScale) {
73        mDensityScale = densityScale;
74    }
75
76    public void setPagingTouchSlop(float pagingTouchSlop) {
77        mPagingTouchSlop = pagingTouchSlop;
78    }
79
80    private float getPos(MotionEvent ev) {
81        return mSwipeDirection == X ? ev.getX() : ev.getY();
82    }
83
84    private float getPos(View v) {
85        return mSwipeDirection == X ? v.getX() : v.getY();
86    }
87
88    private float getVelocity(VelocityTracker vt) {
89        return mSwipeDirection == X ? vt.getXVelocity() :
90                vt.getYVelocity();
91    }
92
93    private ObjectAnimator createTranslationAnimation(View v, float newPos) {
94        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
95                mSwipeDirection == X ? "translationX" : "translationY", newPos);
96        return anim;
97    }
98
99    private float getPerpendicularVelocity(VelocityTracker vt) {
100        return mSwipeDirection == X ? vt.getYVelocity() :
101                vt.getXVelocity();
102    }
103
104    private void setTranslation(View v, float translate) {
105        if (mSwipeDirection == X) {
106            v.setTranslationX(translate);
107        } else {
108            v.setTranslationY(translate);
109        }
110    }
111
112    private float getSize(View v) {
113        return mSwipeDirection == X ? v.getMeasuredWidth() :
114                v.getMeasuredHeight();
115    }
116
117    private float getContentSize(View v) {
118        View content = mCallback.getChildContentView(v);
119        return getSize(content);
120    }
121
122    private float getAlphaForOffset(View view, float thumbSize) {
123        final float fadeSize = ALPHA_FADE_END * thumbSize;
124        float result = 1.0f;
125        float pos = getPos(view);
126        if (pos >= thumbSize * ALPHA_FADE_START) {
127            result = 1.0f - (pos - thumbSize * ALPHA_FADE_START) / fadeSize;
128        } else if (pos < thumbSize * (1.0f - ALPHA_FADE_START)) {
129            result = 1.0f + (thumbSize * ALPHA_FADE_START + pos) / fadeSize;
130        }
131        return result;
132    }
133
134    // invalidate the view's own bounds all the way up the view hierarchy
135    public static void invalidateGlobalRegion(View view) {
136        invalidateGlobalRegion(
137            view,
138            new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
139    }
140
141    // invalidate a rectangle relative to the view's coordinate system all the way up the view
142    // hierarchy
143    public static void invalidateGlobalRegion(View view, RectF childBounds) {
144        childBounds.offset(view.getX(), view.getY());
145        if (DEBUG_INVALIDATE)
146            Log.v(TAG, "-------------");
147        while (view.getParent() != null && view.getParent() instanceof View) {
148            view = (View) view.getParent();
149            view.getMatrix().mapRect(childBounds);
150            view.invalidate((int) Math.floor(childBounds.left),
151                            (int) Math.floor(childBounds.top),
152                            (int) Math.ceil(childBounds.right),
153                            (int) Math.ceil(childBounds.bottom));
154            if (DEBUG_INVALIDATE) {
155                Log.v(TAG, "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            }
160        }
161    }
162
163    public boolean onInterceptTouchEvent(MotionEvent ev) {
164        final int action = ev.getAction();
165
166        switch (action) {
167            case MotionEvent.ACTION_DOWN:
168                mDragging = false;
169                mCurrView = mCallback.getChildAtPosition(ev);
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) - getPos(mCurrView);
183                    }
184                }
185                break;
186            case MotionEvent.ACTION_UP:
187                mDragging = false;
188                mCurrView = null;
189                break;
190        }
191        return mDragging;
192    }
193
194    public void dismissChild(final View animView, float velocity) {
195        float newPos;
196        if (velocity < 0 || (velocity == 0 && getPos(animView) < 0)) {
197            newPos = -getSize(animView);
198        } else {
199            newPos = getSize(animView);
200        }
201        int duration = MAX_ESCAPE_ANIMATION_DURATION;
202        if (velocity != 0) {
203            duration = Math.min(duration,
204                                (int) (Math.abs(newPos - getPos(animView)) * 1000f / Math
205                                        .abs(velocity)));
206        }
207        ObjectAnimator anim = createTranslationAnimation(animView, newPos);
208        anim.setInterpolator(new LinearInterpolator());
209        anim.setDuration(duration);
210        anim.addListener(new AnimatorListener() {
211            public void onAnimationStart(Animator animation) {
212            }
213
214            public void onAnimationRepeat(Animator animation) {
215            }
216
217            public void onAnimationEnd(Animator animation) {
218                mCallback.onChildDismissed(animView);
219            }
220
221            public void onAnimationCancel(Animator animation) {
222                mCallback.onChildDismissed(animView);
223            }
224        });
225        anim.addUpdateListener(new AnimatorUpdateListener() {
226            public void onAnimationUpdate(ValueAnimator animation) {
227                if (FADE_OUT_DURING_SWIPE) {
228                    animView.setAlpha(getAlphaForOffset(animView, getContentSize(animView)));
229                }
230                invalidateGlobalRegion(animView);
231            }
232        });
233        anim.start();
234    }
235
236    public void snapChild(final View animView, float velocity) {
237        ObjectAnimator anim = createTranslationAnimation(animView, 0);
238        int duration = SNAP_ANIM_LEN;
239        anim.setDuration(duration);
240        anim.addUpdateListener(new AnimatorUpdateListener() {
241            public void onAnimationUpdate(ValueAnimator animation) {
242                if (FADE_OUT_DURING_SWIPE) {
243                    animView.setAlpha(getAlphaForOffset(animView, getContentSize(animView)));
244                }
245                invalidateGlobalRegion(animView);
246            }
247        });
248        anim.start();
249    }
250
251    public boolean onTouchEvent(MotionEvent ev) {
252        if (!mDragging) {
253            return false;
254        }
255
256        mVelocityTracker.addMovement(ev);
257        final int action = ev.getAction();
258        switch (action) {
259            case MotionEvent.ACTION_OUTSIDE:
260            case MotionEvent.ACTION_MOVE:
261                if (mCurrView != null) {
262                    float delta = getPos(ev) - mInitialTouchPos;
263                    // don't let items that can't be dismissed be dragged more than
264                    // maxScrollDistance
265                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
266                        float size = getSize(mCurrView);
267                        float maxScrollDistance = 0.15f * size;
268                        if (Math.abs(delta) >= size) {
269                            delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
270                        } else {
271                            delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
272                        }
273                    }
274                    setTranslation(mCurrView, delta);
275                    if (FADE_OUT_DURING_SWIPE) {
276                        mCurrView.setAlpha(getAlphaForOffset(mCurrView, getContentSize(mCurrView)));
277                    }
278                    invalidateGlobalRegion(mCurrView);
279                }
280                break;
281            case MotionEvent.ACTION_UP:
282            case MotionEvent.ACTION_CANCEL:
283                if (mCurrView != null) {
284                    float maxVelocity = 1000; // px/sec
285                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
286                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
287                    float velocity = getVelocity(mVelocityTracker);
288                    float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
289
290                    // Decide whether to dismiss the current view
291                    boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
292                            Math.abs(getPos(mCurrView)) > 0.4 * getSize(mCurrView);
293                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
294                            (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
295                            (velocity > 0) == (getPos(mCurrView) > 0);
296
297                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
298                            (childSwipedFastEnough || childSwipedFarEnough);
299
300                    if (dismissChild) {
301                        // flingadingy
302                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
303                    } else {
304                        // snappity
305                        snapChild(mCurrView, velocity);
306                    }
307                }
308                break;
309        }
310        return true;
311    }
312
313    public interface Callback {
314        View getChildAtPosition(MotionEvent ev);
315
316        View getChildContentView(View v);
317
318        boolean canChildBeDismissed(View v);
319
320        void onBeginDrag(View v);
321
322        void onChildDismissed(View v);
323    }
324}
325