1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.chrome.browser.banners;
6
7import android.animation.Animator;
8import android.animation.AnimatorListenerAdapter;
9import android.animation.AnimatorSet;
10import android.animation.ObjectAnimator;
11import android.animation.PropertyValuesHolder;
12import android.content.Context;
13import android.util.AttributeSet;
14import android.view.GestureDetector;
15import android.view.GestureDetector.SimpleOnGestureListener;
16import android.view.Gravity;
17import android.view.MotionEvent;
18import android.view.View;
19import android.view.ViewGroup;
20import android.view.animation.DecelerateInterpolator;
21import android.view.animation.Interpolator;
22import android.widget.FrameLayout;
23
24import org.chromium.content.browser.ContentViewCore;
25import org.chromium.content_public.browser.GestureStateListener;
26import org.chromium.ui.UiUtils;
27
28/**
29 * View that appears on the screen as the user scrolls on the page and can be swiped away.
30 * Meant to be tacked onto the {@link org.chromium.content.browser.ContentViewCore}'s view and
31 * alerted when either the page scroll position or viewport size changes.
32 *
33 * GENERAL BEHAVIOR
34 * This View is brought onto the screen by sliding upwards from the bottom of the screen.  Afterward
35 * the View slides onto and off of the screen vertically as the user scrolls upwards or
36 * downwards on the page.  Users dismiss the View by swiping it away horizontally.
37 *
38 * VERTICAL SCROLL CALCULATIONS
39 * To determine how close the user is to the top of the page, the View must not only be informed of
40 * page scroll position changes, but also of changes in the viewport size (which happens as the
41 * omnibox appears and disappears, or as the page rotates e.g.).  When the viewport size gradually
42 * shrinks, the user is most likely to be scrolling the page downwards while the omnibox comes back
43 * into view.
44 *
45 * When the user first begins scrolling the page, both the scroll position and the viewport size are
46 * summed and recorded together.  This is because a pixel change in the viewport height is
47 * equivalent to a pixel change in the content's scroll offset:
48 * - As the user scrolls the page downward, either the viewport height will increase (as the omnibox
49 *   is slid off of the screen) or the content scroll offset will increase.
50 * - As the user scrolls the page upward, either the viewport height will decrease (as the omnibox
51 *   is brought back onto the screen) or the content scroll offset will decrease.
52 *
53 * As the scroll offset or the viewport height are updated via a scroll or fling, the difference
54 * from the initial value is used to determine the View's Y-translation.  If a gesture is stopped,
55 * the View will be snapped back into the center of the screen or entirely off of the screen, based
56 * on how much of the View is visible, or where the user is currently located on the page.
57 *
58 * HORIZONTAL SCROLL CALCULATIONS
59 * Horizontal drags and swipes are used to dismiss the View.  Translating the View far enough
60 * horizontally (with "enough" defined by the DISMISS_SWIPE_THRESHOLD AND DISMISS_FLING_THRESHOLD)
61 * triggers an animation that removes the View from the hierarchy.  Failing to meet the threshold
62 * will result in the View being translated back to the center of the screen.
63 *
64 * Because the fling velocity handed in by Android is highly inaccurate and often indicates
65 * that a fling is moving in an opposite direction than expected, the scroll direction is tracked
66 * to determine which direction the user was dragging the View when the fling was initiated.  When a
67 * fling is completed, the more forgiving FLING_THRESHOLD is used to determine how far a user must
68 * swipe to dismiss the View rather than try to use the fling velocity.
69 */
70public abstract class SwipableOverlayView extends FrameLayout {
71    private static final float ALPHA_THRESHOLD = 0.25f;
72    private static final float DISMISS_SWIPE_THRESHOLD = 0.75f;
73    private static final float FULL_THRESHOLD = 0.5f;
74    private static final float VERTICAL_FLING_SHOW_THRESHOLD = 0.2f;
75    private static final float VERTICAL_FLING_HIDE_THRESHOLD = 0.9f;
76    protected static final float ZERO_THRESHOLD = 0.001f;
77
78    private static final int GESTURE_NONE = 0;
79    private static final int GESTURE_SCROLLING = 1;
80    private static final int GESTURE_FLINGING = 2;
81
82    private static final int DRAGGED_LEFT = -1;
83    private static final int DRAGGED_CANCEL = 0;
84    private static final int DRAGGED_RIGHT = 1;
85
86    protected static final long MS_ANIMATION_DURATION = 250;
87    private static final long MS_DISMISS_FLING_THRESHOLD = MS_ANIMATION_DURATION * 2;
88    private static final long MS_SLOW_DISMISS = MS_ANIMATION_DURATION * 3;
89
90    // Detects when the user is dragging the View.
91    private final GestureDetector mGestureDetector;
92
93    // Detects when the user is dragging the ContentViewCore.
94    private final GestureStateListener mGestureStateListener;
95
96    // Monitors for animation completions and resets the state.
97    private final AnimatorListenerAdapter mAnimatorListenerAdapter;
98
99    // Interpolator used for the animation.
100    private final Interpolator mInterpolator;
101
102    // Tracks whether the user is scrolling or flinging.
103    private int mGestureState;
104
105    // Animation currently being used to translate the View.
106    private AnimatorSet mCurrentAnimation;
107
108    // Direction the user is horizontally dragging.
109    private int mDragDirection;
110
111    // How quickly the user is horizontally dragging.
112    private float mDragXPerMs;
113
114    // WHen the user first started dragging.
115    private long mDragStartMs;
116
117    // Used to determine when the layout has changed and the Viewport must be updated.
118    private int mParentHeight;
119
120    // Location of the View when the current gesture was first started.
121    private float mInitialTranslationY;
122
123    // Offset from the top of the page when the current gesture was first started.
124    private int mInitialOffsetY;
125
126    // How tall the View is, including its margins.
127    private int mTotalHeight;
128
129    // Whether or not the View ever been fully displayed.
130    private boolean mIsBeingDisplayedForFirstTime;
131
132    // Whether or not the View has been, or is being, dismissed.
133    private boolean mIsDismissed;
134
135    // The ContentViewCore to which the overlay is added.
136    private ContentViewCore mContentViewCore;
137
138    /**
139     * Creates a SwipableOverlayView.
140     * @param context Context for acquiring resources.
141     * @param attrs Attributes from the XML layout inflation.
142     */
143    public SwipableOverlayView(Context context, AttributeSet attrs) {
144        super(context, attrs);
145        SimpleOnGestureListener gestureListener = createGestureListener();
146        mGestureDetector = new GestureDetector(context, gestureListener);
147        mGestureStateListener = createGestureStateListener();
148        mGestureState = GESTURE_NONE;
149        mAnimatorListenerAdapter = createAnimatorListenerAdapter();
150        mInterpolator = new DecelerateInterpolator(1.0f);
151    }
152
153    /**
154     * Adds this View to the given ContentViewCore's view.
155     * @param layout Layout to add this View to.
156     */
157    protected void addToView(ContentViewCore contentViewCore) {
158        assert mContentViewCore == null;
159        mContentViewCore = contentViewCore;
160        contentViewCore.getContainerView().addView(this, 0, createLayoutParams());
161        contentViewCore.addGestureStateListener(mGestureStateListener);
162
163        // Listen for the layout to know when to animate the View coming onto the screen.
164        addOnLayoutChangeListener(createLayoutChangeListener());
165    }
166
167    /**
168     * Creates a set of LayoutParams that makes the View hug the bottom of the screen.  Override it
169     * for other types of behavior.
170     * @return LayoutParams for use when adding the View to its parent.
171     */
172    protected ViewGroup.MarginLayoutParams createLayoutParams() {
173        return new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT,
174                Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
175    }
176
177    /**
178     * Removes the View from its parent.
179     */
180    boolean removeFromParent() {
181        if (mContentViewCore != null) {
182            mContentViewCore.getContainerView().removeView(this);
183            mContentViewCore = null;
184            return true;
185        }
186        return false;
187    }
188
189    /**
190     * See {@link #android.view.ViewGroup.onLayout(boolean, int, int, int, int)}.
191     */
192    @Override
193    protected void onLayout(boolean changed, int l, int t, int r, int b) {
194        // Hide the View when the keyboard is showing.
195        boolean keyboardIsShowing = UiUtils.isKeyboardShowing(getContext(), this);
196        setVisibility(keyboardIsShowing ? INVISIBLE : VISIBLE);
197
198        // Update the viewport height when the parent View's height changes (e.g. after rotation).
199        int currentParentHeight = getParent() == null ? 0 : ((View) getParent()).getHeight();
200        if (mParentHeight != currentParentHeight) {
201            mParentHeight = currentParentHeight;
202            mGestureState = GESTURE_NONE;
203            if (mCurrentAnimation != null) mCurrentAnimation.end();
204        }
205
206        // Update the known effective height of the View.
207        mTotalHeight = getMeasuredHeight();
208        if (getLayoutParams() instanceof MarginLayoutParams) {
209            MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
210            mTotalHeight += params.topMargin + params.bottomMargin;
211        }
212
213        super.onLayout(changed, l, t, r, b);
214    }
215
216    /**
217     * See {@link #android.view.View.onTouchEvent(MotionEvent)}.
218     */
219    @Override
220    public boolean onTouchEvent(MotionEvent event) {
221        if (mGestureDetector.onTouchEvent(event)) return true;
222        if (mCurrentAnimation != null) return true;
223
224        int action = event.getActionMasked();
225        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
226            onFinishHorizontalGesture();
227            return true;
228        }
229        return false;
230    }
231
232    /**
233     * Creates a listener that monitors horizontal gestures performed on the View.
234     * @return The SimpleOnGestureListener that will monitor the View.
235     */
236    private SimpleOnGestureListener createGestureListener() {
237        return new SimpleOnGestureListener() {
238            @Override
239            public boolean onDown(MotionEvent e) {
240                mGestureState = GESTURE_SCROLLING;
241                mDragDirection = DRAGGED_CANCEL;
242                mDragXPerMs = 0;
243                mDragStartMs = e.getEventTime();
244                return true;
245            }
246
247            @Override
248            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distX, float distY) {
249                float distance = e2.getX() - e1.getX();
250                setTranslationX(getTranslationX() + distance);
251                setAlpha(calculateAnimationAlpha());
252
253                // Because the Android-calculated fling velocity is highly unreliable, we track what
254                // direction the user is dragging the View from here.
255                mDragDirection = distance < 0 ? DRAGGED_LEFT : DRAGGED_RIGHT;
256                return true;
257            }
258
259            @Override
260            public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) {
261                mGestureState = GESTURE_FLINGING;
262
263                // The direction and speed of the Android-given velocity feels completely disjoint
264                // from what the user actually perceives.
265                float androidXPerMs = Math.abs(vX) / 1000.0f;
266
267                // Track how quickly the user has translated the view to this point.
268                float dragXPerMs = Math.abs(getTranslationX()) / (e2.getEventTime() - mDragStartMs);
269
270                // Check if the velocity from the user's drag is higher; if so, use that one
271                // instead since that often feels more correct.
272                mDragXPerMs = mDragDirection * Math.max(androidXPerMs, dragXPerMs);
273                onFinishHorizontalGesture();
274                return true;
275            }
276
277            @Override
278            public boolean onSingleTapConfirmed(MotionEvent e) {
279                onViewClicked();
280                return true;
281            }
282
283            @Override
284            public void onShowPress(MotionEvent e) {
285                onViewPressed(e);
286            }
287        };
288    }
289
290    /**
291     * Called at the end of a user gesture on the banner to either return the banner to a neutral
292     * position in the center of the screen or dismiss it entirely.
293     */
294    private void onFinishHorizontalGesture() {
295        mDragDirection = determineFinalHorizontalLocation();
296        if (mDragDirection == DRAGGED_CANCEL) {
297            // Move the View back to the center of the screen.
298            createHorizontalSnapAnimation(true);
299        } else {
300            // User swiped the View away.  Dismiss it.
301            onViewSwipedAway();
302            dismiss(true);
303        }
304    }
305
306    /**
307     * Creates a listener than monitors the ContentViewCore for scrolls and flings.
308     * The listener updates the location of this View to account for the user's gestures.
309     * @return GestureStateListener to send to the ContentViewCore.
310     */
311    private GestureStateListener createGestureStateListener() {
312        return new GestureStateListener() {
313            @Override
314            public void onFlingStartGesture(int vx, int vy, int scrollOffsetY, int scrollExtentY) {
315                if (!cancelCurrentAnimation()) return;
316                beginGesture(scrollOffsetY, scrollExtentY);
317                mGestureState = GESTURE_FLINGING;
318            }
319
320            @Override
321            public void onFlingEndGesture(int scrollOffsetY, int scrollExtentY) {
322                if (mGestureState != GESTURE_FLINGING) return;
323                mGestureState = GESTURE_NONE;
324
325                int finalOffsetY = computeScrollDifference(scrollOffsetY, scrollExtentY);
326                updateTranslation(scrollOffsetY, scrollExtentY);
327
328                boolean isScrollingDownward = finalOffsetY > 0;
329
330                boolean isVisibleInitially = mInitialTranslationY < mTotalHeight;
331                float percentageVisible = 1.0f - (getTranslationY() / mTotalHeight);
332                float visibilityThreshold = isVisibleInitially
333                        ? VERTICAL_FLING_HIDE_THRESHOLD : VERTICAL_FLING_SHOW_THRESHOLD;
334                boolean isVisibleEnough = percentageVisible > visibilityThreshold;
335
336                boolean show = !isScrollingDownward;
337                if (isVisibleInitially) {
338                    // Check if the View was moving off-screen.
339                    boolean isHiding = getTranslationY() > mInitialTranslationY;
340                    show &= isVisibleEnough || !isHiding;
341                } else {
342                    // When near the top of the page, there's not much room left to scroll.
343                    boolean isNearTopOfPage = finalOffsetY < (mTotalHeight * FULL_THRESHOLD);
344                    show &= isVisibleEnough || isNearTopOfPage;
345                }
346                createVerticalSnapAnimation(show);
347            }
348
349            @Override
350            public void onScrollStarted(int scrollOffsetY, int scrollExtentY) {
351                if (!cancelCurrentAnimation()) return;
352                beginGesture(scrollOffsetY, scrollExtentY);
353                mGestureState = GESTURE_SCROLLING;
354            }
355
356            @Override
357            public void onScrollEnded(int scrollOffsetY, int scrollExtentY) {
358                if (mGestureState != GESTURE_SCROLLING) return;
359                mGestureState = GESTURE_NONE;
360
361                int finalOffsetY = computeScrollDifference(scrollOffsetY, scrollExtentY);
362                updateTranslation(scrollOffsetY, scrollExtentY);
363
364                boolean isNearTopOfPage = finalOffsetY < (mTotalHeight * FULL_THRESHOLD);
365                boolean isVisibleEnough = getTranslationY() < mTotalHeight * FULL_THRESHOLD;
366                createVerticalSnapAnimation(isNearTopOfPage || isVisibleEnough);
367            }
368
369            @Override
370            public void onScrollOffsetOrExtentChanged(int scrollOffsetY, int scrollExtentY) {
371                // This function is called for both fling and scrolls.
372                if (mGestureState == GESTURE_NONE || !cancelCurrentAnimation()) return;
373                updateTranslation(scrollOffsetY, scrollExtentY);
374            }
375
376            private void updateTranslation(int scrollOffsetY, int scrollExtentY) {
377                float translation = mInitialTranslationY
378                        + computeScrollDifference(scrollOffsetY, scrollExtentY);
379                translation = Math.max(0.0f, Math.min(mTotalHeight, translation));
380                setTranslationY(translation);
381            }
382        };
383    }
384
385    /**
386     * Creates a listener that is used only to animate the View coming onto the screen.
387     * @return The SimpleOnGestureListener that will monitor the View.
388     */
389    private View.OnLayoutChangeListener createLayoutChangeListener() {
390        return new View.OnLayoutChangeListener() {
391            @Override
392            public void onLayoutChange(View v, int left, int top, int right, int bottom,
393                    int oldLeft, int oldTop, int oldRight, int oldBottom) {
394                removeOnLayoutChangeListener(this);
395
396                // Animate the View coming in from the bottom of the screen.
397                setTranslationY(mTotalHeight);
398                mIsBeingDisplayedForFirstTime = true;
399                createVerticalSnapAnimation(true);
400                mCurrentAnimation.start();
401            }
402        };
403    }
404
405    /**
406     * Create an animation that snaps the View into position vertically.
407     * @param visible If true, snaps the View to the bottom-center of the screen.  If false,
408     *                translates the View below the bottom-center of the screen so that it is
409     *                effectively invisible.
410     */
411    void createVerticalSnapAnimation(boolean visible) {
412        float translationY = visible ? 0.0f : mTotalHeight;
413        float yDifference = Math.abs(translationY - getTranslationY()) / mTotalHeight;
414        long duration = (long) (MS_ANIMATION_DURATION * yDifference);
415        createAnimation(1.0f, 0, translationY, duration);
416    }
417
418    /**
419     * Create an animation that snaps the View into position horizontally.
420     * @param visible If true, snaps the View to the bottom-center of the screen.  If false,
421     *                translates the View to the side of the screen.
422     */
423    private void createHorizontalSnapAnimation(boolean visible) {
424        if (visible) {
425            // Move back to the center of the screen.
426            createAnimation(1.0f, 0.0f, getTranslationY(), MS_ANIMATION_DURATION);
427        } else {
428            if (mDragDirection == DRAGGED_CANCEL) {
429                // No direction was selected
430                mDragDirection = DRAGGED_LEFT;
431            }
432
433            float finalX = mDragDirection * getWidth();
434
435            // Determine how long it will take for the banner to leave the screen.
436            long duration = MS_ANIMATION_DURATION;
437            switch (mGestureState) {
438                case GESTURE_FLINGING:
439                    duration = (long) calculateMsRequiredToFlingOffScreen();
440                    break;
441                case GESTURE_NONE:
442                    // Explicitly use a slow animation to help educate the user about swiping.
443                    duration = MS_SLOW_DISMISS;
444                    break;
445                default:
446                    break;
447            }
448
449            createAnimation(0.0f, finalX, getTranslationY(), duration);
450        }
451    }
452
453    /**
454     * Dismisses the View, animating it moving off of the screen if needed.
455     * @param horizontally True if the View is being dismissed to the side of the screen.
456     */
457    protected boolean dismiss(boolean horizontally) {
458        if (getParent() == null || mIsDismissed) return false;
459
460        mIsDismissed = true;
461        if (horizontally) {
462            createHorizontalSnapAnimation(false);
463        } else {
464            createVerticalSnapAnimation(false);
465        }
466        return true;
467    }
468
469    /**
470     * @return Whether or not the View has been dismissed.
471     */
472    protected boolean isDismissed() {
473        return mIsDismissed;
474    }
475
476    /**
477     * Calculates how transparent the View should be.
478     *
479     * The transparency value is proportional to how far the View has been swiped away from the
480     * center of the screen.  The {@link ALPHA_THRESHOLD} determines at what point the View should
481     * start fading away.
482     * @return The alpha value to use for the View.
483     */
484    private float calculateAnimationAlpha() {
485        float percentageSwiped = Math.abs(getTranslationX() / getWidth());
486        float percentageAdjusted = Math.max(0.0f, percentageSwiped - ALPHA_THRESHOLD);
487        float alphaRange = 1.0f - ALPHA_THRESHOLD;
488        return 1.0f - percentageAdjusted / alphaRange;
489    }
490
491    private int computeScrollDifference(int scrollOffsetY, int scrollExtentY) {
492        return scrollOffsetY + scrollExtentY - mInitialOffsetY;
493    }
494
495    /**
496     * Determine where the View needs to move.  If the user hasn't tried hard enough to dismiss
497     * the View, move it back to the center.
498     * @return DRAGGED_CANCEL if the View should return to a neutral center position.
499     *         DRAGGED_LEFT if the View should be dismissed to the left.
500     *         DRAGGED_RIGHT if the View should be dismissed to the right.
501     */
502    private int determineFinalHorizontalLocation() {
503        if (mGestureState == GESTURE_FLINGING) {
504            // Because of the unreliability of the fling velocity, we ignore it and instead rely on
505            // the direction the user was last dragging the View.  Moreover, we lower the
506            // translation threshold for dismissal, requiring the View to translate off screen
507            // within a reasonable time frame.
508            float msRequired = calculateMsRequiredToFlingOffScreen();
509            if (msRequired > MS_DISMISS_FLING_THRESHOLD) return DRAGGED_CANCEL;
510        } else if (mGestureState == GESTURE_SCROLLING) {
511            // Check if the user has dragged the View far enough to be dismissed.
512            float dismissPercentage = DISMISS_SWIPE_THRESHOLD;
513            float dismissThreshold = getWidth() * dismissPercentage;
514            if (Math.abs(getTranslationX()) < dismissThreshold) return DRAGGED_CANCEL;
515        }
516
517        return mDragDirection;
518    }
519
520    /**
521     * Assuming a linear velocity, determine how long it would take for the View to translate off
522     * of the screen.
523     */
524    private float calculateMsRequiredToFlingOffScreen() {
525        float remainingDifference = mDragDirection * getWidth() - getTranslationX();
526        return Math.abs(remainingDifference / mDragXPerMs);
527    }
528
529    /**
530     * Creates an animation that slides the View to the given location and visibility.
531     * @param alpha How opaque the View should be at the end.
532     * @param x X-coordinate of the final translation.
533     * @param y Y-coordinate of the final translation.
534     * @param duration How long the animation should run for.
535     */
536    private void createAnimation(float alpha, float x, float y, long duration) {
537        Animator alphaAnimator =
538                ObjectAnimator.ofPropertyValuesHolder(this,
539                        PropertyValuesHolder.ofFloat("alpha", getAlpha(), alpha));
540        Animator translationXAnimator =
541                ObjectAnimator.ofPropertyValuesHolder(this,
542                        PropertyValuesHolder.ofFloat("translationX", getTranslationX(), x));
543        Animator translationYAnimator =
544                ObjectAnimator.ofPropertyValuesHolder(this,
545                        PropertyValuesHolder.ofFloat("translationY", getTranslationY(), y));
546
547        mCurrentAnimation = new AnimatorSet();
548        mCurrentAnimation.setDuration(duration);
549        mCurrentAnimation.playTogether(alphaAnimator, translationXAnimator, translationYAnimator);
550        mCurrentAnimation.addListener(mAnimatorListenerAdapter);
551        mCurrentAnimation.setInterpolator(mInterpolator);
552        mCurrentAnimation.start();
553    }
554
555    /**
556     * Creates an AnimatorListenerAdapter that cleans up after an animation is completed.
557     * @return {@link AnimatorListenerAdapter} to use for animations.
558     */
559    private AnimatorListenerAdapter createAnimatorListenerAdapter() {
560        return new AnimatorListenerAdapter() {
561            @Override
562            public void onAnimationEnd(Animator animation) {
563                if (mIsDismissed) removeFromParent();
564
565                mGestureState = GESTURE_NONE;
566                mCurrentAnimation = null;
567                mIsBeingDisplayedForFirstTime = false;
568            }
569        };
570    }
571
572    /**
573     * Records the conditions of the page when a gesture is initiated.
574     */
575    private void beginGesture(int scrollOffsetY, int scrollExtentY) {
576        mInitialTranslationY = getTranslationY();
577        boolean isInitiallyVisible = mInitialTranslationY < mTotalHeight;
578        int startingY = isInitiallyVisible ? scrollOffsetY : Math.min(scrollOffsetY, mTotalHeight);
579        mInitialOffsetY = startingY + scrollExtentY;
580    }
581
582    /**
583     * Cancels the current animation, if the View isn't being dismissed.
584     * @return True if the animation was canceled or wasn't running, false otherwise.
585     */
586    private boolean cancelCurrentAnimation() {
587        if (!mayCancelCurrentAnimation()) return false;
588        if (mCurrentAnimation != null) mCurrentAnimation.cancel();
589        return true;
590    }
591
592    /**
593     * Determines whether or not the animation can be interrupted.  Animations may not be canceled
594     * when the View is being dismissed or when it's coming onto screen for the first time.
595     * @return Whether or not the animation may be interrupted.
596     */
597    private boolean mayCancelCurrentAnimation() {
598        return !mIsBeingDisplayedForFirstTime && !mIsDismissed;
599    }
600
601    /**
602     * Called when the View has been swiped away by the user.
603     */
604    protected abstract void onViewSwipedAway();
605
606    /**
607     * Called when the View has been clicked.
608     */
609    protected abstract void onViewClicked();
610
611    /**
612     * Called when the View needs to show that it's been pressed.
613     */
614    protected abstract void onViewPressed(MotionEvent event);
615}
616