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