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