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