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 android.content.Context; 20import android.content.res.ColorStateList; 21import android.content.res.TypedArray; 22import android.os.Build; 23import android.os.Handler; 24import android.os.Looper; 25import android.os.Message; 26import android.support.annotation.ColorInt; 27import android.support.annotation.IntDef; 28import android.support.annotation.IntRange; 29import android.support.annotation.NonNull; 30import android.support.annotation.RestrictTo; 31import android.support.annotation.StringRes; 32import android.support.design.R; 33import android.support.v4.view.OnApplyWindowInsetsListener; 34import android.support.v4.view.ViewCompat; 35import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; 36import android.support.v4.view.WindowInsetsCompat; 37import android.text.TextUtils; 38import android.util.AttributeSet; 39import android.view.Gravity; 40import android.view.LayoutInflater; 41import android.view.MotionEvent; 42import android.view.View; 43import android.view.ViewGroup; 44import android.view.ViewParent; 45import android.view.accessibility.AccessibilityManager; 46import android.view.animation.Animation; 47import android.view.animation.AnimationUtils; 48import android.widget.Button; 49import android.widget.FrameLayout; 50import android.widget.LinearLayout; 51import android.widget.TextView; 52 53import java.lang.annotation.Retention; 54import java.lang.annotation.RetentionPolicy; 55 56import static android.support.annotation.RestrictTo.Scope.GROUP_ID; 57import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR; 58 59/** 60 * Snackbars provide lightweight feedback about an operation. They show a brief message at the 61 * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other 62 * elements on screen and only one can be displayed at a time. 63 * <p> 64 * They automatically disappear after a timeout or after user interaction elsewhere on the screen, 65 * particularly after interactions that summon a new surface or activity. Snackbars can be swiped 66 * off screen. 67 * <p> 68 * Snackbars can contain an action which is set via 69 * {@link #setAction(CharSequence, android.view.View.OnClickListener)}. 70 * <p> 71 * To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback} 72 * via {@link #setCallback(Callback)}.</p> 73 */ 74public final class Snackbar { 75 76 /** 77 * Callback class for {@link Snackbar} instances. 78 * 79 * @see Snackbar#setCallback(Callback) 80 */ 81 public static abstract class Callback { 82 /** Indicates that the Snackbar was dismissed via a swipe.*/ 83 public static final int DISMISS_EVENT_SWIPE = 0; 84 /** Indicates that the Snackbar was dismissed via an action click.*/ 85 public static final int DISMISS_EVENT_ACTION = 1; 86 /** Indicates that the Snackbar was dismissed via a timeout.*/ 87 public static final int DISMISS_EVENT_TIMEOUT = 2; 88 /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/ 89 public static final int DISMISS_EVENT_MANUAL = 3; 90 /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/ 91 public static final int DISMISS_EVENT_CONSECUTIVE = 4; 92 93 /** @hide */ 94 @RestrictTo(GROUP_ID) 95 @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT, 96 DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE}) 97 @Retention(RetentionPolicy.SOURCE) 98 public @interface DismissEvent {} 99 100 /** 101 * Called when the given {@link Snackbar} has been dismissed, either through a time-out, 102 * having been manually dismissed, or an action being clicked. 103 * 104 * @param snackbar The snackbar which has been dismissed. 105 * @param event The event which caused the dismissal. One of either: 106 * {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION}, 107 * {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or 108 * {@link #DISMISS_EVENT_CONSECUTIVE}. 109 * 110 * @see Snackbar#dismiss() 111 */ 112 public void onDismissed(Snackbar snackbar, @DismissEvent int event) { 113 // empty 114 } 115 116 /** 117 * Called when the given {@link Snackbar} is visible. 118 * 119 * @param snackbar The snackbar which is now visible. 120 * @see Snackbar#show() 121 */ 122 public void onShown(Snackbar snackbar) { 123 // empty 124 } 125 } 126 127 /** 128 * @hide 129 */ 130 @RestrictTo(GROUP_ID) 131 @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG}) 132 @IntRange(from = 1) 133 @Retention(RetentionPolicy.SOURCE) 134 public @interface Duration {} 135 136 /** 137 * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time 138 * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown. 139 * 140 * @see #setDuration 141 */ 142 public static final int LENGTH_INDEFINITE = -2; 143 144 /** 145 * Show the Snackbar for a short period of time. 146 * 147 * @see #setDuration 148 */ 149 public static final int LENGTH_SHORT = -1; 150 151 /** 152 * Show the Snackbar for a long period of time. 153 * 154 * @see #setDuration 155 */ 156 public static final int LENGTH_LONG = 0; 157 158 static final int ANIMATION_DURATION = 250; 159 static final int ANIMATION_FADE_DURATION = 180; 160 161 static final Handler sHandler; 162 static final int MSG_SHOW = 0; 163 static final int MSG_DISMISS = 1; 164 165 static { 166 sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { 167 @Override 168 public boolean handleMessage(Message message) { 169 switch (message.what) { 170 case MSG_SHOW: 171 ((Snackbar) message.obj).showView(); 172 return true; 173 case MSG_DISMISS: 174 ((Snackbar) message.obj).hideView(message.arg1); 175 return true; 176 } 177 return false; 178 } 179 }); 180 } 181 182 private final ViewGroup mTargetParent; 183 private final Context mContext; 184 final SnackbarLayout mView; 185 private int mDuration; 186 private Callback mCallback; 187 188 private final AccessibilityManager mAccessibilityManager; 189 190 private Snackbar(ViewGroup parent) { 191 mTargetParent = parent; 192 mContext = parent.getContext(); 193 194 ThemeUtils.checkAppCompatTheme(mContext); 195 196 LayoutInflater inflater = LayoutInflater.from(mContext); 197 mView = (SnackbarLayout) inflater.inflate( 198 R.layout.design_layout_snackbar, mTargetParent, false); 199 200 mAccessibilityManager = (AccessibilityManager) 201 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 202 } 203 204 /** 205 * Make a Snackbar to display a message 206 * 207 * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given 208 * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent, 209 * which is defined as a {@link CoordinatorLayout} or the window decor's content view, 210 * whichever comes first. 211 * 212 * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable 213 * certain features, such as swipe-to-dismiss and automatically moving of widgets like 214 * {@link FloatingActionButton}. 215 * 216 * @param view The view to find a parent from. 217 * @param text The text to show. Can be formatted text. 218 * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link 219 * #LENGTH_LONG} 220 */ 221 @NonNull 222 public static Snackbar make(@NonNull View view, @NonNull CharSequence text, 223 @Duration int duration) { 224 Snackbar snackbar = new Snackbar(findSuitableParent(view)); 225 snackbar.setText(text); 226 snackbar.setDuration(duration); 227 return snackbar; 228 } 229 230 /** 231 * Make a Snackbar to display a message. 232 * 233 * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given 234 * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent, 235 * which is defined as a {@link CoordinatorLayout} or the window decor's content view, 236 * whichever comes first. 237 * 238 * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable 239 * certain features, such as swipe-to-dismiss and automatically moving of widgets like 240 * {@link FloatingActionButton}. 241 * 242 * @param view The view to find a parent from. 243 * @param resId The resource id of the string resource to use. Can be formatted text. 244 * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link 245 * #LENGTH_LONG} 246 */ 247 @NonNull 248 public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) { 249 return make(view, view.getResources().getText(resId), duration); 250 } 251 252 private static ViewGroup findSuitableParent(View view) { 253 ViewGroup fallback = null; 254 do { 255 if (view instanceof CoordinatorLayout) { 256 // We've found a CoordinatorLayout, use it 257 return (ViewGroup) view; 258 } else if (view instanceof FrameLayout) { 259 if (view.getId() == android.R.id.content) { 260 // If we've hit the decor content view, then we didn't find a CoL in the 261 // hierarchy, so use it. 262 return (ViewGroup) view; 263 } else { 264 // It's not the content view but we'll use it as our fallback 265 fallback = (ViewGroup) view; 266 } 267 } 268 269 if (view != null) { 270 // Else, we will loop and crawl up the view hierarchy and try to find a parent 271 final ViewParent parent = view.getParent(); 272 view = parent instanceof View ? (View) parent : null; 273 } 274 } while (view != null); 275 276 // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback 277 return fallback; 278 } 279 280 /** 281 * Set the action to be displayed in this {@link Snackbar}. 282 * 283 * @param resId String resource to display 284 * @param listener callback to be invoked when the action is clicked 285 */ 286 @NonNull 287 public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) { 288 return setAction(mContext.getText(resId), listener); 289 } 290 291 /** 292 * Set the action to be displayed in this {@link Snackbar}. 293 * 294 * @param text Text to display 295 * @param listener callback to be invoked when the action is clicked 296 */ 297 @NonNull 298 public Snackbar setAction(CharSequence text, final View.OnClickListener listener) { 299 final TextView tv = mView.getActionView(); 300 301 if (TextUtils.isEmpty(text) || listener == null) { 302 tv.setVisibility(View.GONE); 303 tv.setOnClickListener(null); 304 } else { 305 tv.setVisibility(View.VISIBLE); 306 tv.setText(text); 307 tv.setOnClickListener(new View.OnClickListener() { 308 @Override 309 public void onClick(View view) { 310 listener.onClick(view); 311 // Now dismiss the Snackbar 312 dispatchDismiss(Callback.DISMISS_EVENT_ACTION); 313 } 314 }); 315 } 316 return this; 317 } 318 319 /** 320 * Sets the text color of the action specified in 321 * {@link #setAction(CharSequence, View.OnClickListener)}. 322 */ 323 @NonNull 324 public Snackbar setActionTextColor(ColorStateList colors) { 325 final TextView tv = mView.getActionView(); 326 tv.setTextColor(colors); 327 return this; 328 } 329 330 /** 331 * Sets the text color of the action specified in 332 * {@link #setAction(CharSequence, View.OnClickListener)}. 333 */ 334 @NonNull 335 public Snackbar setActionTextColor(@ColorInt int color) { 336 final TextView tv = mView.getActionView(); 337 tv.setTextColor(color); 338 return this; 339 } 340 341 /** 342 * Update the text in this {@link Snackbar}. 343 * 344 * @param message The new text for the Toast. 345 */ 346 @NonNull 347 public Snackbar setText(@NonNull CharSequence message) { 348 final TextView tv = mView.getMessageView(); 349 tv.setText(message); 350 return this; 351 } 352 353 /** 354 * Update the text in this {@link Snackbar}. 355 * 356 * @param resId The new text for the Toast. 357 */ 358 @NonNull 359 public Snackbar setText(@StringRes int resId) { 360 return setText(mContext.getText(resId)); 361 } 362 363 /** 364 * Set how long to show the view for. 365 * 366 * @param duration either be one of the predefined lengths: 367 * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration 368 * in milliseconds. 369 */ 370 @NonNull 371 public Snackbar setDuration(@Duration int duration) { 372 mDuration = duration; 373 return this; 374 } 375 376 /** 377 * Return the duration. 378 * 379 * @see #setDuration 380 */ 381 @Duration 382 public int getDuration() { 383 return mDuration; 384 } 385 386 /** 387 * Returns the {@link Snackbar}'s view. 388 */ 389 @NonNull 390 public View getView() { 391 return mView; 392 } 393 394 /** 395 * Show the {@link Snackbar}. 396 */ 397 public void show() { 398 SnackbarManager.getInstance().show(mDuration, mManagerCallback); 399 } 400 401 /** 402 * Dismiss the {@link Snackbar}. 403 */ 404 public void dismiss() { 405 dispatchDismiss(Callback.DISMISS_EVENT_MANUAL); 406 } 407 408 void dispatchDismiss(@Callback.DismissEvent int event) { 409 SnackbarManager.getInstance().dismiss(mManagerCallback, event); 410 } 411 412 /** 413 * Set a callback to be called when this the visibility of this {@link Snackbar} changes. 414 */ 415 @NonNull 416 public Snackbar setCallback(Callback callback) { 417 mCallback = callback; 418 return this; 419 } 420 421 /** 422 * Return whether this {@link Snackbar} is currently being shown. 423 */ 424 public boolean isShown() { 425 return SnackbarManager.getInstance().isCurrent(mManagerCallback); 426 } 427 428 /** 429 * Returns whether this {@link Snackbar} is currently being shown, or is queued to be 430 * shown next. 431 */ 432 public boolean isShownOrQueued() { 433 return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback); 434 } 435 436 final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { 437 @Override 438 public void show() { 439 sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this)); 440 } 441 442 @Override 443 public void dismiss(int event) { 444 sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this)); 445 } 446 }; 447 448 final void showView() { 449 if (mView.getParent() == null) { 450 final ViewGroup.LayoutParams lp = mView.getLayoutParams(); 451 452 if (lp instanceof CoordinatorLayout.LayoutParams) { 453 // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior 454 final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp; 455 456 final Behavior behavior = new Behavior(); 457 behavior.setStartAlphaSwipeDistance(0.1f); 458 behavior.setEndAlphaSwipeDistance(0.6f); 459 behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END); 460 behavior.setListener(new SwipeDismissBehavior.OnDismissListener() { 461 @Override 462 public void onDismiss(View view) { 463 view.setVisibility(View.GONE); 464 dispatchDismiss(Callback.DISMISS_EVENT_SWIPE); 465 } 466 467 @Override 468 public void onDragStateChanged(int state) { 469 switch (state) { 470 case SwipeDismissBehavior.STATE_DRAGGING: 471 case SwipeDismissBehavior.STATE_SETTLING: 472 // If the view is being dragged or settling, cancel the timeout 473 SnackbarManager.getInstance().cancelTimeout(mManagerCallback); 474 break; 475 case SwipeDismissBehavior.STATE_IDLE: 476 // If the view has been released and is idle, restore the timeout 477 SnackbarManager.getInstance().restoreTimeout(mManagerCallback); 478 break; 479 } 480 } 481 }); 482 clp.setBehavior(behavior); 483 // Also set the inset edge so that views can dodge the snackbar correctly 484 clp.insetEdge = Gravity.BOTTOM; 485 } 486 487 mTargetParent.addView(mView); 488 } 489 490 mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() { 491 @Override 492 public void onViewAttachedToWindow(View v) {} 493 494 @Override 495 public void onViewDetachedFromWindow(View v) { 496 if (isShownOrQueued()) { 497 // If we haven't already been dismissed then this event is coming from a 498 // non-user initiated action. Hence we need to make sure that we callback 499 // and keep our state up to date. We need to post the call since removeView() 500 // will call through to onDetachedFromWindow and thus overflow. 501 sHandler.post(new Runnable() { 502 @Override 503 public void run() { 504 onViewHidden(Callback.DISMISS_EVENT_MANUAL); 505 } 506 }); 507 } 508 } 509 }); 510 511 if (ViewCompat.isLaidOut(mView)) { 512 if (shouldAnimate()) { 513 // If animations are enabled, animate it in 514 animateViewIn(); 515 } else { 516 // Else if anims are disabled just call back now 517 onViewShown(); 518 } 519 } else { 520 // Otherwise, add one of our layout change listeners and show it in when laid out 521 mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() { 522 @Override 523 public void onLayoutChange(View view, int left, int top, int right, int bottom) { 524 mView.setOnLayoutChangeListener(null); 525 526 if (shouldAnimate()) { 527 // If animations are enabled, animate it in 528 animateViewIn(); 529 } else { 530 // Else if anims are disabled just call back now 531 onViewShown(); 532 } 533 } 534 }); 535 } 536 } 537 538 void animateViewIn() { 539 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 540 ViewCompat.setTranslationY(mView, mView.getHeight()); 541 ViewCompat.animate(mView) 542 .translationY(0f) 543 .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) 544 .setDuration(ANIMATION_DURATION) 545 .setListener(new ViewPropertyAnimatorListenerAdapter() { 546 @Override 547 public void onAnimationStart(View view) { 548 mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION, 549 ANIMATION_FADE_DURATION); 550 } 551 552 @Override 553 public void onAnimationEnd(View view) { 554 onViewShown(); 555 } 556 }).start(); 557 } else { 558 Animation anim = AnimationUtils.loadAnimation(mView.getContext(), 559 R.anim.design_snackbar_in); 560 anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); 561 anim.setDuration(ANIMATION_DURATION); 562 anim.setAnimationListener(new Animation.AnimationListener() { 563 @Override 564 public void onAnimationEnd(Animation animation) { 565 onViewShown(); 566 } 567 568 @Override 569 public void onAnimationStart(Animation animation) {} 570 571 @Override 572 public void onAnimationRepeat(Animation animation) {} 573 }); 574 mView.startAnimation(anim); 575 } 576 } 577 578 private void animateViewOut(final int event) { 579 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 580 ViewCompat.animate(mView) 581 .translationY(mView.getHeight()) 582 .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) 583 .setDuration(ANIMATION_DURATION) 584 .setListener(new ViewPropertyAnimatorListenerAdapter() { 585 @Override 586 public void onAnimationStart(View view) { 587 mView.animateChildrenOut(0, ANIMATION_FADE_DURATION); 588 } 589 590 @Override 591 public void onAnimationEnd(View view) { 592 onViewHidden(event); 593 } 594 }).start(); 595 } else { 596 Animation anim = AnimationUtils.loadAnimation(mView.getContext(), 597 R.anim.design_snackbar_out); 598 anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); 599 anim.setDuration(ANIMATION_DURATION); 600 anim.setAnimationListener(new Animation.AnimationListener() { 601 @Override 602 public void onAnimationEnd(Animation animation) { 603 onViewHidden(event); 604 } 605 606 @Override 607 public void onAnimationStart(Animation animation) {} 608 609 @Override 610 public void onAnimationRepeat(Animation animation) {} 611 }); 612 mView.startAnimation(anim); 613 } 614 } 615 616 final void hideView(@Callback.DismissEvent final int event) { 617 if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) { 618 animateViewOut(event); 619 } else { 620 // If anims are disabled or the view isn't visible, just call back now 621 onViewHidden(event); 622 } 623 } 624 625 void onViewShown() { 626 SnackbarManager.getInstance().onShown(mManagerCallback); 627 if (mCallback != null) { 628 mCallback.onShown(this); 629 } 630 } 631 632 void onViewHidden(int event) { 633 // First tell the SnackbarManager that it has been dismissed 634 SnackbarManager.getInstance().onDismissed(mManagerCallback); 635 // Now call the dismiss listener (if available) 636 if (mCallback != null) { 637 mCallback.onDismissed(this, event); 638 } 639 if (Build.VERSION.SDK_INT < 11) { 640 // We need to hide the Snackbar on pre-v11 since it uses an old style Animation. 641 // ViewGroup has special handling in removeView() when getAnimation() != null in 642 // that it waits. This then means that the calculated insets are wrong and the 643 // any dodging views do not return. We workaround it by setting the view to gone while 644 // ViewGroup actually gets around to removing it. 645 mView.setVisibility(View.GONE); 646 } 647 // Lastly, hide and remove the view from the parent (if attached) 648 final ViewParent parent = mView.getParent(); 649 if (parent instanceof ViewGroup) { 650 ((ViewGroup) parent).removeView(mView); 651 } 652 } 653 654 /** 655 * Returns true if we should animate the Snackbar view in/out. 656 */ 657 boolean shouldAnimate() { 658 return !mAccessibilityManager.isEnabled(); 659 } 660 661 /** 662 * @hide 663 */ 664 @RestrictTo(GROUP_ID) 665 public static class SnackbarLayout extends LinearLayout { 666 private TextView mMessageView; 667 private Button mActionView; 668 669 private int mMaxWidth; 670 private int mMaxInlineActionWidth; 671 672 interface OnLayoutChangeListener { 673 void onLayoutChange(View view, int left, int top, int right, int bottom); 674 } 675 676 interface OnAttachStateChangeListener { 677 void onViewAttachedToWindow(View v); 678 void onViewDetachedFromWindow(View v); 679 } 680 681 private OnLayoutChangeListener mOnLayoutChangeListener; 682 private OnAttachStateChangeListener mOnAttachStateChangeListener; 683 684 public SnackbarLayout(Context context) { 685 this(context, null); 686 } 687 688 public SnackbarLayout(Context context, AttributeSet attrs) { 689 super(context, attrs); 690 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout); 691 mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1); 692 mMaxInlineActionWidth = a.getDimensionPixelSize( 693 R.styleable.SnackbarLayout_maxActionInlineWidth, -1); 694 if (a.hasValue(R.styleable.SnackbarLayout_elevation)) { 695 ViewCompat.setElevation(this, a.getDimensionPixelSize( 696 R.styleable.SnackbarLayout_elevation, 0)); 697 } 698 a.recycle(); 699 700 setClickable(true); 701 702 // Now inflate our content. We need to do this manually rather than using an <include> 703 // in the layout since older versions of the Android do not inflate includes with 704 // the correct Context. 705 LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this); 706 707 ViewCompat.setAccessibilityLiveRegion(this, 708 ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE); 709 ViewCompat.setImportantForAccessibility(this, 710 ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); 711 712 // Make sure that we fit system windows and have a listener to apply any insets 713 ViewCompat.setFitsSystemWindows(this, true); 714 ViewCompat.setOnApplyWindowInsetsListener(this, 715 new android.support.v4.view.OnApplyWindowInsetsListener() { 716 @Override 717 public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { 718 // Copy over the bottom inset as padding so that we're displayed above the 719 // navigation bar 720 v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), 721 v.getPaddingRight(), insets.getSystemWindowInsetBottom()); 722 return insets; 723 } 724 }); 725 } 726 727 @Override 728 protected void onFinishInflate() { 729 super.onFinishInflate(); 730 mMessageView = (TextView) findViewById(R.id.snackbar_text); 731 mActionView = (Button) findViewById(R.id.snackbar_action); 732 } 733 734 TextView getMessageView() { 735 return mMessageView; 736 } 737 738 Button getActionView() { 739 return mActionView; 740 } 741 742 @Override 743 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 744 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 745 746 if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) { 747 widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY); 748 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 749 } 750 751 final int multiLineVPadding = getResources().getDimensionPixelSize( 752 R.dimen.design_snackbar_padding_vertical_2lines); 753 final int singleLineVPadding = getResources().getDimensionPixelSize( 754 R.dimen.design_snackbar_padding_vertical); 755 final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1; 756 757 boolean remeasure = false; 758 if (isMultiLine && mMaxInlineActionWidth > 0 759 && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) { 760 if (updateViewsWithinLayout(VERTICAL, multiLineVPadding, 761 multiLineVPadding - singleLineVPadding)) { 762 remeasure = true; 763 } 764 } else { 765 final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding; 766 if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) { 767 remeasure = true; 768 } 769 } 770 771 if (remeasure) { 772 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 773 } 774 } 775 776 void animateChildrenIn(int delay, int duration) { 777 ViewCompat.setAlpha(mMessageView, 0f); 778 ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration) 779 .setStartDelay(delay).start(); 780 781 if (mActionView.getVisibility() == VISIBLE) { 782 ViewCompat.setAlpha(mActionView, 0f); 783 ViewCompat.animate(mActionView).alpha(1f).setDuration(duration) 784 .setStartDelay(delay).start(); 785 } 786 } 787 788 void animateChildrenOut(int delay, int duration) { 789 ViewCompat.setAlpha(mMessageView, 1f); 790 ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration) 791 .setStartDelay(delay).start(); 792 793 if (mActionView.getVisibility() == VISIBLE) { 794 ViewCompat.setAlpha(mActionView, 1f); 795 ViewCompat.animate(mActionView).alpha(0f).setDuration(duration) 796 .setStartDelay(delay).start(); 797 } 798 } 799 800 @Override 801 protected void onLayout(boolean changed, int l, int t, int r, int b) { 802 super.onLayout(changed, l, t, r, b); 803 if (mOnLayoutChangeListener != null) { 804 mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b); 805 } 806 } 807 808 @Override 809 protected void onAttachedToWindow() { 810 super.onAttachedToWindow(); 811 if (mOnAttachStateChangeListener != null) { 812 mOnAttachStateChangeListener.onViewAttachedToWindow(this); 813 } 814 815 ViewCompat.requestApplyInsets(this); 816 } 817 818 @Override 819 protected void onDetachedFromWindow() { 820 super.onDetachedFromWindow(); 821 if (mOnAttachStateChangeListener != null) { 822 mOnAttachStateChangeListener.onViewDetachedFromWindow(this); 823 } 824 } 825 826 void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) { 827 mOnLayoutChangeListener = onLayoutChangeListener; 828 } 829 830 void setOnAttachStateChangeListener(OnAttachStateChangeListener listener) { 831 mOnAttachStateChangeListener = listener; 832 } 833 834 private boolean updateViewsWithinLayout(final int orientation, 835 final int messagePadTop, final int messagePadBottom) { 836 boolean changed = false; 837 if (orientation != getOrientation()) { 838 setOrientation(orientation); 839 changed = true; 840 } 841 if (mMessageView.getPaddingTop() != messagePadTop 842 || mMessageView.getPaddingBottom() != messagePadBottom) { 843 updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom); 844 changed = true; 845 } 846 return changed; 847 } 848 849 private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) { 850 if (ViewCompat.isPaddingRelative(view)) { 851 ViewCompat.setPaddingRelative(view, 852 ViewCompat.getPaddingStart(view), topPadding, 853 ViewCompat.getPaddingEnd(view), bottomPadding); 854 } else { 855 view.setPadding(view.getPaddingLeft(), topPadding, 856 view.getPaddingRight(), bottomPadding); 857 } 858 } 859 } 860 861 final class Behavior extends SwipeDismissBehavior<SnackbarLayout> { 862 @Override 863 public boolean canSwipeDismissView(View child) { 864 return child instanceof SnackbarLayout; 865 } 866 867 @Override 868 public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child, 869 MotionEvent event) { 870 // We want to make sure that we disable any Snackbar timeouts if the user is 871 // currently touching the Snackbar. We restore the timeout when complete 872 if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) { 873 switch (event.getActionMasked()) { 874 case MotionEvent.ACTION_DOWN: 875 SnackbarManager.getInstance().cancelTimeout(mManagerCallback); 876 break; 877 case MotionEvent.ACTION_UP: 878 case MotionEvent.ACTION_CANCEL: 879 SnackbarManager.getInstance().restoreTimeout(mManagerCallback); 880 break; 881 } 882 } 883 884 return super.onInterceptTouchEvent(parent, child, event); 885 } 886 } 887} 888