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