1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.dialer.list;
19
20import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.ObjectAnimator;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.content.Context;
26import android.content.res.Resources;
27import android.graphics.RectF;
28import android.util.Log;
29import android.view.MotionEvent;
30import android.view.VelocityTracker;
31import android.view.View;
32import android.view.animation.LinearInterpolator;
33
34import com.android.dialer.R;
35
36/**
37 * Copy of packages/apps/UnifiedEmail - com.android.mail.ui.SwipeHelper with changes.
38 */
39public class SwipeHelper {
40    static final String TAG = SwipeHelper.class.getSimpleName();
41    private static final boolean DEBUG_INVALIDATE = false;
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    private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false; // STOPSHIP - DEBUG ONLY
46
47    public static final int IS_SWIPEABLE_TAG = R.id.is_swipeable_tag;
48    public static final Object IS_SWIPEABLE = new Object();
49
50    public static final int X = 0;
51    public static final int Y = 1;
52
53    private static LinearInterpolator sLinearInterpolator = new LinearInterpolator();
54
55    private static int SWIPE_ESCAPE_VELOCITY = -1;
56    private static int DEFAULT_ESCAPE_ANIMATION_DURATION;
57    private static int MAX_ESCAPE_ANIMATION_DURATION;
58    private static int MAX_DISMISS_VELOCITY;
59    private static int SNAP_ANIM_LEN;
60    private static int SWIPE_SCROLL_SLOP;
61    private static float MIN_SWIPE;
62    private static float MIN_VERT;
63    private static float MIN_LOCK;
64
65    public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
66                                               // where fade starts
67    static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width
68                                              // beyond which alpha->0
69    private static final float FACTOR = 1.2f;
70
71    private static final int PROTECTION_PADDING = 50;
72
73    private float mMinAlpha = 0.3f;
74
75    private float mPagingTouchSlop;
76    private final SwipeHelperCallback mCallback;
77    private final int mSwipeDirection;
78    private final VelocityTracker mVelocityTracker;
79
80    private float mInitialTouchPosX;
81    private boolean mDragging;
82    private View mCurrView;
83    private View mCurrAnimView;
84    private boolean mCanCurrViewBeDimissed;
85    private float mDensityScale;
86    private float mLastY;
87    private float mInitialTouchPosY;
88
89    private float mStartAlpha;
90    private boolean mProtected = false;
91
92    private float mChildSwipedFarEnoughFactor = 0.4f;
93    private float mChildSwipedFastEnoughFactor = 0.05f;
94
95    public SwipeHelper(Context context, int swipeDirection, SwipeHelperCallback callback, float densityScale,
96            float pagingTouchSlop) {
97        mCallback = callback;
98        mSwipeDirection = swipeDirection;
99        mVelocityTracker = VelocityTracker.obtain();
100        mDensityScale = densityScale;
101        mPagingTouchSlop = pagingTouchSlop;
102        if (SWIPE_ESCAPE_VELOCITY == -1) {
103            Resources res = context.getResources();
104            SWIPE_ESCAPE_VELOCITY = res.getInteger(R.integer.swipe_escape_velocity);
105            DEFAULT_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.escape_animation_duration);
106            MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration);
107            MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity);
108            SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration);
109            SWIPE_SCROLL_SLOP = res.getInteger(R.integer.swipe_scroll_slop);
110            MIN_SWIPE = res.getDimension(R.dimen.min_swipe);
111            MIN_VERT = res.getDimension(R.dimen.min_vert);
112            MIN_LOCK = res.getDimension(R.dimen.min_lock);
113        }
114    }
115
116    public void setDensityScale(float densityScale) {
117        mDensityScale = densityScale;
118    }
119
120    public void setPagingTouchSlop(float pagingTouchSlop) {
121        mPagingTouchSlop = pagingTouchSlop;
122    }
123
124    public void setChildSwipedFarEnoughFactor(float factor) {
125        mChildSwipedFarEnoughFactor = factor;
126    }
127
128    public void setChildSwipedFastEnoughFactor(float factor) {
129        mChildSwipedFastEnoughFactor = factor;
130    }
131
132    private float getVelocity(VelocityTracker vt) {
133        return mSwipeDirection == X ? vt.getXVelocity() :
134                vt.getYVelocity();
135    }
136
137    private ObjectAnimator createTranslationAnimation(View v, float newPos) {
138        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
139                mSwipeDirection == X ? "translationX" : "translationY", newPos);
140        return anim;
141    }
142
143    private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) {
144        ObjectAnimator anim = createTranslationAnimation(v, newPos);
145        anim.setInterpolator(sLinearInterpolator);
146        anim.setDuration(duration);
147        return anim;
148    }
149
150    private float getPerpendicularVelocity(VelocityTracker vt) {
151        return mSwipeDirection == X ? vt.getYVelocity() :
152                vt.getXVelocity();
153    }
154
155    private void setTranslation(View v, float translate) {
156        if (mSwipeDirection == X) {
157            v.setTranslationX(translate);
158        } else {
159            v.setTranslationY(translate);
160        }
161    }
162
163    private float getSize(View v) {
164        return mSwipeDirection == X ? v.getMeasuredWidth() :
165                v.getMeasuredHeight();
166    }
167
168    public void setMinAlpha(float minAlpha) {
169        mMinAlpha = minAlpha;
170    }
171
172    private float getAlphaForOffset(View view) {
173        float viewSize = getSize(view);
174        final float fadeSize = ALPHA_FADE_END * viewSize;
175        float result = mStartAlpha;
176        float pos = view.getTranslationX();
177        if (pos >= viewSize * ALPHA_FADE_START) {
178            result = mStartAlpha - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
179        } else if (pos < viewSize * (mStartAlpha - ALPHA_FADE_START)) {
180            result = mStartAlpha + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
181        }
182        return Math.max(mMinAlpha, result);
183    }
184
185    // invalidate the view's own bounds all the way up the view hierarchy
186    public static void invalidateGlobalRegion(View view) {
187        invalidateGlobalRegion(
188                view,
189                new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
190    }
191
192    // invalidate a rectangle relative to the view's coordinate system all the way up the view
193    // hierarchy
194    public static void invalidateGlobalRegion(View view, RectF childBounds) {
195        // childBounds.offset(view.getTranslationX(), view.getTranslationY());
196        if (DEBUG_INVALIDATE)
197            Log.v(TAG, "-------------");
198        while (view.getParent() != null && view.getParent() instanceof View) {
199            view = (View) view.getParent();
200            view.getMatrix().mapRect(childBounds);
201            view.invalidate((int) Math.floor(childBounds.left),
202                    (int) Math.floor(childBounds.top),
203                    (int) Math.ceil(childBounds.right),
204                    (int) Math.ceil(childBounds.bottom));
205            if (DEBUG_INVALIDATE) {
206                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
207                        + "," + (int) Math.floor(childBounds.top)
208                        + "," + (int) Math.ceil(childBounds.right)
209                        + "," + (int) Math.ceil(childBounds.bottom));
210            }
211        }
212    }
213
214    public boolean onInterceptTouchEvent(MotionEvent ev) {
215        final int action = ev.getAction();
216        switch (action) {
217            case MotionEvent.ACTION_DOWN:
218                mLastY = ev.getY();
219                mDragging = false;
220                mCurrView = mCallback.getChildAtPosition(ev);
221                mVelocityTracker.clear();
222                if (mCurrView != null) {
223                    mCurrAnimView = mCallback.getChildContentView(mCurrView);
224                    mStartAlpha = mCurrAnimView.getAlpha();
225                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
226                    mVelocityTracker.addMovement(ev);
227                    mInitialTouchPosX = ev.getX();
228                    mInitialTouchPosY = ev.getY();
229                }
230                break;
231            case MotionEvent.ACTION_MOVE:
232                if (mCurrView != null) {
233                    // Check the movement direction.
234                    if (mLastY >= 0 && !mDragging) {
235                        float currY = ev.getY();
236                        float currX = ev.getX();
237                        float deltaY = Math.abs(currY - mInitialTouchPosY);
238                        float deltaX = Math.abs(currX - mInitialTouchPosX);
239                        if (deltaY > SWIPE_SCROLL_SLOP && deltaY > (FACTOR * deltaX)) {
240                            mLastY = ev.getY();
241                            mCallback.onScroll();
242                            return false;
243                        }
244                    }
245                    mVelocityTracker.addMovement(ev);
246                    float pos = ev.getX();
247                    float delta = pos - mInitialTouchPosX;
248                    if (Math.abs(delta) > mPagingTouchSlop) {
249                        mCallback.onBeginDrag(mCallback.getChildContentView(mCurrView));
250                        mDragging = true;
251                        mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX();
252                        mInitialTouchPosY = ev.getY();
253                    }
254                }
255                mLastY = ev.getY();
256                break;
257            case MotionEvent.ACTION_UP:
258            case MotionEvent.ACTION_CANCEL:
259                mDragging = false;
260                mCurrView = null;
261                mCurrAnimView = null;
262                mLastY = -1;
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
271     *            move
272     */
273    private void dismissChild(final View view, float velocity) {
274        final View animView = mCallback.getChildContentView(view);
275        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
276        float newPos = determinePos(animView, velocity);
277        int duration = determineDuration(animView, newPos, velocity);
278
279        animView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
280        ObjectAnimator anim = createDismissAnimation(animView, newPos, duration);
281        anim.addListener(new AnimatorListenerAdapter() {
282            @Override
283            public void onAnimationEnd(Animator animation) {
284                mCallback.onChildDismissed(view);
285                animView.setLayerType(View.LAYER_TYPE_NONE, null);
286            }
287        });
288        anim.addUpdateListener(new AnimatorUpdateListener() {
289            @Override
290            public void onAnimationUpdate(ValueAnimator animation) {
291                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
292                    animView.setAlpha(getAlphaForOffset(animView));
293                }
294                invalidateGlobalRegion(animView);
295            }
296        });
297        anim.start();
298    }
299
300    private int determineDuration(View animView, float newPos, float velocity) {
301        int duration = MAX_ESCAPE_ANIMATION_DURATION;
302        if (velocity != 0) {
303            duration = Math
304                    .min(duration,
305                            (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math
306                                    .abs(velocity)));
307        } else {
308            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
309        }
310        return duration;
311    }
312
313    private float determinePos(View animView, float velocity) {
314        float newPos = 0;
315        if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0)
316                // if we use the Menu to dismiss an item in landscape, animate up
317                || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) {
318            newPos = -getSize(animView);
319        } else {
320            newPos = getSize(animView);
321        }
322        return newPos;
323    }
324
325    public void snapChild(final View view, float velocity) {
326        final View animView = mCallback.getChildContentView(view);
327        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
328        ObjectAnimator anim = createTranslationAnimation(animView, 0);
329        int duration = SNAP_ANIM_LEN;
330        anim.setDuration(duration);
331        anim.addUpdateListener(new AnimatorUpdateListener() {
332            @Override
333            public void onAnimationUpdate(ValueAnimator animation) {
334                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
335                    animView.setAlpha(getAlphaForOffset(animView));
336                }
337                invalidateGlobalRegion(animView);
338            }
339        });
340        anim.addListener(new AnimatorListenerAdapter() {
341            @Override
342            public void onAnimationEnd(Animator animation) {
343                animView.setAlpha(mStartAlpha);
344                mCallback.onDragCancelled(mCurrView);
345            }
346        });
347        anim.start();
348    }
349
350    public boolean onTouchEvent(MotionEvent ev) {
351        if (!mDragging || mProtected) {
352            return false;
353        }
354        mVelocityTracker.addMovement(ev);
355        final int action = ev.getAction();
356        switch (action) {
357            case MotionEvent.ACTION_OUTSIDE:
358            case MotionEvent.ACTION_MOVE:
359                if (mCurrView != null) {
360                    float deltaX = ev.getX() - mInitialTouchPosX;
361                    float deltaY = Math.abs(ev.getY() - mInitialTouchPosY);
362                    // If the user has gone vertical and not gone horizontalish AT
363                    // LEAST minBeforeLock, switch to scroll. Otherwise, cancel
364                    // the swipe.
365                    if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK
366                            && deltaY > (FACTOR * Math.abs(deltaX))) {
367                        mCallback.onScroll();
368                        return false;
369                    }
370                    float minDistance = MIN_SWIPE;
371                    if (Math.abs(deltaX) < minDistance) {
372                        // Don't start the drag until at least X distance has
373                        // occurred.
374                        return true;
375                    }
376                    // don't let items that can't be dismissed be dragged more
377                    // than maxScrollDistance
378                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
379                        float size = getSize(mCurrAnimView);
380                        float maxScrollDistance = 0.15f * size;
381                        if (Math.abs(deltaX) >= size) {
382                            deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance;
383                        } else {
384                            deltaX = maxScrollDistance
385                                    * (float) Math.sin((deltaX / size) * (Math.PI / 2));
386                        }
387                    }
388                    setTranslation(mCurrAnimView, deltaX);
389                    if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
390                        mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
391                    }
392                    invalidateGlobalRegion(mCallback.getChildContentView(mCurrView));
393                }
394                break;
395            case MotionEvent.ACTION_UP:
396            case MotionEvent.ACTION_CANCEL:
397                if (mCurrView != null) {
398                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
399                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
400                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
401                    float velocity = getVelocity(mVelocityTracker);
402                    float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
403
404                    // Decide whether to dismiss the current view
405                    // Tweak constants below as required to prevent erroneous
406                    // swipe/dismiss
407                    float translation = Math.abs(mCurrAnimView.getTranslationX());
408                    float currAnimViewSize = getSize(mCurrAnimView);
409                    // Long swipe = translation of {@link #mChildSwipedFarEnoughFactor} * width
410                    boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH
411                            && translation > mChildSwipedFarEnoughFactor * currAnimViewSize;
412                    // Fast swipe = > escapeVelocity and translation of
413                    // {@link #mChildSwipedFastEnoughFactor} * width
414                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity)
415                            && (Math.abs(velocity) > Math.abs(perpendicularVelocity))
416                            && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0)
417                            && translation > mChildSwipedFastEnoughFactor * currAnimViewSize;
418                    if (LOG_SWIPE_DISMISS_VELOCITY) {
419                        Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
420                                + perpendicularVelocity + ", x: " + translation + "/"
421                                + currAnimViewSize);
422                    }
423
424                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
425                            && (childSwipedFastEnough || childSwipedFarEnough);
426
427                    if (dismissChild) {
428                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
429                    } else {
430                        snapChild(mCurrView, velocity);
431                    }
432                }
433                break;
434        }
435        return true;
436    }
437
438    public static void setSwipeable(View view, boolean swipeable) {
439        view.setTag(IS_SWIPEABLE_TAG, swipeable ? IS_SWIPEABLE : null);
440    }
441
442    public static boolean isSwipeable(View view) {
443        return IS_SWIPEABLE == view.getTag(IS_SWIPEABLE_TAG);
444    }
445
446    public interface SwipeHelperCallback {
447        View getChildAtPosition(MotionEvent ev);
448
449        View getChildContentView(View v);
450
451        void onScroll();
452
453        boolean canChildBeDismissed(View v);
454
455        void onBeginDrag(View v);
456
457        void onChildDismissed(View v);
458
459        void onDragCancelled(View v);
460
461    }
462
463    public interface OnItemGestureListener {
464        public void onSwipe(View view);
465
466        public void onTouch();
467
468        public boolean isSwipeEnabled();
469    }
470}
471