blob: 0fccafecd1b0d1a48130bcd0a0424516c231c258 [file] [log] [blame]
/*
* 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.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.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.
* <p>
* 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.
* <p>
* Snackbars can contain an action which is set via
* {@link #setAction(CharSequence, android.view.View.OnClickListener)}.
* <p>
* To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback}
* via {@link #setCallback(Callback)}.</p>
*/
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})
@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;
private static final int ANIMATION_DURATION = 250;
private 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 mParent;
private final Context mContext;
private final SnackbarLayout mView;
private int mDuration;
private Callback mCallback;
private Snackbar(ViewGroup parent) {
mParent = parent;
mContext = parent.getContext();
ThemeUtils.checkAppCompatTheme(mContext);
LayoutInflater inflater = LayoutInflater.from(mContext);
mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);
}
/**
* Make a Snackbar to display a message
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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 Snackbar is currently being shown.
*/
public boolean isShown() {
return mView.isShown();
}
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) {
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);
}
mParent.addView(mView);
}
if (ViewCompat.isLaidOut(mView)) {
// If the view is already laid out, animate it now
animateViewIn();
} else {
// Otherwise, add one of our layout change listeners and animate it in when laid out
mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
animateViewIn();
mView.setOnLayoutChangeListener(null);
}
});
}
}
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) {
if (mCallback != null) {
mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(mManagerCallback);
}
}).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) {
if (mCallback != null) {
mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(mManagerCallback);
}
@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(int event) {
if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) {
onViewHidden(event);
} else {
animateViewOut(event);
}
}
private void onViewHidden(int event) {
// First remove the view from the parent
mParent.removeView(mView);
// Now call the dismiss listener (if available)
if (mCallback != null) {
mCallback.onDismissed(this, event);
}
// Finally, tell the SnackbarManager that it has been dismissed
SnackbarManager.getInstance().onDismissed(mManagerCallback);
}
/**
* @return if the view is being being dragged or settled by {@link SwipeDismissBehavior}.
*/
private boolean isBeingDragged() {
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
final CoordinatorLayout.LayoutParams cllp = (CoordinatorLayout.LayoutParams) lp;
final CoordinatorLayout.Behavior behavior = cllp.getBehavior();
if (behavior instanceof SwipeDismissBehavior) {
return ((SwipeDismissBehavior) behavior).getDragState()
!= SwipeDismissBehavior.STATE_IDLE;
}
}
return false;
}
/**
* @hide
*/
public static class SnackbarLayout extends LinearLayout {
private TextView mMessageView;
private Button mActionView;
private int mMaxWidth;
private int mMaxInlineActionWidth;
interface OnLayoutChangeListener {
public void onLayoutChange(View view, int left, int top, int right, int bottom);
}
private OnLayoutChangeListener mOnLayoutChangeListener;
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 <include>
// in the layout since older versions of the Android do not inflate includes with
// the correct Context.
LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMessageView = (TextView) findViewById(R.id.snackbar_text);
mActionView = (Button) findViewById(R.id.snackbar_action);
}
TextView getMessageView() {
return mMessageView;
}
Button getActionView() {
return mActionView;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
final int multiLineVPadding = getResources().getDimensionPixelSize(
R.dimen.design_snackbar_padding_vertical_2lines);
final int singleLineVPadding = getResources().getDimensionPixelSize(
R.dimen.design_snackbar_padding_vertical);
final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1;
boolean remeasure = false;
if (isMultiLine && mMaxInlineActionWidth > 0
&& mActionView.getMeasuredWidth() > mMaxInlineActionWidth) {
if (updateViewsWithinLayout(VERTICAL, multiLineVPadding,
multiLineVPadding - singleLineVPadding)) {
remeasure = true;
}
} else {
final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding;
if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) {
remeasure = true;
}
}
if (remeasure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
void animateChildrenIn(int delay, int duration) {
ViewCompat.setAlpha(mMessageView, 0f);
ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration)
.setStartDelay(delay).start();
if (mActionView.getVisibility() == VISIBLE) {
ViewCompat.setAlpha(mActionView, 0f);
ViewCompat.animate(mActionView).alpha(1f).setDuration(duration)
.setStartDelay(delay).start();
}
}
void animateChildrenOut(int delay, int duration) {
ViewCompat.setAlpha(mMessageView, 1f);
ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration)
.setStartDelay(delay).start();
if (mActionView.getVisibility() == VISIBLE) {
ViewCompat.setAlpha(mActionView, 1f);
ViewCompat.animate(mActionView).alpha(0f).setDuration(duration)
.setStartDelay(delay).start();
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && mOnLayoutChangeListener != null) {
mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
}
}
void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) {
mOnLayoutChangeListener = onLayoutChangeListener;
}
private boolean updateViewsWithinLayout(final int orientation,
final int messagePadTop, final int messagePadBottom) {
boolean changed = false;
if (orientation != getOrientation()) {
setOrientation(orientation);
changed = true;
}
if (mMessageView.getPaddingTop() != messagePadTop
|| mMessageView.getPaddingBottom() != messagePadBottom) {
updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom);
changed = true;
}
return changed;
}
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> {
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child,
MotionEvent event) {
// We want to make sure that we disable any Snackbar timeouts if the user is
// currently touching the Snackbar. We restore the timeout when complete
if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
break;
}
}
return super.onInterceptTouchEvent(parent, child, event);
}
}
}