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