Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / android / java / src / org / chromium / chrome / browser / banners / SwipableOverlayView.java
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
5 package org.chromium.chrome.browser.banners;
6
7 import android.animation.Animator;
8 import android.animation.AnimatorListenerAdapter;
9 import android.animation.AnimatorSet;
10 import android.animation.ObjectAnimator;
11 import android.animation.PropertyValuesHolder;
12 import android.content.Context;
13 import android.util.AttributeSet;
14 import android.view.GestureDetector;
15 import android.view.GestureDetector.SimpleOnGestureListener;
16 import android.view.Gravity;
17 import android.view.MotionEvent;
18 import android.view.View;
19 import android.view.ViewGroup;
20 import android.view.animation.DecelerateInterpolator;
21 import android.view.animation.Interpolator;
22 import android.widget.FrameLayout;
23
24 import org.chromium.content.browser.ContentViewCore;
25 import org.chromium.content_public.browser.GestureStateListener;
26 import 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  */
70 public 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 }