/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.design.widget; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.ColorInt; import android.support.annotation.IntDef; import android.support.annotation.IntRange; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.design.R; 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.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityManager; import android.view.animation.Animation; 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; import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR; /** * Snackbars provide lightweight feedback about an operation. They show a brief message at the * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other * elements on screen and only one can be displayed at a time. *
* They automatically disappear after a timeout or after user interaction elsewhere on the screen, * particularly after interactions that summon a new surface or activity. Snackbars can be swiped * off screen. *
* Snackbars can contain an action which is set via * {@link #setAction(CharSequence, android.view.View.OnClickListener)}. *
* To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback} * via {@link #setCallback(Callback)}.
*/ public final class Snackbar { /** * Callback class for {@link Snackbar} instances. * * @see Snackbar#setCallback(Callback) */ public static abstract class Callback { /** Indicates that the Snackbar was dismissed via a swipe.*/ public static final int DISMISS_EVENT_SWIPE = 0; /** Indicates that the Snackbar was dismissed via an action click.*/ public static final int DISMISS_EVENT_ACTION = 1; /** Indicates that the Snackbar was dismissed via a timeout.*/ public static final int DISMISS_EVENT_TIMEOUT = 2; /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/ public static final int DISMISS_EVENT_MANUAL = 3; /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/ public static final int DISMISS_EVENT_CONSECUTIVE = 4; /** @hide */ @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT, DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE}) @Retention(RetentionPolicy.SOURCE) public @interface DismissEvent {} /** * Called when the given {@link Snackbar} has been dismissed, either through a time-out, * having been manually dismissed, or an action being clicked. * * @param snackbar The snackbar which has been dismissed. * @param event The event which caused the dismissal. One of either: * {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION}, * {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or * {@link #DISMISS_EVENT_CONSECUTIVE}. * * @see Snackbar#dismiss() */ public void onDismissed(Snackbar snackbar, @DismissEvent int event) { // empty } /** * Called when the given {@link Snackbar} is visible. * * @param snackbar The snackbar which is now visible. * @see Snackbar#show() */ public void onShown(Snackbar snackbar) { // empty } } /** * @hide */ @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG}) @IntRange(from = 1) @Retention(RetentionPolicy.SOURCE) public @interface Duration {} /** * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown. * * @see #setDuration */ public static final int LENGTH_INDEFINITE = -2; /** * Show the Snackbar for a short period of time. * * @see #setDuration */ public static final int LENGTH_SHORT = -1; /** * Show the Snackbar for a long period of time. * * @see #setDuration */ public static final int LENGTH_LONG = 0; static final int ANIMATION_DURATION = 250; static final int ANIMATION_FADE_DURATION = 180; private static final Handler sHandler; private static final int MSG_SHOW = 0; private static final int MSG_DISMISS = 1; static { sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message message) { switch (message.what) { case MSG_SHOW: ((Snackbar) message.obj).showView(); return true; case MSG_DISMISS: ((Snackbar) message.obj).hideView(message.arg1); return true; } return false; } }); } private final ViewGroup mTargetParent; private final Context mContext; private final SnackbarLayout mView; private int mDuration; private Callback mCallback; private final AccessibilityManager mAccessibilityManager; private Snackbar(ViewGroup parent) { mTargetParent = parent; mContext = parent.getContext(); ThemeUtils.checkAppCompatTheme(mContext); LayoutInflater inflater = LayoutInflater.from(mContext); mView = (SnackbarLayout) inflater.inflate( R.layout.design_layout_snackbar, mTargetParent, false); mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); } /** * Make a Snackbar to display a message * *Snackbar will try and find a parent view to hold Snackbar's view from the value given * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent, * which is defined as a {@link CoordinatorLayout} or the window decor's content view, * whichever comes first. * *
Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable * certain features, such as swipe-to-dismiss and automatically moving of widgets like * {@link FloatingActionButton}. * * @param view The view to find a parent from. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link * #LENGTH_LONG} */ @NonNull public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration) { Snackbar snackbar = new Snackbar(findSuitableParent(view)); snackbar.setText(text); snackbar.setDuration(duration); return snackbar; } /** * Make a Snackbar to display a message. * *
Snackbar will try and find a parent view to hold Snackbar's view from the value given * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent, * which is defined as a {@link CoordinatorLayout} or the window decor's content view, * whichever comes first. * *
Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
* certain features, such as swipe-to-dismiss and automatically moving of widgets like
* {@link FloatingActionButton}.
*
* @param view The view to find a parent from.
* @param resId The resource id of the string resource to use. Can be formatted text.
* @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
* #LENGTH_LONG}
*/
@NonNull
public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {
return make(view, view.getResources().getText(resId), duration);
}
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
// We've found a CoordinatorLayout, use it
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
if (view.getId() == android.R.id.content) {
// If we've hit the decor content view, then we didn't find a CoL in the
// hierarchy, so use it.
return (ViewGroup) view;
} else {
// It's not the content view but we'll use it as our fallback
fallback = (ViewGroup) view;
}
}
if (view != null) {
// Else, we will loop and crawl up the view hierarchy and try to find a parent
final ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
} while (view != null);
// If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
return fallback;
}
/**
* Set the action to be displayed in this {@link Snackbar}.
*
* @param resId String resource to display
* @param listener callback to be invoked when the action is clicked
*/
@NonNull
public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) {
return setAction(mContext.getText(resId), listener);
}
/**
* Set the action to be displayed in this {@link Snackbar}.
*
* @param text Text to display
* @param listener callback to be invoked when the action is clicked
*/
@NonNull
public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
final TextView tv = mView.getActionView();
if (TextUtils.isEmpty(text) || listener == null) {
tv.setVisibility(View.GONE);
tv.setOnClickListener(null);
} else {
tv.setVisibility(View.VISIBLE);
tv.setText(text);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
listener.onClick(view);
// Now dismiss the Snackbar
dispatchDismiss(Callback.DISMISS_EVENT_ACTION);
}
});
}
return this;
}
/**
* Sets the text color of the action specified in
* {@link #setAction(CharSequence, View.OnClickListener)}.
*/
@NonNull
public Snackbar setActionTextColor(ColorStateList colors) {
final TextView tv = mView.getActionView();
tv.setTextColor(colors);
return this;
}
/**
* Sets the text color of the action specified in
* {@link #setAction(CharSequence, View.OnClickListener)}.
*/
@NonNull
public Snackbar setActionTextColor(@ColorInt int color) {
final TextView tv = mView.getActionView();
tv.setTextColor(color);
return this;
}
/**
* Update the text in this {@link Snackbar}.
*
* @param message The new text for the Toast.
*/
@NonNull
public Snackbar setText(@NonNull CharSequence message) {
final TextView tv = mView.getMessageView();
tv.setText(message);
return this;
}
/**
* Update the text in this {@link Snackbar}.
*
* @param resId The new text for the Toast.
*/
@NonNull
public Snackbar setText(@StringRes int resId) {
return setText(mContext.getText(resId));
}
/**
* Set how long to show the view for.
*
* @param duration either be one of the predefined lengths:
* {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration
* in milliseconds.
*/
@NonNull
public Snackbar setDuration(@Duration int duration) {
mDuration = duration;
return this;
}
/**
* Return the duration.
*
* @see #setDuration
*/
@Duration
public int getDuration() {
return mDuration;
}
/**
* Returns the {@link Snackbar}'s view.
*/
@NonNull
public View getView() {
return mView;
}
/**
* Show the {@link Snackbar}.
*/
public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
/**
* Dismiss the {@link Snackbar}.
*/
public void dismiss() {
dispatchDismiss(Callback.DISMISS_EVENT_MANUAL);
}
private void dispatchDismiss(@Callback.DismissEvent int event) {
SnackbarManager.getInstance().dismiss(mManagerCallback, event);
}
/**
* Set a callback to be called when this the visibility of this {@link Snackbar} changes.
*/
@NonNull
public Snackbar setCallback(Callback callback) {
mCallback = callback;
return this;
}
/**
* Return whether this {@link Snackbar} is currently being shown.
*/
public boolean isShown() {
return SnackbarManager.getInstance().isCurrent(mManagerCallback);
}
/**
* Returns whether this {@link Snackbar} is currently being shown, or is queued to be
* shown next.
*/
public boolean isShownOrQueued() {
return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);
}
private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
@Override
public void show() {
sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
}
@Override
public void dismiss(int event) {
sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
}
};
final void showView() {
if (mView.getParent() == null) {
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
// If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
final Behavior behavior = new Behavior();
behavior.setStartAlphaSwipeDistance(0.1f);
behavior.setEndAlphaSwipeDistance(0.6f);
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
@Override
public void onDismiss(View view) {
view.setVisibility(View.GONE);
dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
}
@Override
public void onDragStateChanged(int state) {
switch (state) {
case SwipeDismissBehavior.STATE_DRAGGING:
case SwipeDismissBehavior.STATE_SETTLING:
// If the view is being dragged or settling, cancel the timeout
SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
break;
case SwipeDismissBehavior.STATE_IDLE:
// If the view has been released and is idle, restore the timeout
SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
break;
}
}
});
((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
}
mTargetParent.addView(mView);
}
mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {}
@Override
public void onViewDetachedFromWindow(View v) {
if (isShownOrQueued()) {
// If we haven't already been dismissed then this event is coming from a
// non-user initiated action. Hence we need to make sure that we callback
// and keep our state up to date. We need to post the call since removeView()
// will call through to onDetachedFromWindow and thus overflow.
sHandler.post(new Runnable() {
@Override
public void run() {
onViewHidden(Callback.DISMISS_EVENT_MANUAL);
}
});
}
}
});
if (ViewCompat.isLaidOut(mView)) {
if (shouldAnimate()) {
// If animations are enabled, animate it in
animateViewIn();
} else {
// Else if anims are disabled just call back now
onViewShown();
}
} else {
// Otherwise, add one of our layout change listeners and show it in when laid out
mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
mView.setOnLayoutChangeListener(null);
if (shouldAnimate()) {
// If animations are enabled, animate it in
animateViewIn();
} else {
// Else if anims are disabled just call back now
onViewShown();
}
}
});
}
}
private void animateViewIn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ViewCompat.setTranslationY(mView, mView.getHeight());
ViewCompat.animate(mView)
.translationY(0f)
.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
.setDuration(ANIMATION_DURATION)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationStart(View view) {
mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,
ANIMATION_FADE_DURATION);
}
@Override
public void onAnimationEnd(View view) {
onViewShown();
}
}).start();
} else {
Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
R.anim.design_snackbar_in);
anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
anim.setDuration(ANIMATION_DURATION);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
onViewShown();
}
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
});
mView.startAnimation(anim);
}
}
private void animateViewOut(final int event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ViewCompat.animate(mView)
.translationY(mView.getHeight())
.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
.setDuration(ANIMATION_DURATION)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationStart(View view) {
mView.animateChildrenOut(0, ANIMATION_FADE_DURATION);
}
@Override
public void onAnimationEnd(View view) {
onViewHidden(event);
}
}).start();
} else {
Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
R.anim.design_snackbar_out);
anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
anim.setDuration(ANIMATION_DURATION);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
onViewHidden(event);
}
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
});
mView.startAnimation(anim);
}
}
final void hideView(@Callback.DismissEvent final int event) {
if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) {
animateViewOut(event);
} else {
// If anims are disabled or the view isn't visible, just call back now
onViewHidden(event);
}
}
private void onViewShown() {
SnackbarManager.getInstance().onShown(mManagerCallback);
if (mCallback != null) {
mCallback.onShown(this);
}
}
private void onViewHidden(int event) {
// First tell the SnackbarManager that it has been dismissed
SnackbarManager.getInstance().onDismissed(mManagerCallback);
// Now call the dismiss listener (if available)
if (mCallback != null) {
mCallback.onDismissed(this, event);
}
// Lastly, remove the view from the parent (if attached)
final ViewParent parent = mView.getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(mView);
}
}
/**
* Returns true if we should animate the Snackbar view in/out.
*/
private boolean shouldAnimate() {
return !mAccessibilityManager.isEnabled();
}
/**
* @hide
*/
public static class SnackbarLayout extends LinearLayout {
private TextView mMessageView;
private Button mActionView;
private int mMaxWidth;
private int mMaxInlineActionWidth;
interface OnLayoutChangeListener {
void onLayoutChange(View view, int left, int top, int right, int bottom);
}
interface OnAttachStateChangeListener {
void onViewAttachedToWindow(View v);
void onViewDetachedFromWindow(View v);
}
private OnLayoutChangeListener mOnLayoutChangeListener;
private OnAttachStateChangeListener mOnAttachStateChangeListener;
public SnackbarLayout(Context context) {
this(context, null);
}
public SnackbarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
mMaxInlineActionWidth = a.getDimensionPixelSize(
R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
ViewCompat.setElevation(this, a.getDimensionPixelSize(
R.styleable.SnackbarLayout_elevation, 0));
}
a.recycle();
setClickable(true);
// Now inflate our content. We need to do this manually rather than using an