Source code
package android.support.design.widget;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.design.R;
import android.support.design.widget.SwipeDismissBehavior.OnDismissListener;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public final class Snackbar {
private static final int ANIMATION_DURATION = 250;
private static final int ANIMATION_FADE_DURATION = 180;
public static final int LENGTH_INDEFINITE = -2;
public static final int LENGTH_LONG = 0;
public static final int LENGTH_SHORT = -1;
private static final int MSG_DISMISS = 1;
private static final int MSG_SHOW = 0;
private static final Handler sHandler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
public boolean handleMessage(Message message) {
switch (message.what) {
case 0:
((Snackbar) message.obj).showView();
return true;
case 1:
((Snackbar) message.obj).hideView(message.arg1);
return true;
default:
return false;
}
}
});
private Callback mCallback;
private final Context mContext;
private int mDuration;
private final Callback mManagerCallback = new Callback() {
public void show() {
Snackbar.sHandler.sendMessage(Snackbar.sHandler.obtainMessage(0, Snackbar.this));
}
public void dismiss(int event) {
Snackbar.sHandler.sendMessage(Snackbar.sHandler.obtainMessage(1, event, 0, Snackbar.this));
}
};
private final ViewGroup mTargetParent;
private final SnackbarLayout mView;
public static abstract class Callback {
public static final int DISMISS_EVENT_ACTION = 1;
public static final int DISMISS_EVENT_CONSECUTIVE = 4;
public static final int DISMISS_EVENT_MANUAL = 3;
public static final int DISMISS_EVENT_SWIPE = 0;
public static final int DISMISS_EVENT_TIMEOUT = 2;
@Retention(RetentionPolicy.SOURCE)
public @interface DismissEvent {
}
public void onDismissed(Snackbar snackbar, int event) {
}
public void onShown(Snackbar snackbar) {
}
}
@Retention(RetentionPolicy.SOURCE)
public @interface Duration {
}
public static class SnackbarLayout extends LinearLayout {
private Button mActionView;
private int mMaxInlineActionWidth;
private int mMaxWidth;
private TextView mMessageView;
private OnAttachStateChangeListener mOnAttachStateChangeListener;
private OnLayoutChangeListener mOnLayoutChangeListener;
interface OnAttachStateChangeListener {
void onViewAttachedToWindow(View view);
void onViewDetachedFromWindow(View view);
}
interface OnLayoutChangeListener {
void onLayoutChange(View view, int i, int i2, int i3, int i4);
}
public SnackbarLayout(Context context) {
this(context, null);
}
public SnackbarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
this.mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
this.mMaxInlineActionWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
ViewCompat.setElevation(this, (float) a.getDimensionPixelSize(R.styleable.SnackbarLayout_elevation, 0));
}
a.recycle();
setClickable(true);
LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
ViewCompat.setAccessibilityLiveRegion(this, 1);
}
protected void onFinishInflate() {
super.onFinishInflate();
this.mMessageView = (TextView) findViewById(R.id.snackbar_text);
this.mActionView = (Button) findViewById(R.id.snackbar_action);
}
TextView getMessageView() {
return this.mMessageView;
}
Button getActionView() {
return this.mActionView;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
boolean isMultiLine;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (this.mMaxWidth > 0 && getMeasuredWidth() > this.mMaxWidth) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(this.mMaxWidth, 1073741824);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
int multiLineVPadding = getResources().getDimensionPixelSize(R.dimen.design_snackbar_padding_vertical_2lines);
int singleLineVPadding = getResources().getDimensionPixelSize(R.dimen.design_snackbar_padding_vertical);
if (this.mMessageView.getLayout().getLineCount() > 1) {
isMultiLine = true;
} else {
isMultiLine = false;
}
boolean remeasure = false;
if (!isMultiLine || this.mMaxInlineActionWidth <= 0 || this.mActionView.getMeasuredWidth() <= this.mMaxInlineActionWidth) {
int messagePadding;
if (isMultiLine) {
messagePadding = multiLineVPadding;
} else {
messagePadding = singleLineVPadding;
}
if (updateViewsWithinLayout(0, messagePadding, messagePadding)) {
remeasure = true;
}
} else if (updateViewsWithinLayout(1, multiLineVPadding, multiLineVPadding - singleLineVPadding)) {
remeasure = true;
}
if (remeasure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
void animateChildrenIn(int delay, int duration) {
ViewCompat.setAlpha(this.mMessageView, 0.0f);
ViewCompat.animate(this.mMessageView).alpha(1.0f).setDuration((long) duration).setStartDelay((long) delay).start();
if (this.mActionView.getVisibility() == 0) {
ViewCompat.setAlpha(this.mActionView, 0.0f);
ViewCompat.animate(this.mActionView).alpha(1.0f).setDuration((long) duration).setStartDelay((long) delay).start();
}
}
void animateChildrenOut(int delay, int duration) {
ViewCompat.setAlpha(this.mMessageView, 1.0f);
ViewCompat.animate(this.mMessageView).alpha(0.0f).setDuration((long) duration).setStartDelay((long) delay).start();
if (this.mActionView.getVisibility() == 0) {
ViewCompat.setAlpha(this.mActionView, 1.0f);
ViewCompat.animate(this.mActionView).alpha(0.0f).setDuration((long) duration).setStartDelay((long) delay).start();
}
}
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && this.mOnLayoutChangeListener != null) {
this.mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
}
}
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (this.mOnAttachStateChangeListener != null) {
this.mOnAttachStateChangeListener.onViewAttachedToWindow(this);
}
}
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (this.mOnAttachStateChangeListener != null) {
this.mOnAttachStateChangeListener.onViewDetachedFromWindow(this);
}
}
void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) {
this.mOnLayoutChangeListener = onLayoutChangeListener;
}
void setOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
this.mOnAttachStateChangeListener = listener;
}
private boolean updateViewsWithinLayout(int orientation, int messagePadTop, int messagePadBottom) {
boolean changed = false;
if (orientation != getOrientation()) {
setOrientation(orientation);
changed = true;
}
if (this.mMessageView.getPaddingTop() == messagePadTop && this.mMessageView.getPaddingBottom() == messagePadBottom) {
return changed;
}
updateTopBottomPadding(this.mMessageView, messagePadTop, messagePadBottom);
return true;
}
private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) {
if (ViewCompat.isPaddingRelative(view)) {
ViewCompat.setPaddingRelative(view, ViewCompat.getPaddingStart(view), topPadding, ViewCompat.getPaddingEnd(view), bottomPadding);
} else {
view.setPadding(view.getPaddingLeft(), topPadding, view.getPaddingRight(), bottomPadding);
}
}
}
final class Behavior extends SwipeDismissBehavior<SnackbarLayout> {
Behavior() {
}
public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child, MotionEvent event) {
if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
switch (event.getActionMasked()) {
case 0:
SnackbarManager.getInstance().cancelTimeout(Snackbar.this.mManagerCallback);
break;
case 1:
case 3:
SnackbarManager.getInstance().restoreTimeout(Snackbar.this.mManagerCallback);
break;
}
}
return super.onInterceptTouchEvent(parent, child, event);
}
}
private Snackbar(ViewGroup parent) {
this.mTargetParent = parent;
this.mContext = parent.getContext();
ThemeUtils.checkAppCompatTheme(this.mContext);
this.mView = (SnackbarLayout) LayoutInflater.from(this.mContext).inflate(R.layout.design_layout_snackbar, this.mTargetParent, false);
}
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {
Snackbar snackbar = new Snackbar(findSuitableParent(view));
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
@NonNull
public static Snackbar make(@NonNull View view, @StringRes int resId, int duration) {
return make(view, view.getResources().getText(resId), duration);
}
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
while (!(view instanceof CoordinatorLayout)) {
if (view instanceof FrameLayout) {
if (view.getId() == 16908290) {
return (ViewGroup) view;
}
fallback = (ViewGroup) view;
}
if (view != null) {
ViewParent parent = view.getParent();
if (parent instanceof View) {
view = (View) parent;
continue;
} else {
view = null;
continue;
}
}
if (view == null) {
return fallback;
}
}
return (ViewGroup) view;
}
@NonNull
public Snackbar setAction(@StringRes int resId, OnClickListener listener) {
return setAction(this.mContext.getText(resId), listener);
}
@NonNull
public Snackbar setAction(CharSequence text, final OnClickListener listener) {
TextView tv = this.mView.getActionView();
if (TextUtils.isEmpty(text) || listener == null) {
tv.setVisibility(8);
tv.setOnClickListener(null);
} else {
tv.setVisibility(0);
tv.setText(text);
tv.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
listener.onClick(view);
Snackbar.this.dispatchDismiss(1);
}
});
}
return this;
}
@NonNull
public Snackbar setActionTextColor(ColorStateList colors) {
this.mView.getActionView().setTextColor(colors);
return this;
}
@NonNull
public Snackbar setActionTextColor(@ColorInt int color) {
this.mView.getActionView().setTextColor(color);
return this;
}
@NonNull
public Snackbar setText(@NonNull CharSequence message) {
this.mView.getMessageView().setText(message);
return this;
}
@NonNull
public Snackbar setText(@StringRes int resId) {
return setText(this.mContext.getText(resId));
}
@NonNull
public Snackbar setDuration(int duration) {
this.mDuration = duration;
return this;
}
public int getDuration() {
return this.mDuration;
}
@NonNull
public View getView() {
return this.mView;
}
public void show() {
SnackbarManager.getInstance().show(this.mDuration, this.mManagerCallback);
}
public void dismiss() {
dispatchDismiss(3);
}
private void dispatchDismiss(int event) {
SnackbarManager.getInstance().dismiss(this.mManagerCallback, event);
}
@NonNull
public Snackbar setCallback(Callback callback) {
this.mCallback = callback;
return this;
}
public boolean isShown() {
return SnackbarManager.getInstance().isCurrent(this.mManagerCallback);
}
public boolean isShownOrQueued() {
return SnackbarManager.getInstance().isCurrentOrNext(this.mManagerCallback);
}
final void showView() {
if (this.mView.getParent() == null) {
LayoutParams lp = this.mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
Behavior behavior = new Behavior();
behavior.setStartAlphaSwipeDistance(0.1f);
behavior.setEndAlphaSwipeDistance(0.6f);
behavior.setSwipeDirection(0);
behavior.setListener(new OnDismissListener() {
public void onDismiss(View view) {
Snackbar.this.dispatchDismiss(0);
}
public void onDragStateChanged(int state) {
switch (state) {
case 0:
SnackbarManager.getInstance().restoreTimeout(Snackbar.this.mManagerCallback);
return;
case 1:
case 2:
SnackbarManager.getInstance().cancelTimeout(Snackbar.this.mManagerCallback);
return;
default:
return;
}
}
});
((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
}
this.mTargetParent.addView(this.mView);
}
this.mView.setOnAttachStateChangeListener(new OnAttachStateChangeListener() {
public void onViewAttachedToWindow(View v) {
}
public void onViewDetachedFromWindow(View v) {
if (Snackbar.this.isShownOrQueued()) {
Snackbar.sHandler.post(new Runnable() {
public void run() {
Snackbar.this.onViewHidden(3);
}
});
}
}
});
if (ViewCompat.isLaidOut(this.mView)) {
animateViewIn();
} else {
this.mView.setOnLayoutChangeListener(new OnLayoutChangeListener() {
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
Snackbar.this.animateViewIn();
Snackbar.this.mView.setOnLayoutChangeListener(null);
}
});
}
}
private void animateViewIn() {
if (VERSION.SDK_INT >= 14) {
ViewCompat.setTranslationY(this.mView, (float) this.mView.getHeight());
ViewCompat.animate(this.mView).translationY(0.0f).setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR).setDuration(250).setListener(new ViewPropertyAnimatorListenerAdapter() {
public void onAnimationStart(View view) {
Snackbar.this.mView.animateChildrenIn(70, Snackbar.ANIMATION_FADE_DURATION);
}
public void onAnimationEnd(View view) {
if (Snackbar.this.mCallback != null) {
Snackbar.this.mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(Snackbar.this.mManagerCallback);
}
}).start();
return;
}
Animation anim = AnimationUtils.loadAnimation(this.mView.getContext(), R.anim.design_snackbar_in);
anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
anim.setDuration(250);
anim.setAnimationListener(new AnimationListener() {
public void onAnimationEnd(Animation animation) {
if (Snackbar.this.mCallback != null) {
Snackbar.this.mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(Snackbar.this.mManagerCallback);
}
public void onAnimationStart(Animation animation) {
}
public void onAnimationRepeat(Animation animation) {
}
});
this.mView.startAnimation(anim);
}
private void animateViewOut(final int event) {
if (VERSION.SDK_INT >= 14) {
ViewCompat.animate(this.mView).translationY((float) this.mView.getHeight()).setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR).setDuration(250).setListener(new ViewPropertyAnimatorListenerAdapter() {
public void onAnimationStart(View view) {
Snackbar.this.mView.animateChildrenOut(0, Snackbar.ANIMATION_FADE_DURATION);
}
public void onAnimationEnd(View view) {
Snackbar.this.onViewHidden(event);
}
}).start();
return;
}
Animation anim = AnimationUtils.loadAnimation(this.mView.getContext(), R.anim.design_snackbar_out);
anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
anim.setDuration(250);
anim.setAnimationListener(new AnimationListener() {
public void onAnimationEnd(Animation animation) {
Snackbar.this.onViewHidden(event);
}
public void onAnimationStart(Animation animation) {
}
public void onAnimationRepeat(Animation animation) {
}
});
this.mView.startAnimation(anim);
}
final void hideView(int event) {
if (this.mView.getVisibility() != 0 || isBeingDragged()) {
onViewHidden(event);
} else {
animateViewOut(event);
}
}
private void onViewHidden(int event) {
ViewParent parent = this.mView.getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(this.mView);
}
if (this.mCallback != null) {
this.mCallback.onDismissed(this, event);
}
SnackbarManager.getInstance().onDismissed(this.mManagerCallback);
}
private boolean isBeingDragged() {
LayoutParams lp = this.mView.getLayoutParams();
if (!(lp instanceof CoordinatorLayout.LayoutParams)) {
return false;
}
android.support.design.widget.CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) lp).getBehavior();
if (!(behavior instanceof SwipeDismissBehavior) || ((SwipeDismissBehavior) behavior).getDragState() == 0) {
return false;
}
return true;
}
}