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