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