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