Snackbar.java revision b7f9224b1495db47eb8fd813b5912250e900770a
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.TypedArray;
21import android.os.Build;
22import android.os.Handler;
23import android.os.Looper;
24import android.os.Message;
25import android.support.annotation.IntDef;
26import android.support.annotation.StringRes;
27import android.support.design.R;
28import android.support.v4.view.ViewCompat;
29import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
30import android.text.TextUtils;
31import android.util.AttributeSet;
32import android.view.LayoutInflater;
33import android.view.MotionEvent;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.animation.Animation;
37import android.view.animation.AnimationUtils;
38import android.widget.LinearLayout;
39import android.widget.TextView;
40
41import java.lang.annotation.Retention;
42import java.lang.annotation.RetentionPolicy;
43
44import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR;
45
46/**
47 * Snackbars provide lightweight feedback about an operation. They show a brief message at the
48 * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other
49 * elements on screen and only one can be displayed at a time.
50 * <p>
51 * They automatically disappear after a timeout or after user interaction elsewhere on the screen,
52 * particularly after interactions that summon a new surface or activity. Snackbars can be swiped
53 * off screen.
54 * <p>
55 * Snackbars can contain an action which is set via
56 * {@link #setAction(CharSequence, android.view.View.OnClickListener)}.
57 */
58public class Snackbar {
59
60    /**
61     * @hide
62     */
63    @IntDef({LENGTH_SHORT, LENGTH_LONG})
64    @Retention(RetentionPolicy.SOURCE)
65    public @interface Duration {}
66
67    /**
68     * Show the Snackbar for a short period of time.
69     *
70     * @see #setDuration
71     */
72    public static final int LENGTH_SHORT = -1;
73
74    /**
75     * Show the Snackbar for a long period of time.
76     *
77     * @see #setDuration
78     */
79    public static final int LENGTH_LONG = 0;
80
81    private static final int ANIMATION_DURATION = 250;
82    private static final int ANIMATION_FADE_DURATION = 180;
83
84    private static final Handler sHandler;
85    private static final int MSG_SHOW = 0;
86    private static final int MSG_DISMISS = 1;
87
88    static {
89        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
90            @Override
91            public boolean handleMessage(Message message) {
92                switch (message.what) {
93                    case MSG_SHOW:
94                        ((Snackbar) message.obj).showView();
95                        return true;
96                    case MSG_DISMISS:
97                        ((Snackbar) message.obj).hideView();
98                        return true;
99                }
100                return false;
101            }
102        });
103    }
104
105    private final ViewGroup mParent;
106    private final Context mContext;
107    private final SnackbarLayout mView;
108    private int mDuration;
109
110    Snackbar(ViewGroup parent) {
111        mParent = parent;
112        mContext = parent.getContext();
113
114        LayoutInflater inflater = LayoutInflater.from(mContext);
115        mView = (SnackbarLayout) inflater.inflate(R.layout.layout_snackbar, mParent, false);
116    }
117
118    /**
119     * Make a Snackbar to display a message.
120     *
121     * @param parent   The parent to add Snackbars to.
122     * @param text     The text to show.  Can be formatted text.
123     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or {@link
124     *                 #LENGTH_LONG}
125     */
126    public static Snackbar make(ViewGroup parent, CharSequence text, @Duration int duration) {
127        Snackbar snackbar = new Snackbar(parent);
128        snackbar.setText(text);
129        snackbar.setDuration(duration);
130        return snackbar;
131    }
132
133    /**
134     * Make a Snackbar to display a message.
135     *
136     * @param parent   The parent to add Snackbars to.
137     * @param resId    The resource id of the string resource to use.  Can be formatted text.
138     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or {@link
139     *                 #LENGTH_LONG}
140     */
141    public static Snackbar make(ViewGroup parent, int resId, @Duration int duration) {
142        return make(parent, parent.getResources().getText(resId), duration);
143    }
144
145    /**
146     * Set the action to be displayed in this {@link Snackbar}.
147     *
148     * @param resId    String resource to display
149     * @param listener callback to be invoked when the action is clicked
150     */
151    public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) {
152        return setAction(mContext.getText(resId), listener);
153    }
154
155    /**
156     * Set the action to be displayed in this {@link Snackbar}.
157     *
158     * @param text     Text to display
159     * @param listener callback to be invoked when the action is clicked
160     */
161    public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
162        final TextView tv = mView.getActionView();
163
164        if (TextUtils.isEmpty(text) || listener == null) {
165            tv.setVisibility(View.GONE);
166            tv.setOnClickListener(null);
167        } else {
168            tv.setVisibility(View.VISIBLE);
169            tv.setText(text);
170            tv.setOnClickListener(new View.OnClickListener() {
171                @Override
172                public void onClick(View view) {
173                    listener.onClick(view);
174
175                    // Now dismiss the Snackbar
176                    dismiss();
177                }
178            });
179        }
180        return this;
181    }
182
183    /**
184     * Update the text in this {@link Snackbar}.
185     *
186     * @param message The new text for the Toast.
187     */
188    public Snackbar setText(CharSequence message) {
189        final TextView tv = mView.getMessageView();
190        tv.setText(message);
191        return this;
192    }
193
194    /**
195     * Update the text in this {@link Snackbar}.
196     *
197     * @param resId The new text for the Toast.
198     */
199    public Snackbar setText(@StringRes int resId) {
200        return setText(mContext.getText(resId));
201    }
202
203    /**
204     * Set how long to show the view for.
205     *
206     * @param duration either be one of the predefined lengths:
207     *                 {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration
208     *                 in milliseconds.
209     */
210    public Snackbar setDuration(@Duration int duration) {
211        mDuration = duration;
212        return this;
213    }
214
215    /**
216     * Return the duration.
217     *
218     * @see #setDuration
219     */
220    @Duration
221    public int getDuration() {
222        return mDuration;
223    }
224
225    /**
226     * Returns the {@link Snackbar}'s view.
227     */
228    public View getView() {
229        return mView;
230    }
231
232    /**
233     * Show the {@link Snackbar}.
234     */
235    public void show() {
236        SnackbarManager.getInstance().show(mDuration, mManagerCallback);
237    }
238
239    /**
240     * Dismiss the {@link Snackbar}.
241     */
242    public void dismiss() {
243        SnackbarManager.getInstance().dismiss(mManagerCallback);
244    }
245
246    private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
247        @Override
248        public void show() {
249            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
250        }
251
252        @Override
253        public void dismiss() {
254            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, Snackbar.this));
255        }
256    };
257
258    final void showView() {
259        if (mView.getParent() == null) {
260            final ViewGroup.LayoutParams lp = mView.getLayoutParams();
261
262            if (lp instanceof CoordinatorLayout.LayoutParams) {
263                // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
264
265                final Behavior behavior = new Behavior();
266                behavior.setStartAlphaSwipeDistance(0.1f);
267                behavior.setEndAlphaSwipeDistance(0.6f);
268                behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
269                behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
270                    @Override
271                    public void onDismiss(View view) {
272                        dismiss();
273                    }
274
275                    @Override
276                    public void onDragStateChanged(int state) {
277                        switch (state) {
278                            case SwipeDismissBehavior.STATE_DRAGGING:
279                            case SwipeDismissBehavior.STATE_SETTLING:
280                                // If the view is being dragged or settling, cancel the timeout
281                                SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
282                                break;
283                            case SwipeDismissBehavior.STATE_IDLE:
284                                // If the view has been released and is idle, restore the timeout
285                                SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
286                                break;
287                        }
288                    }
289                });
290                ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
291            }
292
293            mParent.addView(mView);
294        }
295
296        if (ViewCompat.isLaidOut(mView)) {
297            // If the view is already laid out, animate it now
298            animateViewIn();
299        } else {
300            // Otherwise, add one of our layout change listeners and animate it in when laid out
301            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
302                @Override
303                public void onLayoutChange(View view, int left, int top, int right, int bottom) {
304                    animateViewIn();
305                    mView.setOnLayoutChangeListener(null);
306                }
307            });
308        }
309    }
310
311    private void animateViewIn() {
312        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
313            ViewCompat.setTranslationY(mView, mView.getHeight());
314            ViewCompat.animate(mView).translationY(0f)
315                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
316                    .setDuration(ANIMATION_DURATION)
317                    .setListener(new ViewPropertyAnimatorListenerAdapter() {
318                        @Override
319                        public void onAnimationStart(View view) {
320                            mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,
321                                    ANIMATION_FADE_DURATION);
322                        }
323
324                        @Override
325                        public void onAnimationEnd(View view) {
326                            SnackbarManager.getInstance().onShown(mManagerCallback);
327                        }
328                    }).start();
329        } else {
330            Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.snackbar_in);
331            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
332            anim.setDuration(ANIMATION_DURATION);
333            anim.setAnimationListener(new Animation.AnimationListener() {
334                @Override
335                public void onAnimationEnd(Animation animation) {
336                    SnackbarManager.getInstance().onShown(mManagerCallback);
337                }
338
339                @Override
340                public void onAnimationStart(Animation animation) {}
341
342                @Override
343                public void onAnimationRepeat(Animation animation) {}
344            });
345            mView.startAnimation(anim);
346        }
347    }
348
349    private void animateViewOut() {
350        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
351            ViewCompat.animate(mView).translationY(mView.getHeight())
352                    .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
353                    .setDuration(ANIMATION_DURATION)
354                    .setListener(new ViewPropertyAnimatorListenerAdapter() {
355                        @Override
356                        public void onAnimationStart(View view) {
357                            mView.animateChildrenOut(0, ANIMATION_FADE_DURATION);
358                        }
359
360                        @Override
361                        public void onAnimationEnd(View view) {
362                            onViewHidden();
363                        }
364                    }).start();
365        } else {
366            Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.snackbar_out);
367            anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
368            anim.setDuration(ANIMATION_DURATION);
369            anim.setAnimationListener(new Animation.AnimationListener() {
370                @Override
371                public void onAnimationEnd(Animation animation) {
372                    onViewHidden();
373                }
374
375                @Override
376                public void onAnimationStart(Animation animation) {}
377
378                @Override
379                public void onAnimationRepeat(Animation animation) {}
380            });
381            mView.startAnimation(anim);
382        }
383    }
384
385    final void hideView() {
386        if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) {
387            onViewHidden();
388        } else {
389            animateViewOut();
390        }
391    }
392
393    private void onViewHidden() {
394        // First remove the view from the parent
395        mParent.removeView(mView);
396        // Now, tell the SnackbarManager that it has been dismissed
397        SnackbarManager.getInstance().onDismissed(mManagerCallback);
398    }
399
400    /**
401     * @return if the view is being being dragged or settled by {@link SwipeDismissBehavior}.
402     */
403    private boolean isBeingDragged() {
404        final ViewGroup.LayoutParams lp = mView.getLayoutParams();
405
406        if (lp instanceof CoordinatorLayout.LayoutParams) {
407            final CoordinatorLayout.LayoutParams cllp = (CoordinatorLayout.LayoutParams) lp;
408            final CoordinatorLayout.Behavior behavior = cllp.getBehavior();
409
410            if (behavior instanceof SwipeDismissBehavior) {
411                return ((SwipeDismissBehavior) behavior).getDragState()
412                        != SwipeDismissBehavior.STATE_IDLE;
413            }
414        }
415        return false;
416    }
417
418    /**
419     * @hide
420     */
421    public static class SnackbarLayout extends LinearLayout {
422        private TextView mMessageView;
423        private TextView mActionView;
424
425        private int mMaxWidth;
426        private int mMaxInlineActionWidth;
427
428        interface OnLayoutChangeListener {
429            public void onLayoutChange(View view, int left, int top, int right, int bottom);
430        }
431
432        private OnLayoutChangeListener mOnLayoutChangeListener;
433
434        public SnackbarLayout(Context context) {
435            this(context, null);
436        }
437
438        public SnackbarLayout(Context context, AttributeSet attrs) {
439            super(context, attrs);
440            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
441            mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
442            mMaxInlineActionWidth = a.getDimensionPixelSize(
443                    R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
444            a.recycle();
445
446            setClickable(true);
447
448            // Now inflate our content. We need to do this manually rather than using an <include>
449            // in the layout since older versions of the Android do not inflate includes with
450            // the correct Context.
451            LayoutInflater.from(context).inflate(R.layout.layout_snackbar_include, this);
452        }
453
454        @Override
455        protected void onFinishInflate() {
456            super.onFinishInflate();
457            mMessageView = (TextView) findViewById(R.id.snackbar_text);
458            mActionView = (TextView) findViewById(R.id.snackbar_action);
459        }
460
461        TextView getMessageView() {
462            return mMessageView;
463        }
464
465        TextView getActionView() {
466            return mActionView;
467        }
468
469        @Override
470        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
471            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
472
473            if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) {
474                widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
475                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
476            }
477
478            final int multiLineVPadding = getResources().getDimensionPixelSize(
479                    R.dimen.snackbar_padding_vertical_2lines);
480            final int singleLineVPadding = getResources().getDimensionPixelSize(
481                    R.dimen.snackbar_padding_vertical);
482            final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1;
483
484            boolean remeasure = false;
485            if (isMultiLine && mMaxInlineActionWidth > 0
486                    && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) {
487                if (updateViewsWithinLayout(VERTICAL, multiLineVPadding,
488                        multiLineVPadding - singleLineVPadding)) {
489                    remeasure = true;
490                }
491            } else {
492                final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding;
493                if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) {
494                    remeasure = true;
495                }
496            }
497
498            if (remeasure) {
499                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
500            }
501        }
502
503        void animateChildrenIn(int delay, int duration) {
504            ViewCompat.setAlpha(mMessageView, 0f);
505            ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration)
506                    .setStartDelay(delay).start();
507
508            if (mActionView.getVisibility() == VISIBLE) {
509                ViewCompat.setAlpha(mActionView, 0f);
510                ViewCompat.animate(mActionView).alpha(1f).setDuration(duration)
511                        .setStartDelay(delay).start();
512            }
513        }
514
515        void animateChildrenOut(int delay, int duration) {
516            ViewCompat.setAlpha(mMessageView, 1f);
517            ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration)
518                    .setStartDelay(delay).start();
519
520            if (mActionView.getVisibility() == VISIBLE) {
521                ViewCompat.setAlpha(mActionView, 1f);
522                ViewCompat.animate(mActionView).alpha(0f).setDuration(duration)
523                        .setStartDelay(delay).start();
524            }
525        }
526
527        @Override
528        protected void onLayout(boolean changed, int l, int t, int r, int b) {
529            super.onLayout(changed, l, t, r, b);
530            if (changed && mOnLayoutChangeListener != null) {
531                mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
532            }
533        }
534
535        void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) {
536            mOnLayoutChangeListener = onLayoutChangeListener;
537        }
538
539        private boolean updateViewsWithinLayout(final int orientation,
540                final int messagePadTop, final int messagePadBottom) {
541            boolean changed = false;
542            if (orientation != getOrientation()) {
543                setOrientation(orientation);
544                changed = true;
545            }
546            if (mMessageView.getPaddingTop() != messagePadTop
547                    || mMessageView.getPaddingBottom() != messagePadBottom) {
548                updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom);
549                changed = true;
550            }
551            return changed;
552        }
553
554        private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) {
555            if (ViewCompat.isPaddingRelative(view)) {
556                ViewCompat.setPaddingRelative(view,
557                        ViewCompat.getPaddingStart(view), topPadding,
558                        ViewCompat.getPaddingEnd(view), bottomPadding);
559            } else {
560                view.setPadding(view.getPaddingLeft(), topPadding,
561                        view.getPaddingRight(), bottomPadding);
562            }
563        }
564    }
565
566    final class Behavior extends SwipeDismissBehavior<SnackbarLayout> {
567        @Override
568        public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child,
569                MotionEvent event) {
570            // We want to make sure that we disable any Snackbar timeouts if the user is
571            // currently touching the Snackbar. We restore the timeout when complete
572            if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
573                switch (event.getActionMasked()) {
574                    case MotionEvent.ACTION_DOWN:
575                        SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
576                        break;
577                    case MotionEvent.ACTION_UP:
578                    case MotionEvent.ACTION_CANCEL:
579                        SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
580                        break;
581                }
582            }
583
584            return super.onInterceptTouchEvent(parent, child, event);
585        }
586    }
587}
588