Snackbar.java revision a89b7a8e4840e52dfda1442bcb885686680556f6
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.design.widget;
18
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.TypedArray;
22import android.os.Build;
23import android.os.Handler;
24import android.os.Looper;
25import android.os.Message;
26import android.support.annotation.ColorInt;
27import android.support.annotation.IntDef;
28import android.support.annotation.NonNull;
29import android.support.annotation.StringRes;
30import android.support.design.R;
31import android.support.v4.view.ViewCompat;
32import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
33import android.text.TextUtils;
34import android.util.AttributeSet;
35import android.view.LayoutInflater;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.ViewGroup;
39import android.view.ViewParent;
40import android.view.accessibility.AccessibilityManager;
41import android.view.animation.Animation;
42import android.view.animation.AnimationUtils;
43import android.widget.Button;
44import android.widget.FrameLayout;
45import android.widget.LinearLayout;
46import android.widget.TextView;
47
48import java.lang.annotation.Retention;
49import java.lang.annotation.RetentionPolicy;
50
51import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR;
52
53/**
54 * Snackbars provide lightweight feedback about an operation. They show a brief message at the
55 * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other
56 * elements on screen and only one can be displayed at a time.
57 * <p>
58 * They automatically disappear after a timeout or after user interaction elsewhere on the screen,
59 * particularly after interactions that summon a new surface or activity. Snackbars can be swiped
60 * off screen.
61 * <p>
62 * Snackbars can contain an action which is set via
63 * {@link #setAction(CharSequence, android.view.View.OnClickListener)}.
64 * <p>
65 * To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback}
66 * via {@link #setCallback(Callback)}.</p>
67 */
68public final class Snackbar {
69
70    /**
71     * Callback class for {@link Snackbar} instances.
72     *
73     * @see Snackbar#setCallback(Callback)
74     */
75    public static abstract class Callback {
76        /** Indicates that the Snackbar was dismissed via a swipe.*/
77        public static final int DISMISS_EVENT_SWIPE = 0;
78        /** Indicates that the Snackbar was dismissed via an action click.*/
79        public static final int DISMISS_EVENT_ACTION = 1;
80        /** Indicates that the Snackbar was dismissed via a timeout.*/
81        public static final int DISMISS_EVENT_TIMEOUT = 2;
82        /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/
83        public static final int DISMISS_EVENT_MANUAL = 3;
84        /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/
85        public static final int DISMISS_EVENT_CONSECUTIVE = 4;
86
87        /** @hide */
88        @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT,
89                DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE})
90        @Retention(RetentionPolicy.SOURCE)
91        public @interface DismissEvent {}
92
93        /**
94         * Called when the given {@link Snackbar} has been dismissed, either through a time-out,
95         * having been manually dismissed, or an action being clicked.
96         *
97         * @param snackbar The snackbar which has been dismissed.
98         * @param event The event which caused the dismissal. One of either:
99         *              {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION},
100         *              {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or
101         *              {@link #DISMISS_EVENT_CONSECUTIVE}.
102         *
103         * @see Snackbar#dismiss()
104         */
105        public void onDismissed(Snackbar snackbar, @DismissEvent int event) {
106            // empty
107        }
108
109        /**
110         * Called when the given {@link Snackbar} is visible.
111         *
112         * @param snackbar The snackbar which is now visible.
113         * @see Snackbar#show()
114         */
115        public void onShown(Snackbar snackbar) {
116            // empty
117        }
118    }
119
120    /**
121     * @hide
122     */
123    @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG})
124    @Retention(RetentionPolicy.SOURCE)
125    public @interface Duration {}
126
127    /**
128     * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time
129     * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown.
130     *
131     * @see #setDuration
132     */
133    public static final int LENGTH_INDEFINITE = -2;
134
135    /**
136     * Show the Snackbar for a short period of time.
137     *
138     * @see #setDuration
139     */
140    public static final int LENGTH_SHORT = -1;
141
142    /**
143     * Show the Snackbar for a long period of time.
144     *
145     * @see #setDuration
146     */
147    public static final int LENGTH_LONG = 0;
148
149    static final int ANIMATION_DURATION = 250;
150    static final int ANIMATION_FADE_DURATION = 180;
151
152    private static final Handler sHandler;
153    private static final int MSG_SHOW = 0;
154    private static final int MSG_DISMISS = 1;
155
156    static {
157        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
158            @Override
159            public boolean handleMessage(Message message) {
160                switch (message.what) {
161                    case MSG_SHOW:
162                        ((Snackbar) message.obj).showView();
163                        return true;
164                    case MSG_DISMISS:
165                        ((Snackbar) message.obj).hideView(message.arg1);
166                        return true;
167                }
168                return false;
169            }
170        });
171    }
172
173    private final ViewGroup mTargetParent;
174    private final Context mContext;
175    private final SnackbarLayout mView;
176    private int mDuration;
177    private Callback mCallback;
178
179    private final AccessibilityManager mAccessibilityManager;
180
181    private Snackbar(ViewGroup parent) {
182        mTargetParent = parent;
183        mContext = parent.getContext();
184
185        ThemeUtils.checkAppCompatTheme(mContext);
186
187        LayoutInflater inflater = LayoutInflater.from(mContext);
188        mView = (SnackbarLayout) inflater.inflate(
189                R.layout.design_layout_snackbar, mTargetParent, false);
190
191        mAccessibilityManager = (AccessibilityManager)
192                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
193    }
194
195    /**
196     * Make a Snackbar to display a message
197     *
198     * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
199     * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
200     * which is defined as a {@link CoordinatorLayout} or the window decor's content view,
201     * whichever comes first.
202     *
203     * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
204     * certain features, such as swipe-to-dismiss and automatically moving of widgets like
205     * {@link FloatingActionButton}.
206     *
207     * @param view     The view to find a parent from.
208     * @param text     The text to show.  Can be formatted text.
209     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or {@link
210     *                 #LENGTH_LONG}
211     */
212    @NonNull
213    public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
214            @Duration int duration) {
215        Snackbar snackbar = new Snackbar(findSuitableParent(view));
216        snackbar.setText(text);
217        snackbar.setDuration(duration);
218        return snackbar;
219    }
220
221    /**
222     * Make a Snackbar to display a message.
223     *
224     * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
225     * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
226     * which is defined as a {@link CoordinatorLayout} or the window decor's content view,
227     * whichever comes first.
228     *
229     * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
230     * certain features, such as swipe-to-dismiss and automatically moving of widgets like
231     * {@link FloatingActionButton}.
232     *
233     * @param view     The view to find a parent from.
234     * @param resId    The resource id of the string resource to use. Can be formatted text.
235     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or {@link
236     *                 #LENGTH_LONG}
237     */
238    @NonNull
239    public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {
240        return make(view, view.getResources().getText(resId), duration);
241    }
242
243    private static ViewGroup findSuitableParent(View view) {
244        ViewGroup fallback = null;
245        do {
246            if (view instanceof CoordinatorLayout) {
247                // We've found a CoordinatorLayout, use it
248                return (ViewGroup) view;
249            } else if (view instanceof FrameLayout) {
250                if (view.getId() == android.R.id.content) {
251                    // If we've hit the decor content view, then we didn't find a CoL in the
252                    // hierarchy, so use it.
253                    return (ViewGroup) view;
254                } else {
255                    // It's not the content view but we'll use it as our fallback
256                    fallback = (ViewGroup) view;
257                }
258            }
259
260            if (view != null) {
261                // Else, we will loop and crawl up the view hierarchy and try to find a parent
262                final ViewParent parent = view.getParent();
263                view = parent instanceof View ? (View) parent : null;
264            }
265        } while (view != null);
266
267        // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
268        return fallback;
269    }
270
271    /**
272     * Set the action to be displayed in this {@link Snackbar}.
273     *
274     * @param resId    String resource to display
275     * @param listener callback to be invoked when the action is clicked
276     */
277    @NonNull
278    public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) {
279        return setAction(mContext.getText(resId), listener);
280    }
281
282    /**
283     * Set the action to be displayed in this {@link Snackbar}.
284     *
285     * @param text     Text to display
286     * @param listener callback to be invoked when the action is clicked
287     */
288    @NonNull
289    public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
290        final TextView tv = mView.getActionView();
291
292        if (TextUtils.isEmpty(text) || listener == null) {
293            tv.setVisibility(View.GONE);
294            tv.setOnClickListener(null);
295        } else {
296            tv.setVisibility(View.VISIBLE);
297            tv.setText(text);
298            tv.setOnClickListener(new View.OnClickListener() {
299                @Override
300                public void onClick(View view) {
301                    listener.onClick(view);
302                    // Now dismiss the Snackbar
303                    dispatchDismiss(Callback.DISMISS_EVENT_ACTION);
304                }
305            });
306        }
307        return this;
308    }
309
310    /**
311     * Sets the text color of the action specified in
312     * {@link #setAction(CharSequence, View.OnClickListener)}.
313     */
314    @NonNull
315    public Snackbar setActionTextColor(ColorStateList colors) {
316        final TextView tv = mView.getActionView();
317        tv.setTextColor(colors);
318        return this;
319    }
320
321    /**
322     * Sets the text color of the action specified in
323     * {@link #setAction(CharSequence, View.OnClickListener)}.
324     */
325    @NonNull
326    public Snackbar setActionTextColor(@ColorInt int color) {
327        final TextView tv = mView.getActionView();
328        tv.setTextColor(color);
329        return this;
330    }
331
332    /**
333     * Update the text in this {@link Snackbar}.
334     *
335     * @param message The new text for the Toast.
336     */
337    @NonNull
338    public Snackbar setText(@NonNull CharSequence message) {
339        final TextView tv = mView.getMessageView();
340        tv.setText(message);
341        return this;
342    }
343
344    /**
345     * Update the text in this {@link Snackbar}.
346     *
347     * @param resId The new text for the Toast.
348     */
349    @NonNull
350    public Snackbar setText(@StringRes int resId) {
351        return setText(mContext.getText(resId));
352    }
353
354    /**
355     * Set how long to show the view for.
356     *
357     * @param duration either be one of the predefined lengths:
358     *                 {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration
359     *                 in milliseconds.
360     */
361    @NonNull
362    public Snackbar setDuration(@Duration int duration) {
363        mDuration = duration;
364        return this;
365    }
366
367    /**
368     * Return the duration.
369     *
370     * @see #setDuration
371     */
372    @Duration
373    public int getDuration() {
374        return mDuration;
375    }
376
377    /**
378     * Returns the {@link Snackbar}'s view.
379     */
380    @NonNull
381    public View getView() {
382        return mView;
383    }
384
385    /**
386     * Show the {@link Snackbar}.
387     */
388    public void show() {
389        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
390    }
391
392    /**
393     * Dismiss the {@link Snackbar}.
394     */
395    public void dismiss() {
396        dispatchDismiss(Callback.DISMISS_EVENT_MANUAL);
397    }
398
399    private void dispatchDismiss(@Callback.DismissEvent int event) {
400        SnackbarManager.getInstance().dismiss(mManagerCallback, event);
401    }
402
403    /**
404     * Set a callback to be called when this the visibility of this {@link Snackbar} changes.
405     */
406    @NonNull
407    public Snackbar setCallback(Callback callback) {
408        mCallback = callback;
409        return this;
410    }
411
412    /**
413     * Return whether this {@link Snackbar} is currently being shown.
414     */
415    public boolean isShown() {
416        return SnackbarManager.getInstance().isCurrent(mManagerCallback);
417    }
418
419    /**
420     * Returns whether this {@link Snackbar} is currently being shown, or is queued to be
421     * shown next.
422     */
423    public boolean isShownOrQueued() {
424        return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);
425    }
426
427    private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
428        @Override
429        public void show() {
430            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
431        }
432
433        @Override
434        public void dismiss(int event) {
435            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
436        }
437    };
438
439    final void showView() {
440        if (mView.getParent() == null) {
441            final ViewGroup.LayoutParams lp = mView.getLayoutParams();
442
443            if (lp instanceof CoordinatorLayout.LayoutParams) {
444                // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
445
446                final Behavior behavior = new Behavior();
447                behavior.setStartAlphaSwipeDistance(0.1f);
448                behavior.setEndAlphaSwipeDistance(0.6f);
449                behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
450                behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
451                    @Override
452                    public void onDismiss(View view) {
453                        view.setVisibility(View.GONE);
454                        dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);
455                    }
456
457                    @Override
458                    public void onDragStateChanged(int state) {
459                        switch (state) {
460                            case SwipeDismissBehavior.STATE_DRAGGING:
461                            case SwipeDismissBehavior.STATE_SETTLING:
462                                // If the view is being dragged or settling, cancel the timeout
463                                SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
464                                break;
465                            case SwipeDismissBehavior.STATE_IDLE:
466                                // If the view has been released and is idle, restore the timeout
467                                SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
468                                break;
469                        }
470                    }
471                });
472                ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
473            }
474
475            mTargetParent.addView(mView);
476        }
477
478        mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() {
479            @Override
480            public void onViewAttachedToWindow(View v) {}
481
482            @Override
483            public void onViewDetachedFromWindow(View v) {
484                if (isShownOrQueued()) {
485                    // If we haven't already been dismissed then this event is coming from a
486                    // non-user initiated action. Hence we need to make sure that we callback
487                    // and keep our state up to date. We need to post the call since removeView()
488                    // will call through to onDetachedFromWindow and thus overflow.
489                    sHandler.post(new Runnable() {
490                        @Override
491                        public void run() {
492                            onViewHidden(Callback.DISMISS_EVENT_MANUAL);
493                        }
494                    });
495                }
496            }
497        });
498
499        if (ViewCompat.isLaidOut(mView)) {
500            // If the view is already laid out, animate it now
501            animateViewIn();
502        } else {
503            // Otherwise, add one of our layout change listeners and animate it in when laid out
504            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
505                @Override
506                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
507                    animateViewIn();
508                    mView.setOnLayoutChangeListener(null);
509                }
510            });
511        }
512    }
513
514    private void animateViewIn() {
515        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
516            ViewCompat.setTranslationY(mView, mView.getHeight());
517            ViewCompat.animate(mView)
518                    .translationY(0f)
519                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
520                    .setDuration(ANIMATION_DURATION)
521                    .setListener(new ViewPropertyAnimatorListenerAdapter() {
522                        @Override
523                        public void onAnimationStart(View view) {
524                            if (!mAccessibilityManager.isEnabled()) {
525                                // Animating the children in causes Talkback to think that they're
526                                // not visible when the TYPE_WINDOW_CONTENT_CHANGED event if fired.
527                                // Fixed by skipping the animation when an accessibility manager
528                                // is enabled
529                                mView.animateChildrenIn(
530                                        ANIMATION_DURATION - ANIMATION_FADE_DURATION,
531                                        ANIMATION_FADE_DURATION);
532                            }
533                        }
534
535                        @Override
536                        public void onAnimationEnd(View view) {
537                            onViewShown();
538                        }
539                    }).start();
540        } else {
541            Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
542                    R.anim.design_snackbar_in);
543            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
544            anim.setDuration(ANIMATION_DURATION);
545            anim.setAnimationListener(new Animation.AnimationListener() {
546                @Override
547                public void onAnimationEnd(Animation animation) {
548                    onViewShown();
549                }
550
551                @Override
552                public void onAnimationStart(Animation animation) {}
553
554                @Override
555                public void onAnimationRepeat(Animation animation) {}
556            });
557            mView.startAnimation(anim);
558        }
559    }
560
561    private void animateViewOut(final int event) {
562        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
563            ViewCompat.animate(mView)
564                    .translationY(mView.getHeight())
565                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
566                    .setDuration(ANIMATION_DURATION)
567                    .setListener(new ViewPropertyAnimatorListenerAdapter() {
568                        boolean mEndCalled = false;
569
570                        @Override
571                        public void onAnimationStart(View view) {
572                            if (!mAccessibilityManager.isEnabled()) {
573                                // Animating the children in causes Talkback to think that they're
574                                // not visible when the TYPE_WINDOW_CONTENT_CHANGED event if fired.
575                                // Fixed by skipping the animation when an accessibility manager
576                                // is enabled
577                                mView.animateChildrenOut(0, ANIMATION_FADE_DURATION);
578                            }
579                        }
580
581                        @Override
582                        public void onAnimationEnd(View view) {
583                            onViewHidden(event);
584                        }
585                    }).start();
586        } else {
587            Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_out);
588            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
589            anim.setDuration(ANIMATION_DURATION);
590            anim.setAnimationListener(new Animation.AnimationListener() {
591                @Override
592                public void onAnimationEnd(Animation animation) {
593                    onViewHidden(event);
594                }
595
596                @Override
597                public void onAnimationStart(Animation animation) {}
598
599                @Override
600                public void onAnimationRepeat(Animation animation) {}
601            });
602            mView.startAnimation(anim);
603        }
604    }
605
606    final void hideView(@Callback.DismissEvent final int event) {
607        if (mView.getVisibility() != View.VISIBLE) {
608            onViewHidden(event);
609        } else {
610            animateViewOut(event);
611        }
612    }
613
614    private void onViewShown() {
615        SnackbarManager.getInstance().onShown(mManagerCallback);
616        if (mCallback != null) {
617            mCallback.onShown(this);
618        }
619    }
620
621    private void onViewHidden(int event) {
622        // First tell the SnackbarManager that it has been dismissed
623        SnackbarManager.getInstance().onDismissed(mManagerCallback);
624        // Now call the dismiss listener (if available)
625        if (mCallback != null) {
626            mCallback.onDismissed(this, event);
627        }
628        // Lastly, remove the view from the parent (if attached)
629        final ViewParent parent = mView.getParent();
630        if (parent instanceof ViewGroup) {
631            ((ViewGroup) parent).removeView(mView);
632        }
633    }
634
635    /**
636     * @hide
637     */
638    public static class SnackbarLayout extends LinearLayout {
639        private TextView mMessageView;
640        private Button mActionView;
641
642        private int mMaxWidth;
643        private int mMaxInlineActionWidth;
644
645        interface OnLayoutChangeListener {
646            void onLayoutChange(View view, int left, int top, int right, int bottom);
647        }
648
649        interface OnAttachStateChangeListener {
650            void onViewAttachedToWindow(View v);
651            void onViewDetachedFromWindow(View v);
652        }
653
654        private OnLayoutChangeListener mOnLayoutChangeListener;
655        private OnAttachStateChangeListener mOnAttachStateChangeListener;
656
657        public SnackbarLayout(Context context) {
658            this(context, null);
659        }
660
661        public SnackbarLayout(Context context, AttributeSet attrs) {
662            super(context, attrs);
663            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
664            mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
665            mMaxInlineActionWidth = a.getDimensionPixelSize(
666                    R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
667            if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
668                ViewCompat.setElevation(this, a.getDimensionPixelSize(
669                        R.styleable.SnackbarLayout_elevation, 0));
670            }
671            a.recycle();
672
673            setClickable(true);
674
675            // Now inflate our content. We need to do this manually rather than using an <include>
676            // in the layout since older versions of the Android do not inflate includes with
677            // the correct Context.
678            LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
679
680            ViewCompat.setAccessibilityLiveRegion(this,
681                    ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
682            ViewCompat.setImportantForAccessibility(this,
683                    ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
684        }
685
686        @Override
687        protected void onFinishInflate() {
688            super.onFinishInflate();
689            mMessageView = (TextView) findViewById(R.id.snackbar_text);
690            mActionView = (Button) findViewById(R.id.snackbar_action);
691        }
692
693        TextView getMessageView() {
694            return mMessageView;
695        }
696
697        Button getActionView() {
698            return mActionView;
699        }
700
701        @Override
702        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
703            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
704
705            if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) {
706                widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
707                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
708            }
709
710            final int multiLineVPadding = getResources().getDimensionPixelSize(
711                    R.dimen.design_snackbar_padding_vertical_2lines);
712            final int singleLineVPadding = getResources().getDimensionPixelSize(
713                    R.dimen.design_snackbar_padding_vertical);
714            final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1;
715
716            boolean remeasure = false;
717            if (isMultiLine && mMaxInlineActionWidth > 0
718                    && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) {
719                if (updateViewsWithinLayout(VERTICAL, multiLineVPadding,
720                        multiLineVPadding - singleLineVPadding)) {
721                    remeasure = true;
722                }
723            } else {
724                final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding;
725                if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) {
726                    remeasure = true;
727                }
728            }
729
730            if (remeasure) {
731                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
732            }
733        }
734
735        void animateChildrenIn(int delay, int duration) {
736            ViewCompat.setAlpha(mMessageView, 0f);
737            ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration)
738                    .setStartDelay(delay).start();
739
740            if (mActionView.getVisibility() == VISIBLE) {
741                ViewCompat.setAlpha(mActionView, 0f);
742                ViewCompat.animate(mActionView).alpha(1f).setDuration(duration)
743                        .setStartDelay(delay).start();
744            }
745        }
746
747        void animateChildrenOut(int delay, int duration) {
748            ViewCompat.setAlpha(mMessageView, 1f);
749            ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration)
750                    .setStartDelay(delay).start();
751
752            if (mActionView.getVisibility() == VISIBLE) {
753                ViewCompat.setAlpha(mActionView, 1f);
754                ViewCompat.animate(mActionView).alpha(0f).setDuration(duration)
755                        .setStartDelay(delay).start();
756            }
757        }
758
759        @Override
760        protected void onLayout(boolean changed, int l, int t, int r, int b) {
761            super.onLayout(changed, l, t, r, b);
762            if (changed && mOnLayoutChangeListener != null) {
763                mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
764            }
765        }
766
767        @Override
768        protected void onAttachedToWindow() {
769            super.onAttachedToWindow();
770            if (mOnAttachStateChangeListener != null) {
771                mOnAttachStateChangeListener.onViewAttachedToWindow(this);
772            }
773        }
774
775        @Override
776        protected void onDetachedFromWindow() {
777            super.onDetachedFromWindow();
778            if (mOnAttachStateChangeListener != null) {
779                mOnAttachStateChangeListener.onViewDetachedFromWindow(this);
780            }
781        }
782
783        void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) {
784            mOnLayoutChangeListener = onLayoutChangeListener;
785        }
786
787        void setOnAttachStateChangeListener(OnAttachStateChangeListener listener) {
788            mOnAttachStateChangeListener = listener;
789        }
790
791        private boolean updateViewsWithinLayout(final int orientation,
792                final int messagePadTop, final int messagePadBottom) {
793            boolean changed = false;
794            if (orientation != getOrientation()) {
795                setOrientation(orientation);
796                changed = true;
797            }
798            if (mMessageView.getPaddingTop() != messagePadTop
799                    || mMessageView.getPaddingBottom() != messagePadBottom) {
800                updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom);
801                changed = true;
802            }
803            return changed;
804        }
805
806        private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) {
807            if (ViewCompat.isPaddingRelative(view)) {
808                ViewCompat.setPaddingRelative(view,
809                        ViewCompat.getPaddingStart(view), topPadding,
810                        ViewCompat.getPaddingEnd(view), bottomPadding);
811            } else {
812                view.setPadding(view.getPaddingLeft(), topPadding,
813                        view.getPaddingRight(), bottomPadding);
814            }
815        }
816    }
817
818    final class Behavior extends SwipeDismissBehavior<SnackbarLayout> {
819        @Override
820        public boolean canSwipeDismissView(View child) {
821            return child instanceof SnackbarLayout;
822        }
823
824        @Override
825        public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child,
826                MotionEvent event) {
827            // We want to make sure that we disable any Snackbar timeouts if the user is
828            // currently touching the Snackbar. We restore the timeout when complete
829            if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
830                switch (event.getActionMasked()) {
831                    case MotionEvent.ACTION_DOWN:
832                        SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
833                        break;
834                    case MotionEvent.ACTION_UP:
835                    case MotionEvent.ACTION_CANCEL:
836                        SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
837                        break;
838                }
839            }
840
841            return super.onInterceptTouchEvent(parent, child, event);
842        }
843    }
844}
845