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