SwipeHelper.java revision ff8553f20964f4c31b0c503a9e1daff6ae08a9c7
17fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao/*
27fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * Copyright (C) 2012 Google Inc.
37fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * Licensed to The Android Open Source Project.
47fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao *
57fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * Licensed under the Apache License, Version 2.0 (the "License");
67fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * you may not use this file except in compliance with the License.
77fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * You may obtain a copy of the License at
87fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao *
97fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao *      http://www.apache.org/licenses/LICENSE-2.0
107fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao *
117fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * Unless required by applicable law or agreed to in writing, software
127fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * distributed under the License is distributed on an "AS IS" BASIS,
137fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
147fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * See the License for the specific language governing permissions and
157fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao * limitations under the License.
167fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao */
177fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao
187fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaopackage com.android.mail.ui;
197fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao
207fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport android.animation.Animator;
217fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport android.animation.AnimatorListenerAdapter;
227fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport android.animation.AnimatorSet;
237fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport android.animation.ObjectAnimator;
246702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.animation.ValueAnimator;
256702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.animation.ValueAnimator.AnimatorUpdateListener;
266702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.content.Context;
276702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.content.res.Resources;
286702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.graphics.RectF;
297fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport android.util.Log;
306702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.view.MotionEvent;
316702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.view.VelocityTracker;
327fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport android.view.View;
336702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport android.view.animation.DecelerateInterpolator;
346702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier
356702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport com.android.mail.R;
366702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport com.android.mail.browse.ConversationItemView;
376702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartierimport com.android.mail.utils.Utils;
387fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao
397fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport java.util.ArrayList;
407fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaoimport java.util.Collection;
4166f19258f9728d4ffe026074d8fd429d639802faMathieu Chartier
427fbee0731b14b5bf392a4254f5cd84685ab517dajeffhaopublic class SwipeHelper {
437fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    static final String TAG = "com.android.systemui.SwipeHelper";
447fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static final boolean DEBUG_INVALIDATE = false;
457fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static final boolean CONSTRAIN_SWIPE = true;
467fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static final boolean FADE_OUT_DURING_SWIPE = true;
477fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true;
486702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    // Turn on for debugging only during development.
497fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static final boolean LOG_SWIPE_DISMISS_VELOCITY = false;
506702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier
517fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    public static final int X = 0;
526702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    public static final int Y = 1;
537fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao
547fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static DecelerateInterpolator sDecelerateInterpolator =
557fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao                                                        new DecelerateInterpolator(1.0f);
567fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao
577fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static int SWIPE_ESCAPE_VELOCITY = -1;
586702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private static int DEFAULT_ESCAPE_ANIMATION_DURATION;
597fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static int MAX_ESCAPE_ANIMATION_DURATION;
606702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private static int MAX_DISMISS_VELOCITY;
617fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static int SNAP_ANIM_LEN;
626702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private static int DISMISS_ANIMATION_DURATION;
637fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static float MIN_SWIPE;
647fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static float MIN_VERT;
657fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private static float MIN_LOCK;
667fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao
677fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    public static float ALPHA_FADE_START = 0f; // fraction of thumbnail width
687fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao                                                 // where fade starts
696702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    public static float ALPHA_TEXT_FADE_START = 0.4f;
706702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    static final float ALPHA_FADE_END = 0.7f; // fraction of thumbnail width
716702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier                                              // beyond which alpha->0
726702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private static final float FACTOR = 1.2f;
736702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private float mMinAlpha = 0.5f;
746702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier
756702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    /* Dead region where swipe cannot be initiated. */
767fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private final static int DEAD_REGION_FOR_SWIPE = 64;
776702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier
786702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private float mPagingTouchSlop;
796702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private Callback mCallback;
806702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private int mSwipeDirection;
816702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private VelocityTracker mVelocityTracker;
826702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier
837fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private float mInitialTouchPosX;
847fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private boolean mDragging;
857fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private SwipeableItemView mCurrView;
867fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private View mCurrAnimView;
877fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao    private boolean mCanCurrViewBeDimissed;
886702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private float mDensityScale;
896702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private float mLastY;
906702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private float mInitialTouchPosY;
916702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    private LeaveBehindItem mPrevView;
926702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier
936702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier    public SwipeHelper(Context context, int swipeDirection, Callback callback, float densityScale,
946702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier            float pagingTouchSlop) {
956702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier        mCallback = callback;
966702243ea2332b566d8e8b871cc9db0906d835adMathieu Chartier        mSwipeDirection = swipeDirection;
977fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao        mVelocityTracker = VelocityTracker.obtain();
987fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao        mDensityScale = densityScale;
997fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao        mPagingTouchSlop = pagingTouchSlop;
1007fbee0731b14b5bf392a4254f5cd84685ab517dajeffhao        if (SWIPE_ESCAPE_VELOCITY == -1) {
101            Resources res = context.getResources();
102            SWIPE_ESCAPE_VELOCITY = res.getInteger(R.integer.swipe_escape_velocity);
103            DEFAULT_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.escape_animation_duration);
104            MAX_ESCAPE_ANIMATION_DURATION = res.getInteger(R.integer.max_escape_animation_duration);
105            MAX_DISMISS_VELOCITY = res.getInteger(R.integer.max_dismiss_velocity);
106            SNAP_ANIM_LEN = res.getInteger(R.integer.snap_animation_duration);
107            DISMISS_ANIMATION_DURATION = res.getInteger(R.integer.dismiss_animation_duration);
108            MIN_SWIPE = res.getDimension(R.dimen.min_swipe);
109            MIN_VERT = res.getDimension(R.dimen.min_vert);
110            MIN_LOCK = res.getDimension(R.dimen.min_lock);
111        }
112    }
113
114    public void setDensityScale(float densityScale) {
115        mDensityScale = densityScale;
116    }
117
118    public void setPagingTouchSlop(float pagingTouchSlop) {
119        mPagingTouchSlop = pagingTouchSlop;
120    }
121
122    private float getVelocity(VelocityTracker vt) {
123        return mSwipeDirection == X ? vt.getXVelocity() :
124                vt.getYVelocity();
125    }
126
127    private ObjectAnimator createTranslationAnimation(View v, float newPos) {
128        ObjectAnimator anim = ObjectAnimator.ofFloat(v,
129                mSwipeDirection == X ? "translationX" : "translationY", newPos);
130        return anim;
131    }
132
133    private ObjectAnimator createDismissAnimation(View v, float newPos, int duration) {
134        ObjectAnimator anim = createTranslationAnimation(v, newPos);
135        anim.setInterpolator(sDecelerateInterpolator);
136        anim.setDuration(duration);
137        return anim;
138    }
139
140    private float getPerpendicularVelocity(VelocityTracker vt) {
141        return mSwipeDirection == X ? vt.getYVelocity() :
142                vt.getXVelocity();
143    }
144
145    private void setTranslation(View v, float translate) {
146        if (mSwipeDirection == X) {
147            v.setTranslationX(translate);
148        } else {
149            v.setTranslationY(translate);
150        }
151    }
152
153    private float getSize(View v) {
154        return mSwipeDirection == X ? v.getMeasuredWidth() :
155                v.getMeasuredHeight();
156    }
157
158    public void setMinAlpha(float minAlpha) {
159        mMinAlpha = minAlpha;
160    }
161
162    private float getAlphaForOffset(View view) {
163        float viewSize = getSize(view);
164        final float fadeSize = ALPHA_FADE_END * viewSize;
165        float result = 1.0f;
166        float pos = view.getTranslationX();
167        if (pos >= viewSize * ALPHA_FADE_START) {
168            result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize;
169        } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) {
170            result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize;
171        }
172        return Math.max(mMinAlpha, result);
173    }
174
175    private float getTextAlphaForOffset(View view) {
176        float viewSize = getSize(view);
177        final float fadeSize = ALPHA_TEXT_FADE_START * viewSize;
178        float result = 1.0f;
179        float pos = view.getTranslationX();
180        if (pos >= 0) {
181            result = 1.0f - pos / fadeSize;
182        } else if (pos < 0) {
183            result = 1.0f + pos / fadeSize;
184        }
185        return Math.max(0, result);
186    }
187
188    // invalidate the view's own bounds all the way up the view hierarchy
189    public static void invalidateGlobalRegion(View view) {
190        invalidateGlobalRegion(
191            view,
192            new RectF(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
193    }
194
195    // invalidate a rectangle relative to the view's coordinate system all the way up the view
196    // hierarchy
197    public static void invalidateGlobalRegion(View view, RectF childBounds) {
198        //childBounds.offset(view.getTranslationX(), view.getTranslationY());
199        if (DEBUG_INVALIDATE)
200            Log.v(TAG, "-------------");
201        while (view.getParent() != null && view.getParent() instanceof View) {
202            view = (View) view.getParent();
203            view.getMatrix().mapRect(childBounds);
204            view.invalidate((int) Math.floor(childBounds.left),
205                            (int) Math.floor(childBounds.top),
206                            (int) Math.ceil(childBounds.right),
207                            (int) Math.ceil(childBounds.bottom));
208            if (DEBUG_INVALIDATE) {
209                Log.v(TAG, "INVALIDATE(" + (int) Math.floor(childBounds.left)
210                        + "," + (int) Math.floor(childBounds.top)
211                        + "," + (int) Math.ceil(childBounds.right)
212                        + "," + (int) Math.ceil(childBounds.bottom));
213            }
214        }
215    }
216
217    public boolean onInterceptTouchEvent(MotionEvent ev) {
218        final int action = ev.getAction();
219        switch (action) {
220            case MotionEvent.ACTION_DOWN:
221                mLastY = ev.getY();
222                mDragging = false;
223                View view = mCallback.getChildAtPosition(ev);
224                if (view instanceof SwipeableItemView) {
225                    mCurrView = (SwipeableItemView) view;
226                }
227                mVelocityTracker.clear();
228                if (mCurrView != null) {
229                    mCurrAnimView = mCurrView.getSwipeableView();
230                    mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView);
231                    mVelocityTracker.addMovement(ev);
232                    mInitialTouchPosX = ev.getX();
233                    mInitialTouchPosY = ev.getY();
234                }
235                mCallback.cancelDismissCounter();
236                break;
237            case MotionEvent.ACTION_MOVE:
238                if (mCurrView != null) {
239                    // Check the movement direction.
240                    if (mLastY >= 0 && !mDragging) {
241                        float currY = ev.getY();
242                        float currX = ev.getX();
243                        float deltaY = Math.abs(currY - mInitialTouchPosY);
244                        float deltaX = Math.abs(currX - mInitialTouchPosX);
245                        if (deltaY > mCurrView.getMinAllowScrollDistance()
246                                && deltaY > (FACTOR * deltaX)) {
247                            mLastY = ev.getY();
248                            mCallback.onScroll();
249                            return false;
250                        }
251                    }
252                    mVelocityTracker.addMovement(ev);
253                    float pos = ev.getX();
254                    float delta = pos - mInitialTouchPosX;
255                    if (Math.abs(delta) > mPagingTouchSlop) {
256                        mCallback.onBeginDrag(mCurrView.getSwipeableView());
257                        mPrevView = mCallback.getLastSwipedItem();
258                        mDragging = true;
259                        mInitialTouchPosX = ev.getX() - mCurrAnimView.getTranslationX();
260                        mInitialTouchPosY = ev.getY();
261                    }
262                }
263                mLastY = ev.getY();
264                break;
265            case MotionEvent.ACTION_UP:
266            case MotionEvent.ACTION_CANCEL:
267                mDragging = false;
268                mCurrView = null;
269                mCurrAnimView = null;
270                mLastY = -1;
271                break;
272        }
273        return mDragging;
274    }
275
276    /**
277     * @param view The view to be dismissed
278     * @param velocity The desired pixels/second speed at which the view should
279     *            move
280     */
281    private void dismissChild(final SwipeableItemView view, float velocity) {
282        final View animView = mCurrView.getSwipeableView();
283        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
284        float newPos = determinePos(animView, velocity);
285        int duration = determineDuration(animView, newPos, velocity);
286
287        Utils.enableHardwareLayer(animView);
288        ObjectAnimator anim = createDismissAnimation(animView, newPos, duration);
289        anim.addListener(new AnimatorListenerAdapter() {
290            @Override
291            public void onAnimationEnd(Animator animation) {
292                mCallback.onChildDismissed(mCurrView);
293                animView.setLayerType(View.LAYER_TYPE_NONE, null);
294            }
295        });
296        anim.addUpdateListener(new AnimatorUpdateListener() {
297            @Override
298            public void onAnimationUpdate(ValueAnimator animation) {
299                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
300                    animView.setAlpha(getAlphaForOffset(animView));
301                }
302                invalidateGlobalRegion(animView);
303            }
304        });
305        anim.start();
306    }
307
308    private void dismissChildren(final Collection<ConversationItemView> views, float velocity,
309            AnimatorListenerAdapter listener) {
310        final View animView = mCurrView.getSwipeableView();
311        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(mCurrView);
312        float newPos = determinePos(animView, velocity);
313        int duration = DISMISS_ANIMATION_DURATION;
314        ArrayList<Animator> animations = new ArrayList<Animator>();
315        ObjectAnimator anim;
316        for (final ConversationItemView view : views) {
317            Utils.enableHardwareLayer(view);
318            anim = createDismissAnimation(view, newPos, duration);
319            anim.addUpdateListener(new AnimatorUpdateListener() {
320                @Override
321                public void onAnimationUpdate(ValueAnimator animation) {
322                    if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
323                        view.setAlpha(getAlphaForOffset(view));
324                    }
325                    invalidateGlobalRegion(view);
326                }
327            });
328            anim.addListener(new AnimatorListenerAdapter() {
329                @Override
330                public void onAnimationEnd(Animator animation) {
331                    view.setLayerType(View.LAYER_TYPE_NONE, null);
332                }
333            });
334            animations.add(anim);
335        }
336        AnimatorSet transitionSet = new AnimatorSet();
337        transitionSet.playTogether(animations);
338        transitionSet.addListener(listener);
339        transitionSet.start();
340    }
341
342    public void dismissChildren(ConversationItemView first,
343            final Collection<ConversationItemView> views, AnimatorListenerAdapter listener) {
344        mCurrView = first;
345        dismissChildren(views, 0f, listener);
346    }
347
348    private static int determineDuration(View animView, float newPos, float velocity) {
349        int duration = MAX_ESCAPE_ANIMATION_DURATION;
350        if (velocity != 0) {
351            duration = Math
352                    .min(duration,
353                            (int) (Math.abs(newPos - animView.getTranslationX()) * 1000f / Math
354                                    .abs(velocity)));
355        } else {
356            duration = DEFAULT_ESCAPE_ANIMATION_DURATION;
357        }
358        return duration;
359    }
360
361    private float determinePos(View animView, float velocity) {
362        final float newPos;
363        if (velocity < 0 || (velocity == 0 && animView.getTranslationX() < 0)
364        // if we use the Menu to dismiss an item in landscape, animate up
365                || (velocity == 0 && animView.getTranslationX() == 0 && mSwipeDirection == Y)) {
366            newPos = -getSize(animView);
367        } else {
368            newPos = getSize(animView);
369        }
370        return newPos;
371    }
372
373    public void snapChild(final SwipeableItemView view) {
374        final View animView = view.getSwipeableView();
375        final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
376        final ObjectAnimator anim = createTranslationAnimation(animView, 0);
377        final int duration = SNAP_ANIM_LEN;
378        anim.setDuration(duration);
379        anim.addUpdateListener(new AnimatorUpdateListener() {
380            @Override
381            public void onAnimationUpdate(ValueAnimator animation) {
382                if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) {
383                    animView.setAlpha(getAlphaForOffset(animView));
384                }
385                invalidateGlobalRegion(animView);
386            }
387        });
388        anim.addListener(new Animator.AnimatorListener() {
389            @Override
390            public void onAnimationStart(Animator animation) {
391            }
392            @Override
393            public void onAnimationEnd(Animator animation) {
394                animView.setAlpha(1.0f);
395                mCallback.onDragCancelled(mCurrView);
396            }
397            @Override
398            public void onAnimationCancel(Animator animation) {
399            }
400            @Override
401            public void onAnimationRepeat(Animator animation) {
402            }
403        });
404        anim.start();
405    }
406
407    public boolean onTouchEvent(MotionEvent ev) {
408        if (!mDragging) {
409            return false;
410        }
411        mVelocityTracker.addMovement(ev);
412        final int action = ev.getAction();
413        switch (action) {
414            case MotionEvent.ACTION_OUTSIDE:
415            case MotionEvent.ACTION_MOVE:
416                if (mCurrView != null) {
417                    float deltaX = ev.getX() - mInitialTouchPosX;
418                    float deltaY = Math.abs(ev.getY() - mInitialTouchPosY);
419                    // If the swipe started in the dead region, ignore it.
420                    if (mInitialTouchPosX <= (DEAD_REGION_FOR_SWIPE * mDensityScale)){
421                            return true;
422                    }
423                    // If the user has gone vertical and not gone horizontalish AT
424                    // LEAST minBeforeLock, switch to scroll. Otherwise, cancel
425                    // the swipe.
426                    if (!mDragging && deltaY > MIN_VERT && (Math.abs(deltaX)) < MIN_LOCK
427                            && deltaY > (FACTOR * Math.abs(deltaX))) {
428                        mCallback.onScroll();
429                        return false;
430                    }
431                    float minDistance = MIN_SWIPE;
432                    if (Math.abs(deltaX) < minDistance) {
433                        // Don't start the drag until at least X distance has
434                        // occurred.
435                        return true;
436                    }
437                    // don't let items that can't be dismissed be dragged more
438                    // than maxScrollDistance
439                    if (CONSTRAIN_SWIPE && !mCallback.canChildBeDismissed(mCurrView)) {
440                        float size = getSize(mCurrAnimView);
441                        float maxScrollDistance = 0.15f * size;
442                        if (Math.abs(deltaX) >= size) {
443                            deltaX = deltaX > 0 ? maxScrollDistance : -maxScrollDistance;
444                        } else {
445                            deltaX = maxScrollDistance
446                                    * (float) Math.sin((deltaX / size) * (Math.PI / 2));
447                        }
448                    }
449                    setTranslation(mCurrAnimView, deltaX);
450                    if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) {
451                        mCurrAnimView.setAlpha(getAlphaForOffset(mCurrAnimView));
452                        if (mPrevView != null) {
453                            // Base how much the text of the prev item is faded
454                            // on how far the current item has moved.
455                            mPrevView.setTextAlpha(getTextAlphaForOffset(mCurrAnimView));
456                        }
457                    }
458                    invalidateGlobalRegion(mCurrView.getSwipeableView());
459                }
460                break;
461            case MotionEvent.ACTION_UP:
462            case MotionEvent.ACTION_CANCEL:
463                if (mCurrView != null) {
464                    float maxVelocity = MAX_DISMISS_VELOCITY * mDensityScale;
465                    mVelocityTracker.computeCurrentVelocity(1000 /* px/sec */, maxVelocity);
466                    float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale;
467                    float velocity = getVelocity(mVelocityTracker);
468                    float perpendicularVelocity = getPerpendicularVelocity(mVelocityTracker);
469
470                    // Decide whether to dismiss the current view
471                    // Tweak constants below as required to prevent erroneous
472                    // swipe/dismiss
473                    float translation = Math.abs(mCurrAnimView.getTranslationX());
474                    float currAnimViewSize = getSize(mCurrAnimView);
475                    // Long swipe = translation of .4 * width
476                    boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH
477                            && translation > 0.4 * currAnimViewSize;
478                    // Fast swipe = > escapeVelocity and translation of .1 *
479                    // width
480                    boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity)
481                            && (Math.abs(velocity) > Math.abs(perpendicularVelocity))
482                            && (velocity > 0) == (mCurrAnimView.getTranslationX() > 0)
483                            && translation > 0.05 * currAnimViewSize;
484                    if (LOG_SWIPE_DISMISS_VELOCITY) {
485                        Log.v(TAG, "Swipe/Dismiss: " + velocity + "/" + escapeVelocity + "/"
486                                + perpendicularVelocity + ", x: " + translation + "/"
487                                + currAnimViewSize);
488                    }
489
490                    boolean dismissChild = mCallback.canChildBeDismissed(mCurrView)
491                            && (childSwipedFastEnough || childSwipedFarEnough);
492
493                    if (dismissChild) {
494                        dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f);
495                    } else {
496                        snapChild(mCurrView);
497                    }
498                }
499                break;
500        }
501        return true;
502    }
503
504    public interface Callback {
505        View getChildAtPosition(MotionEvent ev);
506
507        void cancelDismissCounter();
508
509        void onScroll();
510
511        boolean canChildBeDismissed(SwipeableItemView v);
512
513        void onBeginDrag(View v);
514
515        void onChildDismissed(SwipeableItemView v);
516
517        void onDragCancelled(SwipeableItemView v);
518
519        ConversationSelectionSet getSelectionSet();
520
521        LeaveBehindItem getLastSwipedItem();
522    }
523}
524