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    // invalidate the view's own bounds all the way up the view hierarchy
158    public static void invalidateGlobalRegion(View view) {
159        invalidateGlobalRegion(
160            view,
161            new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
162    }
163
164    // invalidate a rectangle relative to the view's coordinate system all the way up the view
165    // hierarchy
166    public static void invalidateGlobalRegion(View view, RectF childBounds) {
167        //childBounds.offset(view.getTranslationX(), view.getTranslationY());
168        if (DEBUG_INVALIDATE)
169            Log.v(TAG, "-------------");
170        while (view.getParent() != null && view.getParent() instanceof View) {
171            view = (View) view.getParent();
172            view.getMatrix().mapRect(childBounds);
173            view.invalidate((int) Math.floor(childBounds.left),
174                            (int) Math.floor(childBounds.top),
175                            (int) Math.ceil(childBounds.right),
176                            (int) Math.ceil(childBounds.bottom));
177            if (DEBUG_INVALIDATE) {
178                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
179                        + "," + (int) Math.floor(childBounds.top)
180                        + "," + (int) Math.ceil(childBounds.right)
181                        + "," + (int) Math.ceil(childBounds.bottom));
182            }
183        }
184    }
185
186    public void removeLongPressCallback() {
187        if (mWatchLongPress != null) {
188            mHandler.removeCallbacks(mWatchLongPress);
189            mWatchLongPress = null;
190        }
191    }
192
193    public boolean onInterceptTouchEvent(MotionEvent ev) {
194        final int action = ev.getAction();
195
196        switch (action) {
197            case MotionEvent.ACTION_DOWN:
198                mDragging = false;
199                mLongPressSent = false;
200                mCurrView = mCallback.getChildAtPosition(ev);
201                mVelocityTracker.clear();
202                if (mCurrView != null) {
203                    mCurrAnimView = mCallback.getChildContentView(mCurrView);
204                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
205                    mVelocityTracker.addMovement(ev);
206                    mInitialTouchPos = getPos(ev);
207
208                    if (mLongPressListener != null) {
209                        if (mWatchLongPress == null) {
210                            mWatchLongPress = new Runnable() {
211                                @Override
212                                public void run() {
213                                    if (mCurrView != null && !mLongPressSent) {
214                                        mLongPressSent = true;
215                                        mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
216                                        mLongPressListener.onLongClick(mCurrView);
217                                    }
218                                }
219                            };
220                        }
221                        mHandler.postDelayed(mWatchLongPress, mLongPressTimeout);
222                    }
223
224                }
225                break;
226
227            case MotionEvent.ACTION_MOVE:
228                if (mCurrView != null && !mLongPressSent) {
229                    mVelocityTracker.addMovement(ev);
230                    float pos = getPos(ev);
231                    float delta = pos - mInitialTouchPos;
232                    if (Math.abs(delta) > mPagingTouchSlop) {
233                        mCallback.onBeginDrag(mCurrView);
234                        mDragging = true;
235                        mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView);
236
237                        removeLongPressCallback();
238                    }
239                }
240
241                break;
242
243            case MotionEvent.ACTION_UP:
244            case MotionEvent.ACTION_CANCEL:
245                mDragging = false;
246                mCurrView = null;
247                mCurrAnimView = null;
248                mLongPressSent = false;
249                removeLongPressCallback();
250                break;
251        }
252        return mDragging;
253    }
254
255    /**
256     * @param view The view to be dismissed
257     * @param velocity The desired pixels/second speed at which the view should move
258     */
259    public void dismissChild(final View view, float velocity) {
260        final View animView = mCallback.getChildContentView(view);
261        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
262        float newPos;
263
264        if (velocity < 0
265                || (velocity == 0 && getTranslation(animView) < 0)
266                // if we use the Menu to dismiss an item in landscape, animate up
267                || (velocity == 0 && getTranslation(animView) == 0 && mSwipeDirection == Y)) {
268            newPos = -getSize(animView);
269        } else {
270            newPos = getSize(animView);
271        }
272        int duration = MAX_ESCAPE_ANIMATION_DURATION;
273        if (velocity != 0) {
274            duration = Math.min(duration,
275                                (int) (Math.abs(newPos - getTranslation(animView)) * 1000f / Math
276                                        .abs(velocity)));
277        } else {
278            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
279        }
280
281        animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
282        ObjectAnimator anim = createTranslationAnimation(animView, newPos);
283        anim.setInterpolator(sLinearInterpolator);
284        anim.setDuration(duration);
285        anim.addListener(new AnimatorListenerAdapter() {
286            public void onAnimationEnd(Animator animation) {
287                mCallback.onChildDismissed(view);
288                animView.setLayerType(View.LAYER_TYPE_NONE, null);
289            }
290        });
291        anim.addUpdateListener(new AnimatorUpdateListener() {
292            public void onAnimationUpdate(ValueAnimator animation) {
293                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
294                    animView.setAlpha(getAlphaForOffset(animView));
295                }
296                invalidateGlobalRegion(animView);
297            }
298        });
299        anim.start();
300    }
301
302    public void snapChild(final View view, float velocity) {
303        final View animView = mCallback.getChildContentView(view);
304        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(animView);
305        ObjectAnimator anim = createTranslationAnimation(animView, 0);
306        int duration = SNAP_ANIM_LEN;
307        anim.setDuration(duration);
308        anim.addUpdateListener(new AnimatorUpdateListener() {
309            public void onAnimationUpdate(ValueAnimator animation) {
310                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
311                    animView.setAlpha(getAlphaForOffset(animView));
312                }
313                invalidateGlobalRegion(animView);
314            }
315        });
316        anim.start();
317    }
318
319    public boolean onTouchEvent(MotionEvent ev) {
320        if (mLongPressSent) {
321            return true;
322        }
323
324        if (!mDragging) {
325            // We are not doing anything, make sure the long press callback
326            // is not still ticking like a bomb waiting to go off.
327            removeLongPressCallback();
328            return false;
329        }
330
331        mVelocityTracker.addMovement(ev);
332        final int action = ev.getAction();
333        switch (action) {
334            case MotionEvent.ACTION_OUTSIDE:
335            case MotionEvent.ACTION_MOVE:
336                if (mCurrView != null) {
337                    float delta = getPos(ev) - mInitialTouchPos;
338                    // don't let items that can't be dismissed be dragged more than
339                    // maxScrollDistance
340                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
341                        float size = getSize(mCurrAnimView);
342                        float maxScrollDistance = 0.15f * size;
343                        if (Math.abs(delta) >= size) {
344                            delta = delta > 0 ? maxScrollDistance : -maxScrollDistance;
345                        } else {
346                            delta = maxScrollDistance * (float) Math.sin((delta/size)*(Math.PI/2));
347                        }
348                    }
349                    setTranslation(mCurrAnimView, delta);
350                    if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
351                        mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
352                    }
353                    invalidateGlobalRegion(mCurrView);
354                }
355                break;
356            case MotionEvent.ACTION_UP:
357            case MotionEvent.ACTION_CANCEL:
358                if (mCurrView != null) {
359                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
360                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
361                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
362                    float velocity = getVelocity(mVelocityTracker);
363                    float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
364
365                    // Decide whether to dismiss the current view
366                    boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH &&
367                            Math.abs(getTranslation(mCurrAnimView)) > 0.4 * getSize(mCurrAnimView);
368                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) &&
369                            (Math.abs(velocity) > Math.abs(perpendicularVelocity)) &&
370                            (velocity > 0) == (getTranslation(mCurrAnimView) > 0);
371
372                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) &&
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                break;
385        }
386        return true;
387    }
388
389    public interface Callback {
390        View getChildAtPosition(MotionEvent ev);
391
392        View getChildContentView(View v);
393
394        boolean canChildBeDismissed(View v);
395
396        void onBeginDrag(View v);
397
398        void onChildDismissed(View v);
399
400        void onDragCancelled(View v);
401    }
402}
403