SwipeHelper.java revision 602de03cf3933ddf3c2548d54956ee76e60d1143
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        if (Console.Enabled) {
182            Console.log(Constants.Log.UI.TouchEvents,
183                    "[SwipeHelper|interceptTouchEvent]",
184                    Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
185        }
186        final int action = ev.getAction();
187
188        switch (action) {
189            case MotionEvent.ACTION_DOWN:
190                mDragging = false;
191                mCurrView = mCallback.getChildAtPosition(ev);
192                mVelocityTracker.clear();
193                if (mCurrView != null) {
194                    mRtl = isLayoutRtl(mCurrView);
195                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
196                    mVelocityTracker.addMovement(ev);
197                    mInitialTouchPos = getPos(ev);
198                } else {
199                    mCanCurrViewBeDimissed = false;
200                }
201                break;
202            case MotionEvent.ACTION_MOVE:
203                if (mCurrView != null) {
204                    mVelocityTracker.addMovement(ev);
205                    float pos = getPos(ev);
206                    float delta = pos - mInitialTouchPos;
207                    if (Math.abs(delta) > mPagingTouchSlop) {
208                        mCallback.onBeginDrag(mCurrView);
209                        mDragging = true;
210                        mInitialTouchPos = pos - getTranslation(mCurrView);
211                    }
212                }
213                break;
214            case MotionEvent.ACTION_UP:
215            case MotionEvent.ACTION_CANCEL:
216                mDragging = false;
217                mCurrView = null;
218                break;
219        }
220        return mDragging;
221    }
222
223    /**
224     * @param view The view to be dismissed
225     * @param velocity The desired pixels/second speed at which the view should move
226     */
227    private void dismissChild(final View view, float velocity) {
228        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
229        float newPos;
230        if (velocity < 0
231                || (velocity == 0 && getTranslation(view) < 0)
232                // if we use the Menu to dismiss an item in landscape, animate up
233                || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) {
234            newPos = -getSize(view);
235        } else {
236            newPos = getSize(view);
237        }
238        int duration = MAX_ESCAPE_ANIMATION_DURATION;
239        if (velocity != 0) {
240            duration = Math.min(duration,
241                                (int) (Math.abs(newPos - getTranslation(view)) *
242                                        1000f / Math.abs(velocity)));
243        } else {
244            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
245        }
246
247        ValueAnimator anim = createTranslationAnimation(view, newPos);
248        anim.setInterpolator(sLinearInterpolator);
249        anim.setDuration(duration);
250        anim.addListener(new AnimatorListenerAdapter() {
251            @Override
252            public void onAnimationEnd(Animator animation) {
253                mCallback.onChildDismissed(view);
254                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
255                    view.setAlpha(1.f);
256                }
257            }
258        });
259        anim.addUpdateListener(new AnimatorUpdateListener() {
260            @Override
261            public void onAnimationUpdate(ValueAnimator animation) {
262                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
263                    view.setAlpha(getAlphaForOffset(view));
264                }
265            }
266        });
267        anim.start();
268    }
269
270    private void snapChild(final View view, float velocity) {
271        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
272        ValueAnimator anim = createTranslationAnimation(view, 0);
273        int duration = SNAP_ANIM_LEN;
274        anim.setDuration(duration);
275        anim.addUpdateListener(new AnimatorUpdateListener() {
276            @Override
277            public void onAnimationUpdate(ValueAnimator animation) {
278                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
279                    view.setAlpha(getAlphaForOffset(view));
280                }
281                mCallback.onSwipeChanged(mCurrView, view.getTranslationX());
282            }
283        });
284        anim.addListener(new AnimatorListenerAdapter() {
285            @Override
286            public void onAnimationEnd(Animator animation) {
287                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
288                    view.setAlpha(1.0f);
289                }
290                mCallback.onSnapBackCompleted(view);
291            }
292        });
293        anim.start();
294    }
295
296    public boolean onTouchEvent(MotionEvent ev) {
297        if (Console.Enabled) {
298            Console.log(Constants.Log.UI.TouchEvents,
299                    "[SwipeHelper|touchEvent]",
300                    Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
301        }
302
303        if (!mDragging) {
304            if (!onInterceptTouchEvent(ev)) {
305                return mCanCurrViewBeDimissed;
306            }
307        }
308
309        mVelocityTracker.addMovement(ev);
310        final int action = ev.getAction();
311        switch (action) {
312            case MotionEvent.ACTION_OUTSIDE:
313            case MotionEvent.ACTION_MOVE:
314                if (mCurrView != null) {
315                    float delta = getPos(ev) - mInitialTouchPos;
316                    setSwipeAmount(delta);
317                    mCallback.onSwipeChanged(mCurrView, delta);
318                }
319                break;
320            case MotionEvent.ACTION_UP:
321            case MotionEvent.ACTION_CANCEL:
322                if (mCurrView != null) {
323                    endSwipe(mVelocityTracker);
324                }
325                break;
326        }
327        return true;
328    }
329
330    private void setSwipeAmount(float amount) {
331        // don't let items that can't be dismissed be dragged more than
332        // maxScrollDistance
333        if (CONSTRAIN_SWIPE
334                && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) {
335            float size = getSize(mCurrView);
336            float maxScrollDistance = 0.15f * size;
337            if (Math.abs(amount) >= size) {
338                amount = amount > 0 ? maxScrollDistance : -maxScrollDistance;
339            } else {
340                amount = maxScrollDistance * (float) Math.sin((amount/size)*(Math.PI/2));
341            }
342        }
343        setTranslation(mCurrView, amount);
344        if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
345            float alpha = getAlphaForOffset(mCurrView);
346            mCurrView.setAlpha(alpha);
347        }
348    }
349
350    private boolean isValidSwipeDirection(float amount) {
351        if (mSwipeDirection == X) {
352            if (mRtl) {
353                return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart;
354            } else {
355                return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd;
356            }
357        }
358
359        // Vertical swipes are always valid.
360        return true;
361    }
362
363    private void endSwipe(VelocityTracker velocityTracker) {
364        float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
365        velocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
366        float velocity = getVelocity(velocityTracker);
367        float perpendicularVelocity = getPerpendicularVelocity(velocityTracker);
368        float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
369        float translation = getTranslation(mCurrView);
370        // Decide whether to dismiss the current view
371        boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
372                Math.abs(translation) > 0.6 * getSize(mCurrView);
373        boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
374                (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
375                (velocity > 0) == (translation > 0);
376
377        boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
378                && isValidSwipeDirection(translation)
379                && (childSwipedFastEnough || childSwipedFarEnough);
380
381        if (dismissChild) {
382            // flingadingy
383            dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
384        } else {
385            // snappity
386            mCallback.onDragCancelled(mCurrView);
387            snapChild(mCurrView, velocity);
388        }
389    }
390
391    public interface Callback {
392        View getChildAtPosition(MotionEvent ev);
393
394        boolean canChildBeDismissed(View v);
395
396        void onBeginDrag(View v);
397
398        void onSwipeChanged(View v, float delta);
399
400        void onChildDismissed(View v);
401
402        void onSnapBackCompleted(View v);
403
404        void onDragCancelled(View v);
405    }
406}
407