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