SwipeHelper.java revision ebfc6981828b0699eef85c58b23a61f2cac41af3
1/*
2 * Copyright (C) 2014 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.recents.views;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.annotation.TargetApi;
25import android.os.Build;
26import android.util.DisplayMetrics;
27import android.view.MotionEvent;
28import android.view.VelocityTracker;
29import android.view.View;
30import android.view.animation.LinearInterpolator;
31import com.android.systemui.recents.RecentsConfiguration;
32
33/**
34 * This class facilitates swipe to dismiss. It defines an interface to be implemented by the
35 * by the class hosting the views that need to swiped, and, using this interface, handles touch
36 * events and translates / fades / animates the view as it is dismissed.
37 */
38public class SwipeHelper {
39    static final String TAG = "SwipeHelper";
40    private static final boolean SLOW_ANIMATIONS = false; // DEBUG;
41    private static final boolean CONSTRAIN_SWIPE = true;
42    private static final boolean FADE_OUT_DURING_SWIPE = true;
43    private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
44
45    public static final int X = 0;
46    public static final int Y = 1;
47
48    private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
49
50    private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec
51    private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms
52    private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms
53    private int MAX_DISMISS_VELOCITY = 2000; // dp/sec
54    private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms
55
56    public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width
57                                                 // where fade starts
58    static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width
59                                              // beyond which alpha->0
60    private float mMinAlpha = 0f;
61
62    private float mPagingTouchSlop;
63    Callback mCallback;
64    private int mSwipeDirection;
65    private VelocityTracker mVelocityTracker;
66
67    private float mInitialTouchPos;
68    private boolean mDragging;
69
70    private View mCurrView;
71    private boolean mCanCurrViewBeDimissed;
72    private float mDensityScale;
73
74    public boolean mAllowSwipeTowardsStart = true;
75    public boolean mAllowSwipeTowardsEnd = true;
76    private boolean mRtl;
77
78    public SwipeHelper(int swipeDirection, Callback callback, float densityScale,
79            float pagingTouchSlop) {
80        mCallback = callback;
81        mSwipeDirection = swipeDirection;
82        mVelocityTracker = VelocityTracker.obtain();
83        mDensityScale = densityScale;
84        mPagingTouchSlop = pagingTouchSlop;
85    }
86
87    public void setDensityScale(float densityScale) {
88        mDensityScale = densityScale;
89    }
90
91    public void setPagingTouchSlop(float pagingTouchSlop) {
92        mPagingTouchSlop = pagingTouchSlop;
93    }
94
95    public void cancelOngoingDrag() {
96        if (mDragging) {
97            if (mCurrView != null) {
98                mCallback.onDragCancelled(mCurrView);
99                setTranslation(mCurrView, 0);
100                mCallback.onSnapBackCompleted(mCurrView);
101                mCurrView = null;
102            }
103            mDragging = false;
104        }
105    }
106
107    public void resetTranslation(View v) {
108        setTranslation(v, 0);
109    }
110
111    private float getPos(MotionEvent ev) {
112        return mSwipeDirection == X ? ev.getX() : ev.getY();
113    }
114
115    private float getTranslation(View v) {
116        return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY();
117    }
118
119    private float getVelocity(VelocityTracker vt) {
120        return mSwipeDirection == X ? vt.getXVelocity() :
121                vt.getYVelocity();
122    }
123
124    private ObjectAnimator createTranslationAnimation(View v, float newPos) {
125        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
126                mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos);
127        return anim;
128    }
129
130    private float getPerpendicularVelocity(VelocityTracker vt) {
131        return mSwipeDirection == X ? vt.getYVelocity() :
132                vt.getXVelocity();
133    }
134
135    private void setTranslation(View v, float translate) {
136        if (mSwipeDirection == X) {
137            v.setTranslationX(translate);
138        } else {
139            v.setTranslationY(translate);
140        }
141    }
142
143    private float getSize(View v) {
144        final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics();
145        return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels;
146    }
147
148    public void setMinAlpha(float minAlpha) {
149        mMinAlpha = minAlpha;
150    }
151
152    float getAlphaForOffset(View view) {
153        float viewSize = getSize(view);
154        final float fadeSize = ALPHA_FADE_END * viewSize;
155        float result = 1.0f;
156        float pos = getTranslation(view);
157        if (pos >= viewSize * ALPHA_FADE_START) {
158            result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
159        } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
160            result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
161        }
162        result = Math.min(result, 1.0f);
163        result = Math.max(result, 0f);
164        return Math.max(mMinAlpha, result);
165    }
166
167    /**
168     * Determines whether the given view has RTL layout.
169     */
170    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
171    public static boolean isLayoutRtl(View view) {
172        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
173            return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection();
174        } else {
175            return false;
176        }
177    }
178
179    public boolean onInterceptTouchEvent(MotionEvent ev) {
180        final int action = ev.getAction();
181
182        switch (action) {
183            case MotionEvent.ACTION_DOWN:
184                mDragging = false;
185                mCurrView = mCallback.getChildAtPosition(ev);
186                mVelocityTracker.clear();
187                if (mCurrView != null) {
188                    mRtl = isLayoutRtl(mCurrView);
189                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
190                    mVelocityTracker.addMovement(ev);
191                    mInitialTouchPos = getPos(ev);
192                } else {
193                    mCanCurrViewBeDimissed = false;
194                }
195                break;
196            case MotionEvent.ACTION_MOVE:
197                if (mCurrView != null) {
198                    mVelocityTracker.addMovement(ev);
199                    float pos = getPos(ev);
200                    float delta = pos - mInitialTouchPos;
201                    if (Math.abs(delta) > mPagingTouchSlop) {
202                        mCallback.onBeginDrag(mCurrView);
203                        mDragging = true;
204                        mInitialTouchPos = pos - getTranslation(mCurrView);
205                    }
206                }
207                break;
208            case MotionEvent.ACTION_UP:
209            case MotionEvent.ACTION_CANCEL:
210                mDragging = false;
211                mCurrView = null;
212                break;
213        }
214        return mDragging;
215    }
216
217    /**
218     * @param view The view to be dismissed
219     * @param velocity The desired pixels/second speed at which the view should move
220     */
221    private void dismissChild(final View view, float velocity) {
222        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
223        float newPos;
224        if (velocity < 0
225                || (velocity == 0 && getTranslation(view) < 0)
226                // if we use the Menu to dismiss an item in landscape, animate up
227                || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) {
228            newPos = -getSize(view);
229        } else {
230            newPos = getSize(view);
231        }
232        int duration = MAX_ESCAPE_ANIMATION_DURATION;
233        if (velocity != 0) {
234            duration = Math.min(duration,
235                                (int) (Math.abs(newPos - getTranslation(view)) *
236                                        1000f / Math.abs(velocity)));
237        } else {
238            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
239        }
240
241        ValueAnimator anim = createTranslationAnimation(view, newPos);
242        anim.setInterpolator(sLinearInterpolator);
243        anim.setDuration(duration);
244        anim.addListener(new AnimatorListenerAdapter() {
245            @Override
246            public void onAnimationEnd(Animator animation) {
247                mCallback.onChildDismissed(view);
248                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
249                    view.setAlpha(1.f);
250                }
251            }
252        });
253        anim.addUpdateListener(new AnimatorUpdateListener() {
254            @Override
255            public void onAnimationUpdate(ValueAnimator animation) {
256                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
257                    view.setAlpha(getAlphaForOffset(view));
258                }
259            }
260        });
261        anim.start();
262    }
263
264    private void snapChild(final View view, float velocity) {
265        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
266        ValueAnimator anim = createTranslationAnimation(view, 0);
267        int duration = SNAP_ANIM_LEN;
268        anim.setDuration(duration);
269        anim.setInterpolator(RecentsConfiguration.getInstance().linearOutSlowInInterpolator);
270        anim.addUpdateListener(new AnimatorUpdateListener() {
271            @Override
272            public void onAnimationUpdate(ValueAnimator animation) {
273                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
274                    view.setAlpha(getAlphaForOffset(view));
275                }
276                mCallback.onSwipeChanged(mCurrView, view.getTranslationX());
277            }
278        });
279        anim.addListener(new AnimatorListenerAdapter() {
280            @Override
281            public void onAnimationEnd(Animator animation) {
282                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
283                    view.setAlpha(1.0f);
284                }
285                mCallback.onSnapBackCompleted(view);
286            }
287        });
288        anim.start();
289    }
290
291    public boolean onTouchEvent(MotionEvent ev) {
292        if (!mDragging) {
293            if (!onInterceptTouchEvent(ev)) {
294                return mCanCurrViewBeDimissed;
295            }
296        }
297
298        mVelocityTracker.addMovement(ev);
299        final int action = ev.getAction();
300        switch (action) {
301            case MotionEvent.ACTION_OUTSIDE:
302            case MotionEvent.ACTION_MOVE:
303                if (mCurrView != null) {
304                    float delta = getPos(ev) - mInitialTouchPos;
305                    setSwipeAmount(delta);
306                    mCallback.onSwipeChanged(mCurrView, delta);
307                }
308                break;
309            case MotionEvent.ACTION_UP:
310            case MotionEvent.ACTION_CANCEL:
311                if (mCurrView != null) {
312                    endSwipe(mVelocityTracker);
313                }
314                break;
315        }
316        return true;
317    }
318
319    private void setSwipeAmount(float amount) {
320        // don't let items that can't be dismissed be dragged more than
321        // maxScrollDistance
322        if (CONSTRAIN_SWIPE
323                && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) {
324            float size = getSize(mCurrView);
325            float maxScrollDistance = 0.15f * size;
326            if (Math.abs(amount) >= size) {
327                amount = amount > 0 ? maxScrollDistance : -maxScrollDistance;
328            } else {
329                amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2));
330            }
331        }
332        setTranslation(mCurrView, amount);
333        if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
334            float alpha = getAlphaForOffset(mCurrView);
335            mCurrView.setAlpha(alpha);
336        }
337    }
338
339    private boolean isValidSwipeDirection(float amount) {
340        if (mSwipeDirection == X) {
341            if (mRtl) {
342                return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart;
343            } else {
344                return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd;
345            }
346        }
347
348        // Vertical swipes are always valid.
349        return true;
350    }
351
352    private void endSwipe(VelocityTracker velocityTracker) {
353        float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
354        velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
355        float velocity = getVelocity(velocityTracker);
356        float perpendicularVelocity = getPerpendicularVelocity(velocityTracker);
357        float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
358        float translation = getTranslation(mCurrView);
359        // Decide whether to dismiss the current view
360        boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
361                Math.abs(translation) > 0.6 * getSize(mCurrView);
362        boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
363                (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
364                (velocity > 0) == (translation > 0);
365
366        boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
367                && isValidSwipeDirection(translation)
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
380    public interface Callback {
381        View getChildAtPosition(MotionEvent ev);
382
383        boolean canChildBeDismissed(View v);
384
385        void onBeginDrag(View v);
386
387        void onSwipeChanged(View v, float delta);
388
389        void onChildDismissed(View v);
390
391        void onSnapBackCompleted(View v);
392
393        void onDragCancelled(View v);
394    }
395}
396