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