SwipeHelper.java revision c6461ca5a0892d9b5a47649d49af69165e05b87f
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
200        if (velocity < 0
201                || (velocity == 0 && getTranslation(animView) < 0)
202                // if we use the Menu to dismiss an item in landscape, animate up
203                || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
204            newPos = -getSize(animView);
205        } else {
206            newPos = getSize(animView);
207        }
208        int duration = MAX_ESCAPE_ANIMATION_DURATION;
209        if (velocity != 0) {
210            duration = Math.min(duration,
211                                (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
212                                        .abs(velocity)));
213        }
214        ObjectAnimator anim = createTranslationAnimation(animView, newPos);
215        anim.setInterpolator(new LinearInterpolator());
216        anim.setDuration(duration);
217        anim.addListener(new AnimatorListener() {
218            public void onAnimationStart(Animator animation) {
219            }
220
221            public void onAnimationRepeat(Animator animation) {
222            }
223
224            public void onAnimationEnd(Animator animation) {
225                mCallback.onChildDismissed(view);
226            }
227
228            public void onAnimationCancel(Animator animation) {
229                mCallback.onChildDismissed(view);
230            }
231        });
232        anim.addUpdateListener(new AnimatorUpdateListener() {
233            public void onAnimationUpdate(ValueAnimator animation) {
234                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
235                    animView.setAlpha(getAlphaForOffset(animView));
236                }
237                invalidateGlobalRegion(animView);
238            }
239        });
240        anim.start();
241    }
242
243    public void snapChild(final View view, float velocity) {
244        final View animView = mCallback.getChildContentView(view);
245        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
246        ObjectAnimator anim = createTranslationAnimation(animView, 0);
247        int duration = SNAP_ANIM_LEN;
248        anim.setDuration(duration);
249        anim.addUpdateListener(new AnimatorUpdateListener() {
250            public void onAnimationUpdate(ValueAnimator animation) {
251                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
252                    animView.setAlpha(getAlphaForOffset(animView));
253                }
254                invalidateGlobalRegion(animView);
255            }
256        });
257        anim.start();
258    }
259
260    public boolean onTouchEvent(MotionEvent ev) {
261        if (!mDragging) {
262            return false;
263        }
264
265        mVelocityTracker.addMovement(ev);
266        final int action = ev.getAction();
267        switch (action) {
268            case MotionEvent.ACTION_OUTSIDE:
269            case MotionEvent.ACTION_MOVE:
270                if (mCurrView != null) {
271                    float delta = getPos(ev) - mInitialTouchPos;
272                    // don't let items that can't be dismissed be dragged more than
273                    // maxScrollDistance
274                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
275                        float size = getSize(mCurrAnimView);
276                        float maxScrollDistance = 0.15f * size;
277                        if (Math.abs(delta) >= size) {
278                            delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
279                        } else {
280                            delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
281                        }
282                    }
283                    setTranslation(mCurrAnimView, delta);
284                    if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
285                        mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
286                    }
287                    invalidateGlobalRegion(mCurrView);
288                }
289                break;
290            case MotionEvent.ACTION_UP:
291            case MotionEvent.ACTION_CANCEL:
292                if (mCurrView != null) {
293                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
294                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
295                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
296                    float velocity = getVelocity(mVelocityTracker);
297                    float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
298
299                    // Decide whether to dismiss the current view
300                    boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
301                            Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
302                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
303                            (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
304                            (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
305
306                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
307                            (childSwipedFastEnough || childSwipedFarEnough);
308
309                    if (dismissChild) {
310                        // flingadingy
311                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
312                    } else {
313                        // snappity
314                        mCallback.onDragCancelled(mCurrView);
315                        snapChild(mCurrView, velocity);
316                    }
317                }
318                break;
319        }
320        return true;
321    }
322
323    public interface Callback {
324        View getChildAtPosition(MotionEvent ev);
325
326        View getChildContentView(View v);
327
328        boolean canChildBeDismissed(View v);
329
330        void onBeginDrag(View v);
331
332        void onChildDismissed(View v);
333
334        void onDragCancelled(View v);
335    }
336}
337