SwipeHelper.java revision 28f0e5932944d9abc4b6879b1d05523e9341c385
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.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.content.Context;
25import android.graphics.RectF;
26import android.os.Handler;
27import android.util.Log;
28import android.view.MotionEvent;
29import android.view.VelocityTracker;
30import android.view.View;
31import android.view.ViewConfiguration;
32import android.view.accessibility.AccessibilityEvent;
33import android.view.animation.AnimationUtils;
34import android.view.animation.Interpolator;
35import android.view.animation.LinearInterpolator;
36
37public class SwipeHelper implements Gefingerpoken {
38    static final String TAG = "com.android.systemui.SwipeHelper";
39    private static final boolean DEBUG = false;
40    private static final boolean DEBUG_INVALIDATE = false;
41    private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
42    private static final boolean CONSTRAIN_SWIPE = true;
43    private static final boolean FADE_OUT_DURING_SWIPE = true;
44    private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
45
46    public static final int X = 0;
47    public static final int Y = 1;
48
49    private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
50    private final Interpolator mFastOutLinearInInterpolator;
51
52    private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
53    private int DEFAULT_ESCAPE_ANIMATION_DURATION = 200; // ms
54    private int MAX_ESCAPE_ANIMATION_DURATION = 400; // ms
55    private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
56    private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 150; // ms
57
58    public static float SWIPE_PROGRESS_FADE_START = 0f; // fraction of thumbnail width
59                                                 // where fade starts
60    static final float SWIPE_PROGRESS_FADE_END = 0.5f; // fraction of thumbnail width
61                                              // beyond which swipe progress->0
62    private float mMinSwipeProgress = 0f;
63    private float mMaxSwipeProgress = 1f;
64
65    private float mPagingTouchSlop;
66    private Callback mCallback;
67    private Handler mHandler;
68    private int mSwipeDirection;
69    private VelocityTracker mVelocityTracker;
70
71    private float mInitialTouchPos;
72    private boolean mDragging;
73    private View mCurrView;
74    private View mCurrAnimView;
75    private boolean mCanCurrViewBeDimissed;
76    private float mDensityScale;
77
78    private boolean mLongPressSent;
79    private LongPressListener mLongPressListener;
80    private Runnable mWatchLongPress;
81    private long mLongPressTimeout;
82
83    final private int[] mTmpPos = new int[2];
84
85    public SwipeHelper(int swipeDirection, Callback callback, Context context) {
86        mCallback = callback;
87        mHandler = new Handler();
88        mSwipeDirection = swipeDirection;
89        mVelocityTracker = VelocityTracker.obtain();
90        mDensityScale =  context.getResources().getDisplayMetrics().density;
91        mPagingTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
92
93        mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); // extra long-press!
94        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
95                android.R.interpolator.fast_out_linear_in);
96    }
97
98    public void setLongPressListener(LongPressListener listener) {
99        mLongPressListener = listener;
100    }
101
102    public void setDensityScale(float densityScale) {
103        mDensityScale = densityScale;
104    }
105
106    public void setPagingTouchSlop(float pagingTouchSlop) {
107        mPagingTouchSlop = pagingTouchSlop;
108    }
109
110    private float getPos(MotionEvent ev) {
111        return mSwipeDirection == X ? ev.getX() : ev.getY();
112    }
113
114    private float getTranslation(View v) {
115        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
116    }
117
118    private float getVelocity(VelocityTracker vt) {
119        return mSwipeDirection == X ? vt.getXVelocity() :
120                vt.getYVelocity();
121    }
122
123    private ObjectAnimator createTranslationAnimation(View v, float newPos) {
124        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
125                mSwipeDirection == X ? "translationX" : "translationY", newPos);
126        return anim;
127    }
128
129    private float getPerpendicularVelocity(VelocityTracker vt) {
130        return mSwipeDirection == X ? vt.getYVelocity() :
131                vt.getXVelocity();
132    }
133
134    private void setTranslation(View v, float translate) {
135        if (mSwipeDirection == X) {
136            v.setTranslationX(translate);
137        } else {
138            v.setTranslationY(translate);
139        }
140    }
141
142    private float getSize(View v) {
143        return mSwipeDirection == X ? v.getMeasuredWidth() :
144                v.getMeasuredHeight();
145    }
146
147    public void setMinSwipeProgress(float minSwipeProgress) {
148        mMinSwipeProgress = minSwipeProgress;
149    }
150
151    public void setMaxSwipeProgress(float maxSwipeProgress) {
152        mMaxSwipeProgress = maxSwipeProgress;
153    }
154
155    private float getSwipeProgressForOffset(View view) {
156        float viewSize = getSize(view);
157        final float fadeSize = SWIPE_PROGRESS_FADE_END * viewSize;
158        float result = 1.0f;
159        float pos = getTranslation(view);
160        if (pos >= viewSize * SWIPE_PROGRESS_FADE_START) {
161            result = 1.0f - (pos - viewSize * SWIPE_PROGRESS_FADE_START) / fadeSize;
162        } else if (pos < viewSize * (1.0f - SWIPE_PROGRESS_FADE_START)) {
163            result = 1.0f + (viewSize * SWIPE_PROGRESS_FADE_START + pos) / fadeSize;
164        }
165        return Math.min(Math.max(mMinSwipeProgress, result), mMaxSwipeProgress);
166    }
167
168    private void updateSwipeProgressFromOffset(View animView, boolean dismissable) {
169        float swipeProgress = getSwipeProgressForOffset(animView);
170        if (!mCallback.updateSwipeProgress(animView, dismissable, swipeProgress)) {
171            if (FADE_OUT_DURING_SWIPE && dismissable) {
172                float alpha = swipeProgress;
173                if (alpha != 0f && alpha != 1f) {
174                    animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
175                } else {
176                    animView.setLayerType(View.LAYER_TYPE_NONE, null);
177                }
178                animView.setAlpha(getSwipeProgressForOffset(animView));
179            }
180        }
181        invalidateGlobalRegion(animView);
182    }
183
184    // invalidate the view's own bounds all the way up the view hierarchy
185    public static void invalidateGlobalRegion(View view) {
186        invalidateGlobalRegion(
187            view,
188            new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
189    }
190
191    // invalidate a rectangle relative to the view's coordinate system all the way up the view
192    // hierarchy
193    public static void invalidateGlobalRegion(View view, RectF childBounds) {
194        //childBounds.offset(view.getTranslationX(), view.getTranslationY());
195        if (DEBUG_INVALIDATE)
196            Log.v(TAG, "-------------");
197        while (view.getParent() != null && view.getParent() instanceof View) {
198            view = (View) view.getParent();
199            view.getMatrix().mapRect(childBounds);
200            view.invalidate((int) Math.floor(childBounds.left),
201                            (int) Math.floor(childBounds.top),
202                            (int) Math.ceil(childBounds.right),
203                            (int) Math.ceil(childBounds.bottom));
204            if (DEBUG_INVALIDATE) {
205                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
206                        + "," + (int) Math.floor(childBounds.top)
207                        + "," + (int) Math.ceil(childBounds.right)
208                        + "," + (int) Math.ceil(childBounds.bottom));
209            }
210        }
211    }
212
213    public void removeLongPressCallback() {
214        if (mWatchLongPress != null) {
215            mHandler.removeCallbacks(mWatchLongPress);
216            mWatchLongPress = null;
217        }
218    }
219
220    public boolean onInterceptTouchEvent(final MotionEvent ev) {
221        final int action = ev.getAction();
222
223        switch (action) {
224            case MotionEvent.ACTION_DOWN:
225                mDragging = false;
226                mLongPressSent = false;
227                mCurrView = mCallback.getChildAtPosition(ev);
228                mVelocityTracker.clear();
229                if (mCurrView != null) {
230                    mCurrAnimView = mCallback.getChildContentView(mCurrView);
231                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
232                    mVelocityTracker.addMovement(ev);
233                    mInitialTouchPos = getPos(ev);
234
235                    if (mLongPressListener != null) {
236                        if (mWatchLongPress == null) {
237                            mWatchLongPress = new Runnable() {
238                                @Override
239                                public void run() {
240                                    if (mCurrView != null && !mLongPressSent) {
241                                        mLongPressSent = true;
242                                        mCurrView.sendAccessibilityEvent(
243                                                AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
244                                        mCurrView.getLocationOnScreen(mTmpPos);
245                                        final int x = (int) ev.getRawX() - mTmpPos[0];
246                                        final int y = (int) ev.getRawY() - mTmpPos[1];
247                                        mLongPressListener.onLongPress(mCurrView, x, y);
248                                    }
249                                }
250                            };
251                        }
252                        mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
253                    }
254
255                }
256                break;
257
258            case MotionEvent.ACTION_MOVE:
259                if (mCurrView != null && !mLongPressSent) {
260                    mVelocityTracker.addMovement(ev);
261                    float pos = getPos(ev);
262                    float delta = pos - mInitialTouchPos;
263                    if (Math.abs(delta) > mPagingTouchSlop) {
264                        mCallback.onBeginDrag(mCurrView);
265                        mDragging = true;
266                        mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
267
268                        removeLongPressCallback();
269                    }
270                }
271
272                break;
273
274            case MotionEvent.ACTION_UP:
275            case MotionEvent.ACTION_CANCEL:
276                final boolean captured = (mDragging || mLongPressSent);
277                mDragging = false;
278                mCurrView = null;
279                mCurrAnimView = null;
280                mLongPressSent = false;
281                removeLongPressCallback();
282                if (captured) return true;
283                break;
284        }
285        return mDragging || mLongPressSent;
286    }
287
288    /**
289     * @param view The view to be dismissed
290     * @param velocity The desired pixels/second speed at which the view should move
291     */
292    public void dismissChild(final View view, float velocity) {
293        dismissChild(view, velocity, null, 0, false, 0);
294    }
295
296    /**
297     * @param view The view to be dismissed
298     * @param velocity The desired pixels/second speed at which the view should move
299     * @param endAction The action to perform at the end
300     * @param delay The delay after which we should start
301     * @param useAccelerateInterpolator Should an accelerating Interpolator be used
302     * @param fixedDuration If not 0, this exact duration will be taken
303     */
304    public void dismissChild(final View view, float velocity, final Runnable endAction,
305            long delay, boolean useAccelerateInterpolator, long fixedDuration) {
306        final View animView = mCallback.getChildContentView(view);
307        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
308        float newPos;
309
310        if (velocity < 0
311                || (velocity == 0 && getTranslation(animView) < 0)
312                // if we use the Menu to dismiss an item in landscape, animate up
313                || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
314            newPos = -getSize(animView);
315        } else {
316            newPos = getSize(animView);
317        }
318        long duration;
319        if (fixedDuration == 0) {
320            duration = MAX_ESCAPE_ANIMATION_DURATION;
321            if (velocity != 0) {
322                duration = Math.min(duration,
323                        (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
324                                .abs(velocity))
325                );
326            } else {
327                duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
328            }
329        } else {
330            duration = fixedDuration;
331        }
332
333        animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
334        ObjectAnimator anim = createTranslationAnimation(animView, newPos);
335        if (useAccelerateInterpolator) {
336            anim.setInterpolator(mFastOutLinearInInterpolator);
337        } else {
338            anim.setInterpolator(sLinearInterpolator);
339        }
340        anim.setDuration(duration);
341        if (delay > 0) {
342            anim.setStartDelay(delay);
343        }
344        anim.addListener(new AnimatorListenerAdapter() {
345            public void onAnimationEnd(Animator animation) {
346                mCallback.onChildDismissed(view);
347                if (endAction != null) {
348                    endAction.run();
349                }
350                animView.setLayerType(View.LAYER_TYPE_NONE, null);
351            }
352        });
353        anim.addUpdateListener(new AnimatorUpdateListener() {
354            public void onAnimationUpdate(ValueAnimator animation) {
355                updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
356            }
357        });
358        anim.start();
359    }
360
361    public void snapChild(final View view, float velocity) {
362        final View animView = mCallback.getChildContentView(view);
363        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
364        ObjectAnimator anim = createTranslationAnimation(animView, 0);
365        int duration = SNAP_ANIM_LEN;
366        anim.setDuration(duration);
367        anim.addUpdateListener(new AnimatorUpdateListener() {
368            public void onAnimationUpdate(ValueAnimator animation) {
369                updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
370            }
371        });
372        anim.addListener(new AnimatorListenerAdapter() {
373            public void onAnimationEnd(Animator animator) {
374                updateSwipeProgressFromOffset(animView, canAnimViewBeDismissed);
375                mCallback.onChildSnappedBack(animView);
376            }
377        });
378        anim.start();
379    }
380
381    public boolean onTouchEvent(MotionEvent ev) {
382        if (mLongPressSent) {
383            return true;
384        }
385
386        if (!mDragging) {
387            if (mCallback.getChildAtPosition(ev) != null) {
388
389                // We are dragging directly over a card, make sure that we also catch the gesture
390                // even if nobody else wants the touch event.
391                onInterceptTouchEvent(ev);
392                return true;
393            } else {
394
395                // We are not doing anything, make sure the long press callback
396                // is not still ticking like a bomb waiting to go off.
397                removeLongPressCallback();
398                return false;
399            }
400        }
401
402        mVelocityTracker.addMovement(ev);
403        final int action = ev.getAction();
404        switch (action) {
405            case MotionEvent.ACTION_OUTSIDE:
406            case MotionEvent.ACTION_MOVE:
407                if (mCurrView != null) {
408                    float delta = getPos(ev) - mInitialTouchPos;
409                    // don't let items that can't be dismissed be dragged more than
410                    // maxScrollDistance
411                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
412                        float size = getSize(mCurrAnimView);
413                        float maxScrollDistance = 0.15f * size;
414                        if (Math.abs(delta) >= size) {
415                            delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
416                        } else {
417                            delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
418                        }
419                    }
420                    setTranslation(mCurrAnimView, delta);
421
422                    updateSwipeProgressFromOffset(mCurrAnimView, mCanCurrViewBeDimissed);
423                }
424                break;
425            case MotionEvent.ACTION_UP:
426            case MotionEvent.ACTION_CANCEL:
427                if (mCurrView != null) {
428                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
429                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
430                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
431                    float velocity = getVelocity(mVelocityTracker);
432                    float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
433
434                    // Decide whether to dismiss the current view
435                    boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
436                            Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
437                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
438                            (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
439                            (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
440
441                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
442                            (childSwipedFastEnough || childSwipedFarEnough);
443
444                    if (dismissChild) {
445                        // flingadingy
446                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
447                    } else {
448                        // snappity
449                        mCallback.onDragCancelled(mCurrView);
450                        snapChild(mCurrView, velocity);
451                    }
452                }
453                break;
454        }
455        return true;
456    }
457
458    public interface Callback {
459        View getChildAtPosition(MotionEvent ev);
460
461        View getChildContentView(View v);
462
463        boolean canChildBeDismissed(View v);
464
465        void onBeginDrag(View v);
466
467        void onChildDismissed(View v);
468
469        void onDragCancelled(View v);
470
471        void onChildSnappedBack(View animView);
472
473        /**
474         * Updates the swipe progress on a child.
475         *
476         * @return if true, prevents the default alpha fading.
477         */
478        boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress);
479    }
480
481    /**
482     * Equivalent to View.OnLongClickListener with coordinates
483     */
484    public interface LongPressListener {
485        /**
486         * Equivalent to {@link View.OnLongClickListener#onLongClick(View)} with coordinates
487         * @return whether the longpress was handled
488         */
489        boolean onLongPress(View v, int x, int y);
490    }
491}
492