BaseTransientBottomBar.java revision 8c153ac8980737c4292ae1c019c69471607b3859
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.design.widget; 18 19import static android.support.annotation.RestrictTo.Scope.GROUP_ID; 20import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR; 21 22import android.content.Context; 23import android.content.res.TypedArray; 24import android.os.Build; 25import android.os.Handler; 26import android.os.Looper; 27import android.os.Message; 28import android.support.annotation.IntDef; 29import android.support.annotation.IntRange; 30import android.support.annotation.NonNull; 31import android.support.annotation.Nullable; 32import android.support.annotation.RestrictTo; 33import android.support.design.R; 34import android.support.v4.view.ViewCompat; 35import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; 36import android.support.v4.view.WindowInsetsCompat; 37import android.util.AttributeSet; 38import android.view.Gravity; 39import android.view.LayoutInflater; 40import android.view.MotionEvent; 41import android.view.View; 42import android.view.ViewGroup; 43import android.view.ViewParent; 44import android.view.accessibility.AccessibilityManager; 45import android.view.animation.Animation; 46import android.view.animation.AnimationUtils; 47import android.widget.FrameLayout; 48 49import java.lang.annotation.Retention; 50import java.lang.annotation.RetentionPolicy; 51import java.util.ArrayList; 52import java.util.List; 53 54/** 55 * Base class for lightweight transient bars that are displayed along the bottom edge of the 56 * application window. 57 * 58 * @param <B> The transient bottom bar subclass. 59 */ 60public abstract class BaseTransientBottomBar<B extends BaseTransientBottomBar<B>> { 61 /** 62 * Base class for {@link BaseTransientBottomBar} callbacks. 63 * 64 * @param <B> The transient bottom bar subclass. 65 * @see BaseTransientBottomBar#setCallback(BaseCallback) 66 */ 67 public abstract static class BaseCallback<B> { 68 /** Indicates that the Snackbar was dismissed via a swipe.*/ 69 public static final int DISMISS_EVENT_SWIPE = 0; 70 /** Indicates that the Snackbar was dismissed via an action click.*/ 71 public static final int DISMISS_EVENT_ACTION = 1; 72 /** Indicates that the Snackbar was dismissed via a timeout.*/ 73 public static final int DISMISS_EVENT_TIMEOUT = 2; 74 /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/ 75 public static final int DISMISS_EVENT_MANUAL = 3; 76 /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/ 77 public static final int DISMISS_EVENT_CONSECUTIVE = 4; 78 79 /** @hide */ 80 @RestrictTo(GROUP_ID) 81 @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT, 82 DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE}) 83 @Retention(RetentionPolicy.SOURCE) 84 public @interface DismissEvent {} 85 86 /** 87 * Called when the given {@link BaseTransientBottomBar} has been dismissed, either 88 * through a time-out, having been manually dismissed, or an action being clicked. 89 * 90 * @param transientBottomBar The transient bottom bar which has been dismissed. 91 * @param event The event which caused the dismissal. One of either: 92 * {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION}, 93 * {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or 94 * {@link #DISMISS_EVENT_CONSECUTIVE}. 95 * 96 * @see BaseTransientBottomBar#dismiss() 97 */ 98 public void onDismissed(B transientBottomBar, @DismissEvent int event) { 99 // empty 100 } 101 102 /** 103 * Called when the given {@link BaseTransientBottomBar} is visible. 104 * 105 * @param transientBottomBar The transient bottom bar which is now visible. 106 * @see BaseTransientBottomBar#show() 107 */ 108 public void onShown(B transientBottomBar) { 109 // empty 110 } 111 } 112 113 /** 114 * Interface that defines the behavior of the main content of a transient bottom bar. 115 */ 116 public interface ContentViewCallback { 117 /** 118 * Animates the content of the transient bottom bar in. 119 * 120 * @param delay Animation delay. 121 * @param duration Animation duration. 122 */ 123 void animateContentIn(int delay, int duration); 124 125 /** 126 * Animates the content of the transient bottom bar out. 127 * 128 * @param delay Animation delay. 129 * @param duration Animation duration. 130 */ 131 void animateContentOut(int delay, int duration); 132 } 133 134 /** 135 * @hide 136 */ 137 @RestrictTo(GROUP_ID) 138 @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG}) 139 @IntRange(from = 1) 140 @Retention(RetentionPolicy.SOURCE) 141 public @interface Duration {} 142 143 /** 144 * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time 145 * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown. 146 * 147 * @see #setDuration 148 */ 149 public static final int LENGTH_INDEFINITE = -2; 150 151 /** 152 * Show the Snackbar for a short period of time. 153 * 154 * @see #setDuration 155 */ 156 public static final int LENGTH_SHORT = -1; 157 158 /** 159 * Show the Snackbar for a long period of time. 160 * 161 * @see #setDuration 162 */ 163 public static final int LENGTH_LONG = 0; 164 165 static final int ANIMATION_DURATION = 250; 166 static final int ANIMATION_FADE_DURATION = 180; 167 168 static final Handler sHandler; 169 static final int MSG_SHOW = 0; 170 static final int MSG_DISMISS = 1; 171 172 static { 173 sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { 174 @Override 175 public boolean handleMessage(Message message) { 176 switch (message.what) { 177 case MSG_SHOW: 178 ((BaseTransientBottomBar) message.obj).showView(); 179 return true; 180 case MSG_DISMISS: 181 ((BaseTransientBottomBar) message.obj).hideView(message.arg1); 182 return true; 183 } 184 return false; 185 } 186 }); 187 } 188 189 private final ViewGroup mTargetParent; 190 private final Context mContext; 191 final SnackbarBaseLayout mView; 192 private final ContentViewCallback mContentViewCallback; 193 private int mDuration; 194 195 @Nullable private BaseCallback mCallback; 196 private List<BaseCallback> mCallbacks; 197 198 private final AccessibilityManager mAccessibilityManager; 199 200 /** 201 * @hide 202 */ 203 @RestrictTo(GROUP_ID) 204 interface OnLayoutChangeListener { 205 void onLayoutChange(View view, int left, int top, int right, int bottom); 206 } 207 208 /** 209 * @hide 210 */ 211 @RestrictTo(GROUP_ID) 212 interface OnAttachStateChangeListener { 213 void onViewAttachedToWindow(View v); 214 void onViewDetachedFromWindow(View v); 215 } 216 217 /** 218 * Constructor for the transient bottom bar. 219 * 220 * @param parent The parent for this transient bottom bar. 221 * @param content The content view for this transient bottom bar. 222 * @param contentViewCallback The content view callback for this transient bottom bar. 223 */ 224 protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content, 225 @NonNull ContentViewCallback contentViewCallback) { 226 if (parent == null) { 227 throw new IllegalArgumentException("Transient bottom bar must have non-null parent"); 228 } 229 if (content == null) { 230 throw new IllegalArgumentException("Transient bottom bar must have non-null content"); 231 } 232 if (contentViewCallback == null) { 233 throw new IllegalArgumentException("Transient bottom bar must have non-null callback"); 234 } 235 236 mTargetParent = parent; 237 mContentViewCallback = contentViewCallback; 238 mContext = parent.getContext(); 239 240 ThemeUtils.checkAppCompatTheme(mContext); 241 242 LayoutInflater inflater = LayoutInflater.from(mContext); 243 // Note that for backwards compatibility reasons we inflate a layout that is defined 244 // in the extending Snackbar class. This is to prevent breakage of apps that have custom 245 // coordinator layout behaviors that depend on that layout. 246 mView = (SnackbarBaseLayout) inflater.inflate( 247 R.layout.design_layout_snackbar, mTargetParent, false); 248 mView.addView(content); 249 250 ViewCompat.setAccessibilityLiveRegion(mView, 251 ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); 252 ViewCompat.setImportantForAccessibility(mView, 253 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 254 255 // Make sure that we fit system windows and have a listener to apply any insets 256 ViewCompat.setFitsSystemWindows(mView, true); 257 ViewCompat.setOnApplyWindowInsetsListener(mView, 258 new android.support.v4.view.OnApplyWindowInsetsListener() { 259 @Override 260 public WindowInsetsCompat onApplyWindowInsets(View v, 261 WindowInsetsCompat insets) { 262 // Copy over the bottom inset as padding so that we're displayed 263 // above the navigation bar 264 v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), 265 v.getPaddingRight(), insets.getSystemWindowInsetBottom()); 266 return insets; 267 } 268 }); 269 270 mAccessibilityManager = (AccessibilityManager) 271 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 272 } 273 274 /** 275 * Set how long to show the view for. 276 * 277 * @param duration either be one of the predefined lengths: 278 * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration 279 * in milliseconds. 280 */ 281 @NonNull 282 public B setDuration(@Duration int duration) { 283 mDuration = duration; 284 return (B) this; 285 } 286 287 /** 288 * Return the duration. 289 * 290 * @see #setDuration 291 */ 292 @Duration 293 public int getDuration() { 294 return mDuration; 295 } 296 297 /** 298 * Returns the {@link BaseTransientBottomBar}'s context. 299 */ 300 @NonNull 301 public Context getContext() { 302 return mContext; 303 } 304 305 /** 306 * Returns the {@link BaseTransientBottomBar}'s view. 307 */ 308 @NonNull 309 public View getView() { 310 return mView; 311 } 312 313 /** 314 * Show the {@link BaseTransientBottomBar}. 315 */ 316 public void show() { 317 SnackbarManager.getInstance().show(mDuration, mManagerCallback); 318 } 319 320 /** 321 * Dismiss the {@link BaseTransientBottomBar}. 322 */ 323 public void dismiss() { 324 dispatchDismiss(BaseCallback.DISMISS_EVENT_MANUAL); 325 } 326 327 void dispatchDismiss(@BaseCallback.DismissEvent int event) { 328 SnackbarManager.getInstance().dismiss(mManagerCallback, event); 329 } 330 331 /** 332 * Set a callback to be called when this the visibility of this {@link BaseTransientBottomBar} 333 * changes. Note that this method is deprecated 334 * and you should use {@link #addCallback(BaseCallback)} to add a callback and 335 * {@link #removeCallback(BaseCallback)} to remove a registered callback. 336 * 337 * @param callback Callback to notify when transient bottom bar events occur. 338 * @deprecated Use {@link #addCallback(BaseCallback)} 339 * @see BaseCallback 340 * @see #addCallback(BaseCallback) 341 * @see #removeCallback(BaseCallback) 342 */ 343 @Deprecated 344 @NonNull 345 public B setCallback(BaseCallback callback) { 346 // The logic in this method emulates what we had before support for multiple 347 // registered callbacks. 348 if (mCallback != null) { 349 removeCallback(mCallback); 350 } 351 if (callback != null) { 352 addCallback(callback); 353 } 354 // Update the deprecated field so that we can remove the passed callback the next 355 // time we're called 356 mCallback = callback; 357 return (B) this; 358 } 359 360 /** 361 * Adds the specified callback to the list of callbacks that will be notified of transient 362 * bottom bar events. 363 * 364 * @param callback Callback to notify when transient bottom bar events occur. 365 * @see #removeCallback(BaseCallback) 366 */ 367 @NonNull 368 public B addCallback(@NonNull BaseCallback callback) { 369 if (callback == null) { 370 return (B) this; 371 } 372 if (mCallbacks == null) { 373 mCallbacks = new ArrayList<BaseCallback>(); 374 } 375 mCallbacks.add(callback); 376 return (B) this; 377 } 378 379 /** 380 * Removes the specified callback from the list of callbacks that will be notified of transient 381 * bottom bar events. 382 * 383 * @param callback Callback to remove from being notified of transient bottom bar events 384 * @see #addCallback(BaseCallback) 385 */ 386 @NonNull 387 public B removeCallback(@NonNull BaseCallback callback) { 388 if (callback == null) { 389 return (B) this; 390 } 391 if (mCallbacks == null) { 392 // This can happen if this method is called before the first call to addCallback 393 return (B) this; 394 } 395 mCallbacks.remove(callback); 396 return (B) this; 397 } 398 399 /** 400 * Return whether this {@link BaseTransientBottomBar} is currently being shown. 401 */ 402 public boolean isShown() { 403 return SnackbarManager.getInstance().isCurrent(mManagerCallback); 404 } 405 406 /** 407 * Returns whether this {@link BaseTransientBottomBar} is currently being shown, or is queued 408 * to be shown next. 409 */ 410 public boolean isShownOrQueued() { 411 return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback); 412 } 413 414 final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { 415 @Override 416 public void show() { 417 sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this)); 418 } 419 420 @Override 421 public void dismiss(int event) { 422 sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, 423 BaseTransientBottomBar.this)); 424 } 425 }; 426 427 final void showView() { 428 if (mView.getParent() == null) { 429 final ViewGroup.LayoutParams lp = mView.getLayoutParams(); 430 431 if (lp instanceof CoordinatorLayout.LayoutParams) { 432 // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior 433 final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp; 434 435 final Behavior behavior = new Behavior(); 436 behavior.setStartAlphaSwipeDistance(0.1f); 437 behavior.setEndAlphaSwipeDistance(0.6f); 438 behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END); 439 behavior.setListener(new SwipeDismissBehavior.OnDismissListener() { 440 @Override 441 public void onDismiss(View view) { 442 view.setVisibility(View.GONE); 443 dispatchDismiss(BaseCallback.DISMISS_EVENT_SWIPE); 444 } 445 446 @Override 447 public void onDragStateChanged(int state) { 448 switch (state) { 449 case SwipeDismissBehavior.STATE_DRAGGING: 450 case SwipeDismissBehavior.STATE_SETTLING: 451 // If the view is being dragged or settling, cancel the timeout 452 SnackbarManager.getInstance().cancelTimeout(mManagerCallback); 453 break; 454 case SwipeDismissBehavior.STATE_IDLE: 455 // If the view has been released and is idle, restore the timeout 456 SnackbarManager.getInstance().restoreTimeout(mManagerCallback); 457 break; 458 } 459 } 460 }); 461 clp.setBehavior(behavior); 462 // Also set the inset edge so that views can dodge the bar correctly 463 clp.insetEdge = Gravity.BOTTOM; 464 } 465 466 mTargetParent.addView(mView); 467 } 468 469 mView.setOnAttachStateChangeListener( 470 new BaseTransientBottomBar.OnAttachStateChangeListener() { 471 @Override 472 public void onViewAttachedToWindow(View v) {} 473 474 @Override 475 public void onViewDetachedFromWindow(View v) { 476 if (isShownOrQueued()) { 477 // If we haven't already been dismissed then this event is coming from a 478 // non-user initiated action. Hence we need to make sure that we callback 479 // and keep our state up to date. We need to post the call since 480 // removeView() will call through to onDetachedFromWindow and thus overflow. 481 sHandler.post(new Runnable() { 482 @Override 483 public void run() { 484 onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL); 485 } 486 }); 487 } 488 } 489 }); 490 491 if (ViewCompat.isLaidOut(mView)) { 492 if (shouldAnimate()) { 493 // If animations are enabled, animate it in 494 animateViewIn(); 495 } else { 496 // Else if anims are disabled just call back now 497 onViewShown(); 498 } 499 } else { 500 // Otherwise, add one of our layout change listeners and show it in when laid out 501 mView.setOnLayoutChangeListener(new BaseTransientBottomBar.OnLayoutChangeListener() { 502 @Override 503 public void onLayoutChange(View view, int left, int top, int right, int bottom) { 504 mView.setOnLayoutChangeListener(null); 505 506 if (shouldAnimate()) { 507 // If animations are enabled, animate it in 508 animateViewIn(); 509 } else { 510 // Else if anims are disabled just call back now 511 onViewShown(); 512 } 513 } 514 }); 515 } 516 } 517 518 void animateViewIn() { 519 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 520 ViewCompat.setTranslationY(mView, mView.getHeight()); 521 ViewCompat.animate(mView) 522 .translationY(0f) 523 .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) 524 .setDuration(ANIMATION_DURATION) 525 .setListener(new ViewPropertyAnimatorListenerAdapter() { 526 @Override 527 public void onAnimationStart(View view) { 528 mContentViewCallback.animateContentIn( 529 ANIMATION_DURATION - ANIMATION_FADE_DURATION, 530 ANIMATION_FADE_DURATION); 531 } 532 533 @Override 534 public void onAnimationEnd(View view) { 535 onViewShown(); 536 } 537 }).start(); 538 } else { 539 Animation anim = AnimationUtils.loadAnimation(mView.getContext(), 540 R.anim.design_snackbar_in); 541 anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); 542 anim.setDuration(ANIMATION_DURATION); 543 anim.setAnimationListener(new Animation.AnimationListener() { 544 @Override 545 public void onAnimationEnd(Animation animation) { 546 onViewShown(); 547 } 548 549 @Override 550 public void onAnimationStart(Animation animation) {} 551 552 @Override 553 public void onAnimationRepeat(Animation animation) {} 554 }); 555 mView.startAnimation(anim); 556 } 557 } 558 559 private void animateViewOut(final int event) { 560 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 561 ViewCompat.animate(mView) 562 .translationY(mView.getHeight()) 563 .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) 564 .setDuration(ANIMATION_DURATION) 565 .setListener(new ViewPropertyAnimatorListenerAdapter() { 566 @Override 567 public void onAnimationStart(View view) { 568 mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION); 569 } 570 571 @Override 572 public void onAnimationEnd(View view) { 573 onViewHidden(event); 574 } 575 }).start(); 576 } else { 577 Animation anim = AnimationUtils.loadAnimation(mView.getContext(), 578 R.anim.design_snackbar_out); 579 anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); 580 anim.setDuration(ANIMATION_DURATION); 581 anim.setAnimationListener(new Animation.AnimationListener() { 582 @Override 583 public void onAnimationEnd(Animation animation) { 584 onViewHidden(event); 585 } 586 587 @Override 588 public void onAnimationStart(Animation animation) {} 589 590 @Override 591 public void onAnimationRepeat(Animation animation) {} 592 }); 593 mView.startAnimation(anim); 594 } 595 } 596 597 final void hideView(@BaseCallback.DismissEvent final int event) { 598 if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) { 599 animateViewOut(event); 600 } else { 601 // If anims are disabled or the view isn't visible, just call back now 602 onViewHidden(event); 603 } 604 } 605 606 void onViewShown() { 607 SnackbarManager.getInstance().onShown(mManagerCallback); 608 if (mCallbacks != null) { 609 // Notify the callbacks. Do that from the end of the list so that if a callback 610 // removes itself as the result of being called, it won't mess up with our iteration 611 int callbackCount = mCallbacks.size(); 612 for (int i = callbackCount - 1; i >= 0; i--) { 613 mCallbacks.get(i).onShown(this); 614 } 615 } 616 } 617 618 void onViewHidden(int event) { 619 // First tell the SnackbarManager that it has been dismissed 620 SnackbarManager.getInstance().onDismissed(mManagerCallback); 621 if (mCallbacks != null) { 622 // Notify the callbacks. Do that from the end of the list so that if a callback 623 // removes itself as the result of being called, it won't mess up with our iteration 624 int callbackCount = mCallbacks.size(); 625 for (int i = callbackCount - 1; i >= 0; i--) { 626 mCallbacks.get(i).onDismissed(this, event); 627 } 628 } 629 if (Build.VERSION.SDK_INT < 11) { 630 // We need to hide the Snackbar on pre-v11 since it uses an old style Animation. 631 // ViewGroup has special handling in removeView() when getAnimation() != null in 632 // that it waits. This then means that the calculated insets are wrong and the 633 // any dodging views do not return. We workaround it by setting the view to gone while 634 // ViewGroup actually gets around to removing it. 635 mView.setVisibility(View.GONE); 636 } 637 // Lastly, hide and remove the view from the parent (if attached) 638 final ViewParent parent = mView.getParent(); 639 if (parent instanceof ViewGroup) { 640 ((ViewGroup) parent).removeView(mView); 641 } 642 } 643 644 /** 645 * Returns true if we should animate the Snackbar view in/out. 646 */ 647 boolean shouldAnimate() { 648 return !mAccessibilityManager.isEnabled(); 649 } 650 651 /** 652 * @hide 653 */ 654 @RestrictTo(GROUP_ID) 655 static class SnackbarBaseLayout extends FrameLayout { 656 private BaseTransientBottomBar.OnLayoutChangeListener mOnLayoutChangeListener; 657 private BaseTransientBottomBar.OnAttachStateChangeListener mOnAttachStateChangeListener; 658 659 SnackbarBaseLayout(Context context) { 660 this(context, null); 661 } 662 663 SnackbarBaseLayout(Context context, AttributeSet attrs) { 664 super(context, attrs); 665 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout); 666 if (a.hasValue(R.styleable.SnackbarLayout_elevation)) { 667 ViewCompat.setElevation(this, a.getDimensionPixelSize( 668 R.styleable.SnackbarLayout_elevation, 0)); 669 } 670 a.recycle(); 671 672 setClickable(true); 673 } 674 675 @Override 676 protected void onLayout(boolean changed, int l, int t, int r, int b) { 677 super.onLayout(changed, l, t, r, b); 678 if (mOnLayoutChangeListener != null) { 679 mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b); 680 } 681 } 682 683 @Override 684 protected void onAttachedToWindow() { 685 super.onAttachedToWindow(); 686 if (mOnAttachStateChangeListener != null) { 687 mOnAttachStateChangeListener.onViewAttachedToWindow(this); 688 } 689 690 ViewCompat.requestApplyInsets(this); 691 } 692 693 @Override 694 protected void onDetachedFromWindow() { 695 super.onDetachedFromWindow(); 696 if (mOnAttachStateChangeListener != null) { 697 mOnAttachStateChangeListener.onViewDetachedFromWindow(this); 698 } 699 } 700 701 void setOnLayoutChangeListener( 702 BaseTransientBottomBar.OnLayoutChangeListener onLayoutChangeListener) { 703 mOnLayoutChangeListener = onLayoutChangeListener; 704 } 705 706 void setOnAttachStateChangeListener( 707 BaseTransientBottomBar.OnAttachStateChangeListener listener) { 708 mOnAttachStateChangeListener = listener; 709 } 710 } 711 712 final class Behavior extends SwipeDismissBehavior<SnackbarBaseLayout> { 713 @Override 714 public boolean canSwipeDismissView(View child) { 715 return child instanceof SnackbarBaseLayout; 716 } 717 718 @Override 719 public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarBaseLayout child, 720 MotionEvent event) { 721 // We want to make sure that we disable any Snackbar timeouts if the user is 722 // currently touching the Snackbar. We restore the timeout when complete 723 if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) { 724 switch (event.getActionMasked()) { 725 case MotionEvent.ACTION_DOWN: 726 SnackbarManager.getInstance().cancelTimeout(mManagerCallback); 727 break; 728 case MotionEvent.ACTION_UP: 729 case MotionEvent.ACTION_CANCEL: 730 SnackbarManager.getInstance().restoreTimeout(mManagerCallback); 731 break; 732 } 733 } 734 735 return super.onInterceptTouchEvent(parent, child, event); 736 } 737 } 738} 739