SwipeHelper.java revision 469e96e206f8df44b32ce393f8d19f0cae730030
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.os.Handler;
27import android.util.Log;
28import android.view.accessibility.AccessibilityEvent;
29import android.view.animation.LinearInterpolator;
30import android.view.MotionEvent;
31import android.view.VelocityTracker;
32import android.view.View;
33import android.view.ViewConfiguration;
34
35public class SwipeHelper implements Gefingerpoken {
36    static final String TAG = "com.android.systemui.SwipeHelper";
37    private static final boolean DEBUG = false;
38    private static final boolean DEBUG_INVALIDATE = false;
39    private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
40    private static final boolean CONSTRAIN_SWIPE = true;
41    private static final boolean FADE_OUT_DURING_SWIPE = true;
42    private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
43
44    public static final int X = 0;
45    public static final int Y = 1;
46
47    private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
48
49    private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
50    private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
51    private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
52    private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
53    private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
54
55    public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
56                                                 // where fade starts
57    static final float ALPHA_FADE_END = 0.5f; // fraction of thumbnail width
58                                              // beyond which alpha->0
59    private float mMinAlpha = 0f;
60
61    private float mPagingTouchSlop;
62    private Callback mCallback;
63    private Handler mHandler;
64    private int mSwipeDirection;
65    private VelocityTracker mVelocityTracker;
66
67    private float mInitialTouchPos;
68    private boolean mDragging;
69    private View mCurrView;
70    private View mCurrAnimView;
71    private boolean mCanCurrViewBeDimissed;
72    private float mDensityScale;
73
74    private boolean mLongPressSent;
75    private View.OnLongClickListener mLongPressListener;
76    private Runnable mWatchLongPress;
77    private long mLongPressTimeout;
78
79    public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
80            float pagingTouchSlop) {
81        mCallback = callback;
82        mHandler = new Handler();
83        mSwipeDirection = swipeDirection;
84        mVelocityTracker = VelocityTracker.obtain();
85        mDensityScale = densityScale;
86        mPagingTouchSlop = pagingTouchSlop;
87
88        mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press!
89    }
90
91    public void setLongPressListener(View.OnLongClickListener listener) {
92        mLongPressListener = listener;
93    }
94
95    public void setDensityScale(float densityScale) {
96        mDensityScale = densityScale;
97    }
98
99    public void setPagingTouchSlop(float pagingTouchSlop) {
100        mPagingTouchSlop = pagingTouchSlop;
101    }
102
103    private float getPos(MotionEvent ev) {
104        return mSwipeDirection == X ? ev.getX() : ev.getY();
105    }
106
107    private float getTranslation(View v) {
108        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
109    }
110
111    private float getVelocity(VelocityTracker vt) {
112        return mSwipeDirection == X ? vt.getXVelocity() :
113                vt.getYVelocity();
114    }
115
116    private ObjectAnimator createTranslationAnimation(View v, float newPos) {
117        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
118                mSwipeDirection == X ? "translationX" : "translationY", newPos);
119        return anim;
120    }
121
122    private float getPerpendicularVelocity(VelocityTracker vt) {
123        return mSwipeDirection == X ? vt.getYVelocity() :
124                vt.getXVelocity();
125    }
126
127    private void setTranslation(View v, float translate) {
128        if (mSwipeDirection == X) {
129            v.setTranslationX(translate);
130        } else {
131            v.setTranslationY(translate);
132        }
133    }
134
135    private float getSize(View v) {
136        return mSwipeDirection == X ? v.getMeasuredWidth() :
137                v.getMeasuredHeight();
138    }
139
140    public void setMinAlpha(float minAlpha) {
141        mMinAlpha = minAlpha;
142    }
143
144    private float getAlphaForOffset(View view) {
145        float viewSize = getSize(view);
146        final float fadeSize = ALPHA_FADE_END * viewSize;
147        float result = 1.0f;
148        float pos = getTranslation(view);
149        if (pos >= viewSize * ALPHA_FADE_START) {
150            result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
151        } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
152            result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
153        }
154        return Math.max(mMinAlpha, result);
155    }
156
157    // invalidate the view's own bounds all the way up the view hierarchy
158    public static void invalidateGlobalRegion(View view) {
159        invalidateGlobalRegion(
160            view,
161            new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
162    }
163
164    // invalidate a rectangle relative to the view's coordinate system all the way up the view
165    // hierarchy
166    public static void invalidateGlobalRegion(View view, RectF childBounds) {
167        //childBounds.offset(view.getTranslationX(), view.getTranslationY());
168        if (DEBUG_INVALIDATE)
169            Log.v(TAG, "-------------");
170        while (view.getParent() != null && view.getParent() instanceof View) {
171            view = (View) view.getParent();
172            view.getMatrix().mapRect(childBounds);
173            view.invalidate((int) Math.floor(childBounds.left),
174                            (int) Math.floor(childBounds.top),
175                            (int) Math.ceil(childBounds.right),
176                            (int) Math.ceil(childBounds.bottom));
177            if (DEBUG_INVALIDATE) {
178                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
179                        + "," + (int) Math.floor(childBounds.top)
180                        + "," + (int) Math.ceil(childBounds.right)
181                        + "," + (int) Math.ceil(childBounds.bottom));
182            }
183        }
184    }
185
186    public void removeLongPressCallback() {
187        if (mWatchLongPress != null) {
188            mHandler.removeCallbacks(mWatchLongPress);
189        }
190    }
191
192    public boolean onInterceptTouchEvent(MotionEvent ev) {
193        final int action = ev.getAction();
194
195        switch (action) {
196            case MotionEvent.ACTION_DOWN:
197                mDragging = false;
198                mLongPressSent = false;
199                mCurrView = mCallback.getChildAtPosition(ev);
200                mVelocityTracker.clear();
201                if (mCurrView != null) {
202                    mCurrAnimView = mCallback.getChildContentView(mCurrView);
203                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
204                    mVelocityTracker.addMovement(ev);
205                    mInitialTouchPos = getPos(ev);
206
207                    if (mLongPressListener != null) {
208                        if (mWatchLongPress == null) {
209                            mWatchLongPress = new Runnable() {
210                                @Override
211                                public void run() {
212                                    if (mCurrView != null && !mLongPressSent) {
213                                        mLongPressSent = true;
214                                        mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
215                                        mLongPressListener.onLongClick(mCurrView);
216                                    }
217                                }
218                            };
219                        }
220                        mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
221                    }
222
223                }
224                break;
225
226            case MotionEvent.ACTION_MOVE:
227                if (mCurrView != null && !mLongPressSent) {
228                    mVelocityTracker.addMovement(ev);
229                    float pos = getPos(ev);
230                    float delta = pos - mInitialTouchPos;
231                    if (Math.abs(delta) > mPagingTouchSlop) {
232                        mCallback.onBeginDrag(mCurrView);
233                        mDragging = true;
234                        mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
235
236                        removeLongPressCallback();
237                    }
238                }
239
240                break;
241
242            case MotionEvent.ACTION_UP:
243            case MotionEvent.ACTION_CANCEL:
244                mDragging = false;
245                mCurrView = null;
246                mCurrAnimView = null;
247                mLongPressSent = false;
248                break;
249        }
250        return mDragging;
251    }
252
253    /**
254     * @param view The view to be dismissed
255     * @param velocity The desired pixels/second speed at which the view should move
256     */
257    public void dismissChild(final View view, float velocity) {
258        final View animView = mCallback.getChildContentView(view);
259        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
260        float newPos;
261
262        if (velocity < 0
263                || (velocity == 0 && getTranslation(animView) < 0)
264                // if we use the Menu to dismiss an item in landscape, animate up
265                || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
266            newPos = -getSize(animView);
267        } else {
268            newPos = getSize(animView);
269        }
270        int duration = MAX_ESCAPE_ANIMATION_DURATION;
271        if (velocity != 0) {
272            duration = Math.min(duration,
273                                (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
274                                        .abs(velocity)));
275        } else {
276            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
277        }
278
279        animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
280        ObjectAnimator anim = createTranslationAnimation(animView, newPos);
281        anim.setInterpolator(sLinearInterpolator);
282        anim.setDuration(duration);
283        anim.addListener(new AnimatorListenerAdapter() {
284            public void onAnimationEnd(Animator animation) {
285                mCallback.onChildDismissed(view);
286                animView.setLayerType(View.LAYER_TYPE_NONE, null);
287            }
288        });
289        anim.addUpdateListener(new AnimatorUpdateListener() {
290            public void onAnimationUpdate(ValueAnimator animation) {
291                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
292                    animView.setAlpha(getAlphaForOffset(animView));
293                }
294                invalidateGlobalRegion(animView);
295            }
296        });
297        anim.start();
298    }
299
300    public void snapChild(final View view, float velocity) {
301        final View animView = mCallback.getChildContentView(view);
302        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
303        ObjectAnimator anim = createTranslationAnimation(animView, 0);
304        int duration = SNAP_ANIM_LEN;
305        anim.setDuration(duration);
306        anim.addUpdateListener(new AnimatorUpdateListener() {
307            public void onAnimationUpdate(ValueAnimator animation) {
308                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
309                    animView.setAlpha(getAlphaForOffset(animView));
310                }
311                invalidateGlobalRegion(animView);
312            }
313        });
314        anim.start();
315    }
316
317    public boolean onTouchEvent(MotionEvent ev) {
318        if (mLongPressSent) {
319            return true;
320        }
321
322        if (!mDragging) {
323            return false;
324        }
325
326        mVelocityTracker.addMovement(ev);
327        final int action = ev.getAction();
328        switch (action) {
329            case MotionEvent.ACTION_OUTSIDE:
330            case MotionEvent.ACTION_MOVE:
331                if (mCurrView != null) {
332                    float delta = getPos(ev) - mInitialTouchPos;
333                    // don't let items that can't be dismissed be dragged more than
334                    // maxScrollDistance
335                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
336                        float size = getSize(mCurrAnimView);
337                        float maxScrollDistance = 0.15f * size;
338                        if (Math.abs(delta) >= size) {
339                            delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
340                        } else {
341                            delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
342                        }
343                    }
344                    setTranslation(mCurrAnimView, delta);
345                    if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
346                        mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
347                    }
348                    invalidateGlobalRegion(mCurrView);
349                }
350                break;
351            case MotionEvent.ACTION_UP:
352            case MotionEvent.ACTION_CANCEL:
353                if (mCurrView != null) {
354                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
355                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
356                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
357                    float velocity = getVelocity(mVelocityTracker);
358                    float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
359
360                    // Decide whether to dismiss the current view
361                    boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
362                            Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
363                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
364                            (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
365                            (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
366
367                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
368                            (childSwipedFastEnough || childSwipedFarEnough);
369
370                    if (dismissChild) {
371                        // flingadingy
372                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
373                    } else {
374                        // snappity
375                        mCallback.onDragCancelled(mCurrView);
376                        snapChild(mCurrView, velocity);
377                    }
378                }
379                break;
380        }
381        return true;
382    }
383
384    public interface Callback {
385        View getChildAtPosition(MotionEvent ev);
386
387        View getChildContentView(View v);
388
389        boolean canChildBeDismissed(View v);
390
391        void onBeginDrag(View v);
392
393        void onChildDismissed(View v);
394
395        void onDragCancelled(View v);
396    }
397}
398