Home | History | Annotate | Download | only in banners
      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 }
    616