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