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 static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR;
21
22import android.animation.Animator;
23import android.animation.AnimatorListenerAdapter;
24import android.animation.ValueAnimator;
25import android.content.Context;
26import android.content.res.TypedArray;
27import android.os.Build;
28import android.os.Handler;
29import android.os.Looper;
30import android.os.Message;
31import android.support.annotation.IntDef;
32import android.support.annotation.IntRange;
33import android.support.annotation.NonNull;
34import android.support.annotation.RestrictTo;
35import android.support.design.R;
36import android.support.v4.view.ViewCompat;
37import android.support.v4.view.WindowInsetsCompat;
38import android.util.AttributeSet;
39import android.view.Gravity;
40import android.view.LayoutInflater;
41import android.view.MotionEvent;
42import android.view.View;
43import android.view.ViewGroup;
44import android.view.ViewParent;
45import android.view.accessibility.AccessibilityManager;
46import android.view.animation.Animation;
47import android.view.animation.AnimationUtils;
48import android.widget.FrameLayout;
49
50import java.lang.annotation.Retention;
51import java.lang.annotation.RetentionPolicy;
52import java.util.ArrayList;
53import java.util.List;
54
55/**
56 * Base class for lightweight transient bars that are displayed along the bottom edge of the
57 * application window.
58 *
59 * @param <B> The transient bottom bar subclass.
60 */
61public abstract class BaseTransientBottomBar<B extends BaseTransientBottomBar<B>> {
62    /**
63     * Base class for {@link BaseTransientBottomBar} callbacks.
64     *
65     * @param <B> The transient bottom bar subclass.
66     * @see BaseTransientBottomBar#addCallback(BaseCallback)
67     */
68    public abstract static class BaseCallback<B> {
69        /** Indicates that the Snackbar was dismissed via a swipe.*/
70        public static final int DISMISS_EVENT_SWIPE = 0;
71        /** Indicates that the Snackbar was dismissed via an action click.*/
72        public static final int DISMISS_EVENT_ACTION = 1;
73        /** Indicates that the Snackbar was dismissed via a timeout.*/
74        public static final int DISMISS_EVENT_TIMEOUT = 2;
75        /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/
76        public static final int DISMISS_EVENT_MANUAL = 3;
77        /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/
78        public static final int DISMISS_EVENT_CONSECUTIVE = 4;
79
80        /** @hide */
81        @RestrictTo(LIBRARY_GROUP)
82        @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT,
83                DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE})
84        @Retention(RetentionPolicy.SOURCE)
85        public @interface DismissEvent {}
86
87        /**
88         * Called when the given {@link BaseTransientBottomBar} has been dismissed, either
89         * through a time-out, having been manually dismissed, or an action being clicked.
90         *
91         * @param transientBottomBar The transient bottom bar which has been dismissed.
92         * @param event The event which caused the dismissal. One of either:
93         *              {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION},
94         *              {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or
95         *              {@link #DISMISS_EVENT_CONSECUTIVE}.
96         *
97         * @see BaseTransientBottomBar#dismiss()
98         */
99        public void onDismissed(B transientBottomBar, @DismissEvent int event) {
100            // empty
101        }
102
103        /**
104         * Called when the given {@link BaseTransientBottomBar} is visible.
105         *
106         * @param transientBottomBar The transient bottom bar which is now visible.
107         * @see BaseTransientBottomBar#show()
108         */
109        public void onShown(B transientBottomBar) {
110            // empty
111        }
112    }
113
114    /**
115     * Interface that defines the behavior of the main content of a transient bottom bar.
116     */
117    public interface ContentViewCallback {
118        /**
119         * Animates the content of the transient bottom bar in.
120         *
121         * @param delay Animation delay.
122         * @param duration Animation duration.
123         */
124        void animateContentIn(int delay, int duration);
125
126        /**
127         * Animates the content of the transient bottom bar out.
128         *
129         * @param delay Animation delay.
130         * @param duration Animation duration.
131         */
132        void animateContentOut(int delay, int duration);
133    }
134
135    /**
136     * @hide
137     */
138    @RestrictTo(LIBRARY_GROUP)
139    @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG})
140    @IntRange(from = 1)
141    @Retention(RetentionPolicy.SOURCE)
142    public @interface Duration {}
143
144    /**
145     * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time
146     * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown.
147     *
148     * @see #setDuration
149     */
150    public static final int LENGTH_INDEFINITE = -2;
151
152    /**
153     * Show the Snackbar for a short period of time.
154     *
155     * @see #setDuration
156     */
157    public static final int LENGTH_SHORT = -1;
158
159    /**
160     * Show the Snackbar for a long period of time.
161     *
162     * @see #setDuration
163     */
164    public static final int LENGTH_LONG = 0;
165
166    static final int ANIMATION_DURATION = 250;
167    static final int ANIMATION_FADE_DURATION = 180;
168
169    static final Handler sHandler;
170    static final int MSG_SHOW = 0;
171    static final int MSG_DISMISS = 1;
172
173    // On JB/KK versions of the platform sometimes View.setTranslationY does not
174    // result in layout / draw pass, and CoordinatorLayout relies on a draw pass to
175    // happen to sync vertical positioning of all its child views
176    private static final boolean USE_OFFSET_API = (Build.VERSION.SDK_INT >= 16)
177            && (Build.VERSION.SDK_INT <= 19);
178
179    static {
180        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
181            @Override
182            public boolean handleMessage(Message message) {
183                switch (message.what) {
184                    case MSG_SHOW:
185                        ((BaseTransientBottomBar) message.obj).showView();
186                        return true;
187                    case MSG_DISMISS:
188                        ((BaseTransientBottomBar) message.obj).hideView(message.arg1);
189                        return true;
190                }
191                return false;
192            }
193        });
194    }
195
196    private final ViewGroup mTargetParent;
197    private final Context mContext;
198    final SnackbarBaseLayout mView;
199    private final ContentViewCallback mContentViewCallback;
200    private int mDuration;
201
202    private List<BaseCallback<B>> mCallbacks;
203
204    private final AccessibilityManager mAccessibilityManager;
205
206    /**
207     * @hide
208     */
209    @RestrictTo(LIBRARY_GROUP)
210    interface OnLayoutChangeListener {
211        void onLayoutChange(View view, int left, int top, int right, int bottom);
212    }
213
214    /**
215     * @hide
216     */
217    @RestrictTo(LIBRARY_GROUP)
218    interface OnAttachStateChangeListener {
219        void onViewAttachedToWindow(View v);
220        void onViewDetachedFromWindow(View v);
221    }
222
223    /**
224     * Constructor for the transient bottom bar.
225     *
226     * @param parent The parent for this transient bottom bar.
227     * @param content The content view for this transient bottom bar.
228     * @param contentViewCallback The content view callback for this transient bottom bar.
229     */
230    protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content,
231            @NonNull ContentViewCallback contentViewCallback) {
232        if (parent == null) {
233            throw new IllegalArgumentException("Transient bottom bar must have non-null parent");
234        }
235        if (content == null) {
236            throw new IllegalArgumentException("Transient bottom bar must have non-null content");
237        }
238        if (contentViewCallback == null) {
239            throw new IllegalArgumentException("Transient bottom bar must have non-null callback");
240        }
241
242        mTargetParent = parent;
243        mContentViewCallback = contentViewCallback;
244        mContext = parent.getContext();
245
246        ThemeUtils.checkAppCompatTheme(mContext);
247
248        LayoutInflater inflater = LayoutInflater.from(mContext);
249        // Note that for backwards compatibility reasons we inflate a layout that is defined
250        // in the extending Snackbar class. This is to prevent breakage of apps that have custom
251        // coordinator layout behaviors that depend on that layout.
252        mView = (SnackbarBaseLayout) inflater.inflate(
253                R.layout.design_layout_snackbar, mTargetParent, false);
254        mView.addView(content);
255
256        ViewCompat.setAccessibilityLiveRegion(mView,
257                ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
258        ViewCompat.setImportantForAccessibility(mView,
259                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
260
261        // Make sure that we fit system windows and have a listener to apply any insets
262        ViewCompat.setFitsSystemWindows(mView, true);
263        ViewCompat.setOnApplyWindowInsetsListener(mView,
264                new android.support.v4.view.OnApplyWindowInsetsListener() {
265                    @Override
266                    public WindowInsetsCompat onApplyWindowInsets(View v,
267                            WindowInsetsCompat insets) {
268                        // Copy over the bottom inset as padding so that we're displayed
269                        // above the navigation bar
270                        v.setPadding(v.getPaddingLeft(), v.getPaddingTop(),
271                                v.getPaddingRight(), insets.getSystemWindowInsetBottom());
272                        return insets;
273                    }
274                });
275
276        mAccessibilityManager = (AccessibilityManager)
277                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
278    }
279
280    /**
281     * Set how long to show the view for.
282     *
283     * @param duration either be one of the predefined lengths:
284     *                 {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration
285     *                 in milliseconds.
286     */
287    @NonNull
288    public B setDuration(@Duration int duration) {
289        mDuration = duration;
290        return (B) this;
291    }
292
293    /**
294     * Return the duration.
295     *
296     * @see #setDuration
297     */
298    @Duration
299    public int getDuration() {
300        return mDuration;
301    }
302
303    /**
304     * Returns the {@link BaseTransientBottomBar}'s context.
305     */
306    @NonNull
307    public Context getContext() {
308        return mContext;
309    }
310
311    /**
312     * Returns the {@link BaseTransientBottomBar}'s view.
313     */
314    @NonNull
315    public View getView() {
316        return mView;
317    }
318
319    /**
320     * Show the {@link BaseTransientBottomBar}.
321     */
322    public void show() {
323        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
324    }
325
326    /**
327     * Dismiss the {@link BaseTransientBottomBar}.
328     */
329    public void dismiss() {
330        dispatchDismiss(BaseCallback.DISMISS_EVENT_MANUAL);
331    }
332
333    void dispatchDismiss(@BaseCallback.DismissEvent int event) {
334        SnackbarManager.getInstance().dismiss(mManagerCallback, event);
335    }
336
337    /**
338     * Adds the specified callback to the list of callbacks that will be notified of transient
339     * bottom bar events.
340     *
341     * @param callback Callback to notify when transient bottom bar events occur.
342     * @see #removeCallback(BaseCallback)
343     */
344    @NonNull
345    public B addCallback(@NonNull BaseCallback<B> callback) {
346        if (callback == null) {
347            return (B) this;
348        }
349        if (mCallbacks == null) {
350            mCallbacks = new ArrayList<BaseCallback<B>>();
351        }
352        mCallbacks.add(callback);
353        return (B) this;
354    }
355
356    /**
357     * Removes the specified callback from the list of callbacks that will be notified of transient
358     * bottom bar events.
359     *
360     * @param callback Callback to remove from being notified of transient bottom bar events
361     * @see #addCallback(BaseCallback)
362     */
363    @NonNull
364    public B removeCallback(@NonNull BaseCallback<B> callback) {
365        if (callback == null) {
366            return (B) this;
367        }
368        if (mCallbacks == null) {
369            // This can happen if this method is called before the first call to addCallback
370            return (B) this;
371        }
372        mCallbacks.remove(callback);
373        return (B) this;
374    }
375
376    /**
377     * Return whether this {@link BaseTransientBottomBar} is currently being shown.
378     */
379    public boolean isShown() {
380        return SnackbarManager.getInstance().isCurrent(mManagerCallback);
381    }
382
383    /**
384     * Returns whether this {@link BaseTransientBottomBar} is currently being shown, or is queued
385     * to be shown next.
386     */
387    public boolean isShownOrQueued() {
388        return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);
389    }
390
391    final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
392        @Override
393        public void show() {
394            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
395        }
396
397        @Override
398        public void dismiss(int event) {
399            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
400                    BaseTransientBottomBar.this));
401        }
402    };
403
404    final void showView() {
405        if (mView.getParent() == null) {
406            final ViewGroup.LayoutParams lp = mView.getLayoutParams();
407
408            if (lp instanceof CoordinatorLayout.LayoutParams) {
409                // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
410                final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
411
412                final Behavior behavior = new Behavior();
413                behavior.setStartAlphaSwipeDistance(0.1f);
414                behavior.setEndAlphaSwipeDistance(0.6f);
415                behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
416                behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
417                    @Override
418                    public void onDismiss(View view) {
419                        view.setVisibility(View.GONE);
420                        dispatchDismiss(BaseCallback.DISMISS_EVENT_SWIPE);
421                    }
422
423                    @Override
424                    public void onDragStateChanged(int state) {
425                        switch (state) {
426                            case SwipeDismissBehavior.STATE_DRAGGING:
427                            case SwipeDismissBehavior.STATE_SETTLING:
428                                // If the view is being dragged or settling, pause the timeout
429                                SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
430                                break;
431                            case SwipeDismissBehavior.STATE_IDLE:
432                                // If the view has been released and is idle, restore the timeout
433                                SnackbarManager.getInstance()
434                                        .restoreTimeoutIfPaused(mManagerCallback);
435                                break;
436                        }
437                    }
438                });
439                clp.setBehavior(behavior);
440                // Also set the inset edge so that views can dodge the bar correctly
441                clp.insetEdge = Gravity.BOTTOM;
442            }
443
444            mTargetParent.addView(mView);
445        }
446
447        mView.setOnAttachStateChangeListener(
448                new BaseTransientBottomBar.OnAttachStateChangeListener() {
449                @Override
450                public void onViewAttachedToWindow(View v) {}
451
452                @Override
453                public void onViewDetachedFromWindow(View v) {
454                    if (isShownOrQueued()) {
455                        // If we haven't already been dismissed then this event is coming from a
456                        // non-user initiated action. Hence we need to make sure that we callback
457                        // and keep our state up to date. We need to post the call since
458                        // removeView() will call through to onDetachedFromWindow and thus overflow.
459                        sHandler.post(new Runnable() {
460                            @Override
461                            public void run() {
462                                onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL);
463                            }
464                        });
465                    }
466                }
467            });
468
469        if (ViewCompat.isLaidOut(mView)) {
470            if (shouldAnimate()) {
471                // If animations are enabled, animate it in
472                animateViewIn();
473            } else {
474                // Else if anims are disabled just call back now
475                onViewShown();
476            }
477        } else {
478            // Otherwise, add one of our layout change listeners and show it in when laid out
479            mView.setOnLayoutChangeListener(new BaseTransientBottomBar.OnLayoutChangeListener() {
480                @Override
481                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
482                    mView.setOnLayoutChangeListener(null);
483
484                    if (shouldAnimate()) {
485                        // If animations are enabled, animate it in
486                        animateViewIn();
487                    } else {
488                        // Else if anims are disabled just call back now
489                        onViewShown();
490                    }
491                }
492            });
493        }
494    }
495
496    void animateViewIn() {
497        if (Build.VERSION.SDK_INT >= 12) {
498            final int viewHeight = mView.getHeight();
499            if (USE_OFFSET_API) {
500                ViewCompat.offsetTopAndBottom(mView, viewHeight);
501            } else {
502                mView.setTranslationY(viewHeight);
503            }
504            final ValueAnimator animator = new ValueAnimator();
505            animator.setIntValues(viewHeight, 0);
506            animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
507            animator.setDuration(ANIMATION_DURATION);
508            animator.addListener(new AnimatorListenerAdapter() {
509                @Override
510                public void onAnimationStart(Animator animator) {
511                    mContentViewCallback.animateContentIn(
512                            ANIMATION_DURATION - ANIMATION_FADE_DURATION,
513                            ANIMATION_FADE_DURATION);
514                }
515
516                @Override
517                public void onAnimationEnd(Animator animator) {
518                    onViewShown();
519                }
520            });
521            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
522                private int mPreviousAnimatedIntValue = viewHeight;
523
524                @Override
525                public void onAnimationUpdate(ValueAnimator animator) {
526                    int currentAnimatedIntValue = (int) animator.getAnimatedValue();
527                    if (USE_OFFSET_API) {
528                        ViewCompat.offsetTopAndBottom(mView,
529                                currentAnimatedIntValue - mPreviousAnimatedIntValue);
530                    } else {
531                        mView.setTranslationY(currentAnimatedIntValue);
532                    }
533                    mPreviousAnimatedIntValue = currentAnimatedIntValue;
534                }
535            });
536            animator.start();
537        } else {
538            final Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
539                    R.anim.design_snackbar_in);
540            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
541            anim.setDuration(ANIMATION_DURATION);
542            anim.setAnimationListener(new Animation.AnimationListener() {
543                @Override
544                public void onAnimationEnd(Animation animation) {
545                    onViewShown();
546                }
547
548                @Override
549                public void onAnimationStart(Animation animation) {}
550
551                @Override
552                public void onAnimationRepeat(Animation animation) {}
553            });
554            mView.startAnimation(anim);
555        }
556    }
557
558    private void animateViewOut(final int event) {
559        if (Build.VERSION.SDK_INT >= 12) {
560            final ValueAnimator animator = new ValueAnimator();
561            animator.setIntValues(0, mView.getHeight());
562            animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
563            animator.setDuration(ANIMATION_DURATION);
564            animator.addListener(new AnimatorListenerAdapter() {
565                @Override
566                public void onAnimationStart(Animator animator) {
567                    mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION);
568                }
569
570                @Override
571                public void onAnimationEnd(Animator animator) {
572                    onViewHidden(event);
573                }
574            });
575            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
576                private int mPreviousAnimatedIntValue = 0;
577
578                @Override
579                public void onAnimationUpdate(ValueAnimator animator) {
580                    int currentAnimatedIntValue = (int) animator.getAnimatedValue();
581                    if (USE_OFFSET_API) {
582                        ViewCompat.offsetTopAndBottom(mView,
583                                currentAnimatedIntValue - mPreviousAnimatedIntValue);
584                    } else {
585                        mView.setTranslationY(currentAnimatedIntValue);
586                    }
587                    mPreviousAnimatedIntValue = currentAnimatedIntValue;
588                }
589            });
590            animator.start();
591        } else {
592            final Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
593                    R.anim.design_snackbar_out);
594            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
595            anim.setDuration(ANIMATION_DURATION);
596            anim.setAnimationListener(new Animation.AnimationListener() {
597                @Override
598                public void onAnimationEnd(Animation animation) {
599                    onViewHidden(event);
600                }
601
602                @Override
603                public void onAnimationStart(Animation animation) {}
604
605                @Override
606                public void onAnimationRepeat(Animation animation) {}
607            });
608            mView.startAnimation(anim);
609        }
610    }
611
612    final void hideView(@BaseCallback.DismissEvent final int event) {
613        if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) {
614            animateViewOut(event);
615        } else {
616            // If anims are disabled or the view isn't visible, just call back now
617            onViewHidden(event);
618        }
619    }
620
621    void onViewShown() {
622        SnackbarManager.getInstance().onShown(mManagerCallback);
623        if (mCallbacks != null) {
624            // Notify the callbacks. Do that from the end of the list so that if a callback
625            // removes itself as the result of being called, it won't mess up with our iteration
626            int callbackCount = mCallbacks.size();
627            for (int i = callbackCount - 1; i >= 0; i--) {
628                mCallbacks.get(i).onShown((B) this);
629            }
630        }
631    }
632
633    void onViewHidden(int event) {
634        // First tell the SnackbarManager that it has been dismissed
635        SnackbarManager.getInstance().onDismissed(mManagerCallback);
636        if (mCallbacks != null) {
637            // Notify the callbacks. Do that from the end of the list so that if a callback
638            // removes itself as the result of being called, it won't mess up with our iteration
639            int callbackCount = mCallbacks.size();
640            for (int i = callbackCount - 1; i >= 0; i--) {
641                mCallbacks.get(i).onDismissed((B) this, event);
642            }
643        }
644        if (Build.VERSION.SDK_INT < 11) {
645            // We need to hide the Snackbar on pre-v11 since it uses an old style Animation.
646            // ViewGroup has special handling in removeView() when getAnimation() != null in
647            // that it waits. This then means that the calculated insets are wrong and the
648            // any dodging views do not return. We workaround it by setting the view to gone while
649            // ViewGroup actually gets around to removing it.
650            mView.setVisibility(View.GONE);
651        }
652        // Lastly, hide and remove the view from the parent (if attached)
653        final ViewParent parent = mView.getParent();
654        if (parent instanceof ViewGroup) {
655            ((ViewGroup) parent).removeView(mView);
656        }
657    }
658
659    /**
660     * Returns true if we should animate the Snackbar view in/out.
661     */
662    boolean shouldAnimate() {
663        return !mAccessibilityManager.isEnabled();
664    }
665
666    /**
667     * @hide
668     */
669    @RestrictTo(LIBRARY_GROUP)
670    static class SnackbarBaseLayout extends FrameLayout {
671        private BaseTransientBottomBar.OnLayoutChangeListener mOnLayoutChangeListener;
672        private BaseTransientBottomBar.OnAttachStateChangeListener mOnAttachStateChangeListener;
673
674        SnackbarBaseLayout(Context context) {
675            this(context, null);
676        }
677
678        SnackbarBaseLayout(Context context, AttributeSet attrs) {
679            super(context, attrs);
680            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
681            if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
682                ViewCompat.setElevation(this, a.getDimensionPixelSize(
683                        R.styleable.SnackbarLayout_elevation, 0));
684            }
685            a.recycle();
686
687            setClickable(true);
688        }
689
690        @Override
691        protected void onLayout(boolean changed, int l, int t, int r, int b) {
692            super.onLayout(changed, l, t, r, b);
693            if (mOnLayoutChangeListener != null) {
694                mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
695            }
696        }
697
698        @Override
699        protected void onAttachedToWindow() {
700            super.onAttachedToWindow();
701            if (mOnAttachStateChangeListener != null) {
702                mOnAttachStateChangeListener.onViewAttachedToWindow(this);
703            }
704
705            ViewCompat.requestApplyInsets(this);
706        }
707
708        @Override
709        protected void onDetachedFromWindow() {
710            super.onDetachedFromWindow();
711            if (mOnAttachStateChangeListener != null) {
712                mOnAttachStateChangeListener.onViewDetachedFromWindow(this);
713            }
714        }
715
716        void setOnLayoutChangeListener(
717                BaseTransientBottomBar.OnLayoutChangeListener onLayoutChangeListener) {
718            mOnLayoutChangeListener = onLayoutChangeListener;
719        }
720
721        void setOnAttachStateChangeListener(
722                BaseTransientBottomBar.OnAttachStateChangeListener listener) {
723            mOnAttachStateChangeListener = listener;
724        }
725    }
726
727    final class Behavior extends SwipeDismissBehavior<SnackbarBaseLayout> {
728        @Override
729        public boolean canSwipeDismissView(View child) {
730            return child instanceof SnackbarBaseLayout;
731        }
732
733        @Override
734        public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarBaseLayout child,
735                MotionEvent event) {
736            switch (event.getActionMasked()) {
737                case MotionEvent.ACTION_DOWN:
738                    // We want to make sure that we disable any Snackbar timeouts if the user is
739                    // currently touching the Snackbar. We restore the timeout when complete
740                    if (parent.isPointInChildBounds(child, (int) event.getX(),
741                            (int) event.getY())) {
742                        SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
743                    }
744                    break;
745                case MotionEvent.ACTION_UP:
746                case MotionEvent.ACTION_CANCEL:
747                    SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback);
748                    break;
749            }
750            return super.onInterceptTouchEvent(parent, child, event);
751        }
752    }
753}
754