/* * 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)); } } } } }