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