blob: bd5ffbab2327666bf78577e26019e0c357867b64 [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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
import android.content.res.ColorStateList;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.StringRes;
import android.support.design.R;
import android.support.design.internal.SnackbarContentLayout;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.TextView;
/**
* 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 BaseTransientBottomBar#addCallback(BaseCallback)}.</p>
*/
public final class Snackbar extends BaseTransientBottomBar<Snackbar> {
/**
* 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 = BaseTransientBottomBar.LENGTH_INDEFINITE;
/**
* Show the Snackbar for a short period of time.
*
* @see #setDuration
*/
public static final int LENGTH_SHORT = BaseTransientBottomBar.LENGTH_SHORT;
/**
* Show the Snackbar for a long period of time.
*
* @see #setDuration
*/
public static final int LENGTH_LONG = BaseTransientBottomBar.LENGTH_LONG;
/**
* Callback class for {@link Snackbar} instances.
*
* Note: this class is here to provide backwards-compatible way for apps written before
* the existence of the base {@link BaseTransientBottomBar} class.
*
* @see BaseTransientBottomBar#addCallback(BaseCallback)
*/
public static class Callback extends BaseCallback<Snackbar> {
/** Indicates that the Snackbar was dismissed via a swipe.*/
public static final int DISMISS_EVENT_SWIPE = BaseCallback.DISMISS_EVENT_SWIPE;
/** Indicates that the Snackbar was dismissed via an action click.*/
public static final int DISMISS_EVENT_ACTION = BaseCallback.DISMISS_EVENT_ACTION;
/** Indicates that the Snackbar was dismissed via a timeout.*/
public static final int DISMISS_EVENT_TIMEOUT = BaseCallback.DISMISS_EVENT_TIMEOUT;
/** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/
public static final int DISMISS_EVENT_MANUAL = BaseCallback.DISMISS_EVENT_MANUAL;
/** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/
public static final int DISMISS_EVENT_CONSECUTIVE = BaseCallback.DISMISS_EVENT_CONSECUTIVE;
@Override
public void onShown(Snackbar sb) {
// Stub implementation to make API check happy.
}
@Override
public void onDismissed(Snackbar transientBottomBar, @DismissEvent int event) {
// Stub implementation to make API check happy.
}
}
@Nullable private BaseCallback<Snackbar> mCallback;
private Snackbar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) {
super(parent, content, contentViewCallback);
}
/**
* 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) {
final ViewGroup parent = findSuitableParent(view);
if (parent == null) {
throw new IllegalArgumentException("No suitable parent found from the given view. "
+ "Please provide a valid view.");
}
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final SnackbarContentLayout content =
(SnackbarContentLayout) inflater.inflate(
R.layout.design_layout_snackbar_include, parent, false);
final Snackbar snackbar = new Snackbar(parent, content, content);
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;
}
/**
* Update the text in this {@link Snackbar}.
*
* @param message The new text for this {@link BaseTransientBottomBar}.
*/
@NonNull
public Snackbar setText(@NonNull CharSequence message) {
final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
final TextView tv = contentLayout.getMessageView();
tv.setText(message);
return this;
}
/**
* Update the text in this {@link Snackbar}.
*
* @param resId The new text for this {@link BaseTransientBottomBar}.
*/
@NonNull
public Snackbar setText(@StringRes int resId) {
return setText(getContext().getText(resId));
}
/**
* Set the action to be displayed in this {@link BaseTransientBottomBar}.
*
* @param resId String resource to display for the action
* @param listener callback to be invoked when the action is clicked
*/
@NonNull
public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) {
return setAction(getContext().getText(resId), listener);
}
/**
* Set the action to be displayed in this {@link BaseTransientBottomBar}.
*
* @param text Text to display for the action
* @param listener callback to be invoked when the action is clicked
*/
@NonNull
public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
final TextView tv = contentLayout.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(BaseCallback.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 SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
final TextView tv = contentLayout.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 SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
final TextView tv = contentLayout.getActionView();
tv.setTextColor(color);
return this;
}
/**
* Set a callback to be called when this the visibility of this {@link Snackbar}
* changes. Note that this method is deprecated
* and you should use {@link #addCallback(BaseCallback)} to add a callback and
* {@link #removeCallback(BaseCallback)} to remove a registered callback.
*
* @param callback Callback to notify when transient bottom bar events occur.
* @deprecated Use {@link #addCallback(BaseCallback)}
* @see Callback
* @see #addCallback(BaseCallback)
* @see #removeCallback(BaseCallback)
*/
@Deprecated
@NonNull
public Snackbar setCallback(Callback callback) {
// The logic in this method emulates what we had before support for multiple
// registered callbacks.
if (mCallback != null) {
removeCallback(mCallback);
}
if (callback != null) {
addCallback(callback);
}
// Update the deprecated field so that we can remove the passed callback the next
// time we're called
mCallback = callback;
return this;
}
/**
* @hide
*
* Note: this class is here to provide backwards-compatible way for apps written before
* the existence of the base {@link BaseTransientBottomBar} class.
*/
@RestrictTo(LIBRARY_GROUP)
public static final class SnackbarLayout extends BaseTransientBottomBar.SnackbarBaseLayout {
public SnackbarLayout(Context context) {
super(context);
}
public SnackbarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Work around our backwards-compatible refactoring of Snackbar and inner content
// being inflated against snackbar's parent (instead of against the snackbar itself).
// Every child that is width=MATCH_PARENT is remeasured again and given the full width
// minus the paddings.
int childCount = getChildCount();
int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getLayoutParams().width == ViewGroup.LayoutParams.MATCH_PARENT) {
child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
MeasureSpec.EXACTLY));
}
}
}
}
}