SwipeHelper.java revision 2f2022afa1eb85018368398bd150e9575fc099c9
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.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.Animator.AnimatorListener;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.graphics.RectF;
26import android.util.Log;
27import android.view.animation.LinearInterpolator;
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31
32public 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
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
78    public void setDensityScale(float densityScale) {
79        mDensityScale = densityScale;
80    }
81
82    public void setPagingTouchSlop(float pagingTouchSlop) {
83        mPagingTouchSlop = pagingTouchSlop;
84    }
85
86    private float getPos(MotionEvent ev) {
87        return mSwipeDirection == X ? ev.getX() : ev.getY();
88    }
89
90    private float getTranslation(View v) {
91        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
92    }
93
94    private float getVelocity(VelocityTracker vt) {
95        return mSwipeDirection == X ? vt.getXVelocity() :
96                vt.getYVelocity();
97    }
98
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
105    private float getPerpendicularVelocity(VelocityTracker vt) {
106        return mSwipeDirection == X ? vt.getYVelocity() :
107                vt.getXVelocity();
108    }
109
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
118    private float getSize(View v) {
119        return mSwipeDirection == X ? v.getMeasuredWidth() :
120                v.getMeasuredHeight();
121    }
122
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
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
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
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     */
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
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
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 {
333        View getChildAtPosition(MotionEvent ev);
334
335        View getChildContentView(View v);
336
337        boolean canChildBeDismissed(View v);
338
339        void onBeginDrag(View v);
340
341        void onChildDismissed(View v);
342
343        void onDragCancelled(View v);
344    }
345}
346