Snackbar.java revision b7f9224b1495db47eb8fd813b5912250e900770a
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.TypedArray; 21import android.os.Build; 22import android.os.Handler; 23import android.os.Looper; 24import android.os.Message; 25import android.support.annotation.IntDef; 26import android.support.annotation.StringRes; 27import android.support.design.R; 28import android.support.v4.view.ViewCompat; 29import android.support.v4.view.ViewPropertyAnimatorListenerAdapter; 30import android.text.TextUtils; 31import android.util.AttributeSet; 32import android.view.LayoutInflater; 33import android.view.MotionEvent; 34import android.view.View; 35import android.view.ViewGroup; 36import android.view.animation.Animation; 37import android.view.animation.AnimationUtils; 38import android.widget.LinearLayout; 39import android.widget.TextView; 40 41import java.lang.annotation.Retention; 42import java.lang.annotation.RetentionPolicy; 43 44import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR; 45 46/** 47 * Snackbars provide lightweight feedback about an operation. They show a brief message at the 48 * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other 49 * elements on screen and only one can be displayed at a time. 50 * <p> 51 * They automatically disappear after a timeout or after user interaction elsewhere on the screen, 52 * particularly after interactions that summon a new surface or activity. Snackbars can be swiped 53 * off screen. 54 * <p> 55 * Snackbars can contain an action which is set via 56 * {@link #setAction(CharSequence, android.view.View.OnClickListener)}. 57 */ 58public class Snackbar { 59 60 /** 61 * @hide 62 */ 63 @IntDef({LENGTH_SHORT, LENGTH_LONG}) 64 @Retention(RetentionPolicy.SOURCE) 65 public @interface Duration {} 66 67 /** 68 * Show the Snackbar for a short period of time. 69 * 70 * @see #setDuration 71 */ 72 public static final int LENGTH_SHORT = -1; 73 74 /** 75 * Show the Snackbar for a long period of time. 76 * 77 * @see #setDuration 78 */ 79 public static final int LENGTH_LONG = 0; 80 81 private static final int ANIMATION_DURATION = 250; 82 private static final int ANIMATION_FADE_DURATION = 180; 83 84 private static final Handler sHandler; 85 private static final int MSG_SHOW = 0; 86 private static final int MSG_DISMISS = 1; 87 88 static { 89 sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { 90 @Override 91 public boolean handleMessage(Message message) { 92 switch (message.what) { 93 case MSG_SHOW: 94 ((Snackbar) message.obj).showView(); 95 return true; 96 case MSG_DISMISS: 97 ((Snackbar) message.obj).hideView(); 98 return true; 99 } 100 return false; 101 } 102 }); 103 } 104 105 private final ViewGroup mParent; 106 private final Context mContext; 107 private final SnackbarLayout mView; 108 private int mDuration; 109 110 Snackbar(ViewGroup parent) { 111 mParent = parent; 112 mContext = parent.getContext(); 113 114 LayoutInflater inflater = LayoutInflater.from(mContext); 115 mView = (SnackbarLayout) inflater.inflate(R.layout.layout_snackbar, mParent, false); 116 } 117 118 /** 119 * Make a Snackbar to display a message. 120 * 121 * @param parent The parent to add Snackbars to. 122 * @param text The text to show. Can be formatted text. 123 * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link 124 * #LENGTH_LONG} 125 */ 126 public static Snackbar make(ViewGroup parent, CharSequence text, @Duration int duration) { 127 Snackbar snackbar = new Snackbar(parent); 128 snackbar.setText(text); 129 snackbar.setDuration(duration); 130 return snackbar; 131 } 132 133 /** 134 * Make a Snackbar to display a message. 135 * 136 * @param parent The parent to add Snackbars to. 137 * @param resId The resource id of the string resource to use. Can be formatted text. 138 * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link 139 * #LENGTH_LONG} 140 */ 141 public static Snackbar make(ViewGroup parent, int resId, @Duration int duration) { 142 return make(parent, parent.getResources().getText(resId), duration); 143 } 144 145 /** 146 * Set the action to be displayed in this {@link Snackbar}. 147 * 148 * @param resId String resource to display 149 * @param listener callback to be invoked when the action is clicked 150 */ 151 public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) { 152 return setAction(mContext.getText(resId), listener); 153 } 154 155 /** 156 * Set the action to be displayed in this {@link Snackbar}. 157 * 158 * @param text Text to display 159 * @param listener callback to be invoked when the action is clicked 160 */ 161 public Snackbar setAction(CharSequence text, final View.OnClickListener listener) { 162 final TextView tv = mView.getActionView(); 163 164 if (TextUtils.isEmpty(text) || listener == null) { 165 tv.setVisibility(View.GONE); 166 tv.setOnClickListener(null); 167 } else { 168 tv.setVisibility(View.VISIBLE); 169 tv.setText(text); 170 tv.setOnClickListener(new View.OnClickListener() { 171 @Override 172 public void onClick(View view) { 173 listener.onClick(view); 174 175 // Now dismiss the Snackbar 176 dismiss(); 177 } 178 }); 179 } 180 return this; 181 } 182 183 /** 184 * Update the text in this {@link Snackbar}. 185 * 186 * @param message The new text for the Toast. 187 */ 188 public Snackbar setText(CharSequence message) { 189 final TextView tv = mView.getMessageView(); 190 tv.setText(message); 191 return this; 192 } 193 194 /** 195 * Update the text in this {@link Snackbar}. 196 * 197 * @param resId The new text for the Toast. 198 */ 199 public Snackbar setText(@StringRes int resId) { 200 return setText(mContext.getText(resId)); 201 } 202 203 /** 204 * Set how long to show the view for. 205 * 206 * @param duration either be one of the predefined lengths: 207 * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration 208 * in milliseconds. 209 */ 210 public Snackbar setDuration(@Duration int duration) { 211 mDuration = duration; 212 return this; 213 } 214 215 /** 216 * Return the duration. 217 * 218 * @see #setDuration 219 */ 220 @Duration 221 public int getDuration() { 222 return mDuration; 223 } 224 225 /** 226 * Returns the {@link Snackbar}'s view. 227 */ 228 public View getView() { 229 return mView; 230 } 231 232 /** 233 * Show the {@link Snackbar}. 234 */ 235 public void show() { 236 SnackbarManager.getInstance().show(mDuration, mManagerCallback); 237 } 238 239 /** 240 * Dismiss the {@link Snackbar}. 241 */ 242 public void dismiss() { 243 SnackbarManager.getInstance().dismiss(mManagerCallback); 244 } 245 246 private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { 247 @Override 248 public void show() { 249 sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this)); 250 } 251 252 @Override 253 public void dismiss() { 254 sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, Snackbar.this)); 255 } 256 }; 257 258 final void showView() { 259 if (mView.getParent() == null) { 260 final ViewGroup.LayoutParams lp = mView.getLayoutParams(); 261 262 if (lp instanceof CoordinatorLayout.LayoutParams) { 263 // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior 264 265 final Behavior behavior = new Behavior(); 266 behavior.setStartAlphaSwipeDistance(0.1f); 267 behavior.setEndAlphaSwipeDistance(0.6f); 268 behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END); 269 behavior.setListener(new SwipeDismissBehavior.OnDismissListener() { 270 @Override 271 public void onDismiss(View view) { 272 dismiss(); 273 } 274 275 @Override 276 public void onDragStateChanged(int state) { 277 switch (state) { 278 case SwipeDismissBehavior.STATE_DRAGGING: 279 case SwipeDismissBehavior.STATE_SETTLING: 280 // If the view is being dragged or settling, cancel the timeout 281 SnackbarManager.getInstance().cancelTimeout(mManagerCallback); 282 break; 283 case SwipeDismissBehavior.STATE_IDLE: 284 // If the view has been released and is idle, restore the timeout 285 SnackbarManager.getInstance().restoreTimeout(mManagerCallback); 286 break; 287 } 288 } 289 }); 290 ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior); 291 } 292 293 mParent.addView(mView); 294 } 295 296 if (ViewCompat.isLaidOut(mView)) { 297 // If the view is already laid out, animate it now 298 animateViewIn(); 299 } else { 300 // Otherwise, add one of our layout change listeners and animate it in when laid out 301 mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() { 302 @Override 303 public void onLayoutChange(View view, int left, int top, int right, int bottom) { 304 animateViewIn(); 305 mView.setOnLayoutChangeListener(null); 306 } 307 }); 308 } 309 } 310 311 private void animateViewIn() { 312 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 313 ViewCompat.setTranslationY(mView, mView.getHeight()); 314 ViewCompat.animate(mView).translationY(0f) 315 .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) 316 .setDuration(ANIMATION_DURATION) 317 .setListener(new ViewPropertyAnimatorListenerAdapter() { 318 @Override 319 public void onAnimationStart(View view) { 320 mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION, 321 ANIMATION_FADE_DURATION); 322 } 323 324 @Override 325 public void onAnimationEnd(View view) { 326 SnackbarManager.getInstance().onShown(mManagerCallback); 327 } 328 }).start(); 329 } else { 330 Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.snackbar_in); 331 anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); 332 anim.setDuration(ANIMATION_DURATION); 333 anim.setAnimationListener(new Animation.AnimationListener() { 334 @Override 335 public void onAnimationEnd(Animation animation) { 336 SnackbarManager.getInstance().onShown(mManagerCallback); 337 } 338 339 @Override 340 public void onAnimationStart(Animation animation) {} 341 342 @Override 343 public void onAnimationRepeat(Animation animation) {} 344 }); 345 mView.startAnimation(anim); 346 } 347 } 348 349 private void animateViewOut() { 350 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 351 ViewCompat.animate(mView).translationY(mView.getHeight()) 352 .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR) 353 .setDuration(ANIMATION_DURATION) 354 .setListener(new ViewPropertyAnimatorListenerAdapter() { 355 @Override 356 public void onAnimationStart(View view) { 357 mView.animateChildrenOut(0, ANIMATION_FADE_DURATION); 358 } 359 360 @Override 361 public void onAnimationEnd(View view) { 362 onViewHidden(); 363 } 364 }).start(); 365 } else { 366 Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.snackbar_out); 367 anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR); 368 anim.setDuration(ANIMATION_DURATION); 369 anim.setAnimationListener(new Animation.AnimationListener() { 370 @Override 371 public void onAnimationEnd(Animation animation) { 372 onViewHidden(); 373 } 374 375 @Override 376 public void onAnimationStart(Animation animation) {} 377 378 @Override 379 public void onAnimationRepeat(Animation animation) {} 380 }); 381 mView.startAnimation(anim); 382 } 383 } 384 385 final void hideView() { 386 if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) { 387 onViewHidden(); 388 } else { 389 animateViewOut(); 390 } 391 } 392 393 private void onViewHidden() { 394 // First remove the view from the parent 395 mParent.removeView(mView); 396 // Now, tell the SnackbarManager that it has been dismissed 397 SnackbarManager.getInstance().onDismissed(mManagerCallback); 398 } 399 400 /** 401 * @return if the view is being being dragged or settled by {@link SwipeDismissBehavior}. 402 */ 403 private boolean isBeingDragged() { 404 final ViewGroup.LayoutParams lp = mView.getLayoutParams(); 405 406 if (lp instanceof CoordinatorLayout.LayoutParams) { 407 final CoordinatorLayout.LayoutParams cllp = (CoordinatorLayout.LayoutParams) lp; 408 final CoordinatorLayout.Behavior behavior = cllp.getBehavior(); 409 410 if (behavior instanceof SwipeDismissBehavior) { 411 return ((SwipeDismissBehavior) behavior).getDragState() 412 != SwipeDismissBehavior.STATE_IDLE; 413 } 414 } 415 return false; 416 } 417 418 /** 419 * @hide 420 */ 421 public static class SnackbarLayout extends LinearLayout { 422 private TextView mMessageView; 423 private TextView mActionView; 424 425 private int mMaxWidth; 426 private int mMaxInlineActionWidth; 427 428 interface OnLayoutChangeListener { 429 public void onLayoutChange(View view, int left, int top, int right, int bottom); 430 } 431 432 private OnLayoutChangeListener mOnLayoutChangeListener; 433 434 public SnackbarLayout(Context context) { 435 this(context, null); 436 } 437 438 public SnackbarLayout(Context context, AttributeSet attrs) { 439 super(context, attrs); 440 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout); 441 mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1); 442 mMaxInlineActionWidth = a.getDimensionPixelSize( 443 R.styleable.SnackbarLayout_maxActionInlineWidth, -1); 444 a.recycle(); 445 446 setClickable(true); 447 448 // Now inflate our content. We need to do this manually rather than using an <include> 449 // in the layout since older versions of the Android do not inflate includes with 450 // the correct Context. 451 LayoutInflater.from(context).inflate(R.layout.layout_snackbar_include, this); 452 } 453 454 @Override 455 protected void onFinishInflate() { 456 super.onFinishInflate(); 457 mMessageView = (TextView) findViewById(R.id.snackbar_text); 458 mActionView = (TextView) findViewById(R.id.snackbar_action); 459 } 460 461 TextView getMessageView() { 462 return mMessageView; 463 } 464 465 TextView getActionView() { 466 return mActionView; 467 } 468 469 @Override 470 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 471 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 472 473 if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) { 474 widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY); 475 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 476 } 477 478 final int multiLineVPadding = getResources().getDimensionPixelSize( 479 R.dimen.snackbar_padding_vertical_2lines); 480 final int singleLineVPadding = getResources().getDimensionPixelSize( 481 R.dimen.snackbar_padding_vertical); 482 final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1; 483 484 boolean remeasure = false; 485 if (isMultiLine && mMaxInlineActionWidth > 0 486 && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) { 487 if (updateViewsWithinLayout(VERTICAL, multiLineVPadding, 488 multiLineVPadding - singleLineVPadding)) { 489 remeasure = true; 490 } 491 } else { 492 final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding; 493 if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) { 494 remeasure = true; 495 } 496 } 497 498 if (remeasure) { 499 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 500 } 501 } 502 503 void animateChildrenIn(int delay, int duration) { 504 ViewCompat.setAlpha(mMessageView, 0f); 505 ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration) 506 .setStartDelay(delay).start(); 507 508 if (mActionView.getVisibility() == VISIBLE) { 509 ViewCompat.setAlpha(mActionView, 0f); 510 ViewCompat.animate(mActionView).alpha(1f).setDuration(duration) 511 .setStartDelay(delay).start(); 512 } 513 } 514 515 void animateChildrenOut(int delay, int duration) { 516 ViewCompat.setAlpha(mMessageView, 1f); 517 ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration) 518 .setStartDelay(delay).start(); 519 520 if (mActionView.getVisibility() == VISIBLE) { 521 ViewCompat.setAlpha(mActionView, 1f); 522 ViewCompat.animate(mActionView).alpha(0f).setDuration(duration) 523 .setStartDelay(delay).start(); 524 } 525 } 526 527 @Override 528 protected void onLayout(boolean changed, int l, int t, int r, int b) { 529 super.onLayout(changed, l, t, r, b); 530 if (changed && mOnLayoutChangeListener != null) { 531 mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b); 532 } 533 } 534 535 void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) { 536 mOnLayoutChangeListener = onLayoutChangeListener; 537 } 538 539 private boolean updateViewsWithinLayout(final int orientation, 540 final int messagePadTop, final int messagePadBottom) { 541 boolean changed = false; 542 if (orientation != getOrientation()) { 543 setOrientation(orientation); 544 changed = true; 545 } 546 if (mMessageView.getPaddingTop() != messagePadTop 547 || mMessageView.getPaddingBottom() != messagePadBottom) { 548 updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom); 549 changed = true; 550 } 551 return changed; 552 } 553 554 private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) { 555 if (ViewCompat.isPaddingRelative(view)) { 556 ViewCompat.setPaddingRelative(view, 557 ViewCompat.getPaddingStart(view), topPadding, 558 ViewCompat.getPaddingEnd(view), bottomPadding); 559 } else { 560 view.setPadding(view.getPaddingLeft(), topPadding, 561 view.getPaddingRight(), bottomPadding); 562 } 563 } 564 } 565 566 final class Behavior extends SwipeDismissBehavior<SnackbarLayout> { 567 @Override 568 public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child, 569 MotionEvent event) { 570 // We want to make sure that we disable any Snackbar timeouts if the user is 571 // currently touching the Snackbar. We restore the timeout when complete 572 if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) { 573 switch (event.getActionMasked()) { 574 case MotionEvent.ACTION_DOWN: 575 SnackbarManager.getInstance().cancelTimeout(mManagerCallback); 576 break; 577 case MotionEvent.ACTION_UP: 578 case MotionEvent.ACTION_CANCEL: 579 SnackbarManager.getInstance().restoreTimeout(mManagerCallback); 580 break; 581 } 582 } 583 584 return super.onInterceptTouchEvent(parent, child, event); 585 } 586 } 587} 588