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