SnackBar 源码bug修复、源码解析

来源:互联网 发布:马赛克拼图软件 编辑:程序博客网 时间:2024/04/30 22:51

前言:为什么写这篇博客?原因是:在使用和封装Snackbar 后测试出来一个bug ,在反复显示多个Snackbar后 再也不显示了。 这就让人很抓狂啊!!!于是通读了Snackbar源码,修正了这个bug。 我使用的是TsnackBar,从github 上down的源码,https://github.com/guoyoujin/MySnackBar;这个项目的作者只对Snackbar添加了顶部及底部显示和三种显示主题,在文末附上我修改的TSnackbar,添加了上滑/下滑隐藏Snackbar,并修复了我所提到的bug。

SnackBar 是 android 中一种 用来做消息提示的控件,它不继承自View, 而是内部 管理 SnackbarLayout 隐藏与显示; SnackbarLayout 是SnackBar的内部类, 继承于LinearLayout;

你可以在 android.support:design 包下找到SnackBar;
使用方式就不多说了,进入正题;

1. 源码bug修复:

show了多个Snackbar后不知道为什么show不出来怎么办?只需要做如下修改:

//Snackbar 内函数:  final void hideView(@Callback.DismissEvent final int event) {  //将原先的(view.getVisibility() == mView.VISIBLE) 改为mView.isShown() 即可;  //getVisibility 返回为 Visible只是说明当前view 的显示状态,isShown 返回true说明这个view和所有的父view 都可见; 在这里只有当都可见咱们才执行animateViewOut(),亲测解决了此问题;        if (shouldAnimate() && mView.isShown()) {            animateViewOut(event);        } else {            // If anims are disabled or the view isn't visible, just call back now            onViewHidden(event);        }    }

2. 源码解析:

有三个核心类:SnackBar 、SnackbarManager、SnackbarRecord ;
那它们是如何工作的呢?

SnackBar 管理SnackbarLayout 隐藏与显示;
SnackbarManager 通知 SnackBar 中SnackbarLayout 隐藏与显示;
SnackbarRecord 中保存了Snackbar 的Callback 和SnackBar的显示时长,SnackbarManager 定义了两个SnackbarRecord :mCurrentSnackbar 和 mNextSnackbar;

1.先从第一次调用 snackbar 的show 说起:

  /**     *SnackBar 中的show     * Show the {@link Snackbar}.     */    public void show() {  //调用 SnackbarManager 中的show        SnackbarManager.getInstance().show(mDuration, mManagerCallback);    }
//SnackbarManager 中的show (请理解注释)  public void show(int duration, Callback callback) {        synchronized (mLock) {            if (isCurrentSnackbarLocked(callback)) {                //①                // Means that the callback is already in the queue. We'll just update the duration(当前的回调早已在队列中。我们只需要更新时长)                mCurrentSnackbar.duration = duration;                // If this is the Snackbar currently being shown, call re-schedule it's                // timeout                mHandler.removeCallbacksAndMessages(mCurrentSnackbar);                scheduleTimeoutLocked(mCurrentSnackbar);                return;            } else if (isNextSnackbarLocked(callback)) {                //②                // We'll just update the duration(同样的,下一个想要显示snackbar的回调早已在队列中。我们只需要更新时长)                mNextSnackbar.duration = duration;            } else {                //③                // Else, we need to create a new record and queue it(代表这是下一个需要show的 snackbar )                mNextSnackbar = new SnackbarRecord(duration, callback);            }            if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,                    Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {                //④                // If we currently have a Snackbar, try and cancel it and wait in line(如果当前有显示的Snackbar,尝试去取消显示)                return;            } else {                //⑤                // Clear out the current snackbar                mCurrentSnackbar = null;                // Otherwise, just show it now(如果当前没有要显示的就显示下一个Snackbar)                showNextSnackbarLocked();            }        }    }

调用snackbar 的show 后,会继续调用 snackbarManager的 show ,然后在 snackbarManager的 show 中按顺序执行③⑤代码块;

⑤中执行的showNextSnackbarLocked函数如下:

// SnackbarManager 中的函数    private void showNextSnackbarLocked() {        if (mNextSnackbar != null) {        //把下一个想要显示的赋值给mCurrentSnackbar ;            mCurrentSnackbar = mNextSnackbar;            mNextSnackbar = null;            final Callback callback = mCurrentSnackbar.callback.get();            if (callback != null) {            //Callback 中有show 和 dismiss                callback.show();//实际调用的是如下的mManagerCallback 的show            } else {                // The callback doesn't exist any more, clear out the Snackbar                mCurrentSnackbar = null;            }        }    }
//showNextSnackbarLocked() 中的变量final Callback callback 是 mManagerCallback,它是定义在snackbar 中的成员变量, 早在 SnackbarManager 中的show (int duration, Callback callback) 传入;   private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {        @Override        public void show() {        //发送一条消息让这个Snackbar 显示            sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));        }        @Override        public void dismiss(int event) {       //发送一条消息让这个Snackbar取消显示            sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));        }    };

调用mManagerCallback 的show后 会发一条消息,然后由主线程中的Looper 循环处理消息:

//Snackbar 中的代码块 static {        sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {            @Override            public boolean handleMessage(Message message) {                switch (message.what) {                    case MSG_SHOW:                        ((Snackbar) message.obj).showView();//至此第一个snackbar 就完全显示出来啦!                        return true;                    case MSG_DISMISS:                        ((Snackbar) message.obj).hideView(message.arg1);                        return true;                }                return false;            }        });    }
// Snackbar 中的函数,最终显示一个Snackbar ,显示有两种方式:1.直接显示,2.通过播放动画显示;具体是通过shouldAnimate()控制的,内部只有一句代码:“mAccessibilityManager.isEnabled()” , AccessibilityManager 是一种系统服务(无障碍服务,http://www.xuebuyuan.com/2061597.html),大家在逢年过节可能用过微信红包助手之类的软件,就是利用这个服务实现的,有兴趣的大家可以阅读这篇文章:http://blog.csdn.net/h183288132/article/details/50973112  final void showView() {      //......具体是如何添加到窗口上的 ,由于篇幅原因我就不叙述了(so easy)       if (ViewCompat.isLaidOut(mView)) {       //如果SnackbarLayout 已经摆放好了执行显示动画;            animateViewIn();        } else {            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {                @Override                public void onLayoutChange(View view, int left, int top, int right, int bottom) {                    animateViewIn();                    mView.setOnLayoutChangeListener(null);                }            });        }        mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() {            @Override            public void onViewAttachedToWindow(View v) {            }            @Override            public void onViewDetachedFromWindow(View v) {                if (isShownOrQueued()) {                    // If we haven't already been dismissed then this event is coming from a                    // non-user initiated action. Hence we need to make sure that we callback                    // and keep our state up to date. We need to post the call since removeView()                    // will call through to onDetachedFromWindow and thus overflow.                    //如果view在规定时间内与window 解绑(这时还在排队中), 就执行这个以保证 更新状态,取消显示此Snackbar;                    sHandler.post(new Runnable() {                        @Override                        public void run() {                            onViewHidden(Callback.DISMISS_EVENT_MANUAL);                        }                    });                }            }        });        if (ViewCompat.isLaidOut(mView)) {            if (shouldAnimate()) {                // If animations are enabled, animate it in                animateViewIn();            } else {                // Else if anims are disabled just call back now                onViewShown();            }        } else {            // Otherwise, add one of our layout change listeners and show it in when laid out            mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {                @Override                public void onLayoutChange(View view, int left, int top, int right, int bottom) {                    mView.setOnLayoutChangeListener(null);                    if (shouldAnimate()) {                        // If animations are enabled, animate it in                        animateViewIn();                    } else {                        // Else if anims are disabled just call back now                        onViewShown();                    }                }            });        }    }

那显示出来之后是如何在 之前规定 的一段时间后取消显示呢? 相信大家都已经猜到了, 哈哈就是如下代码:
在animateViewIn()中设置了动画监听器:

anim.setAnimationListener(new Animation.AnimationListener() {            @Override            public void onAnimationEnd(Animation animation) {                onViewShown();//在显示动画执行完后调用onViewShown;            }            @Override            public void onAnimationStart(Animation animation) {            }            @Override            public void onAnimationRepeat(Animation animation) {            }        });
   //通知SnackbarManager Snackbar 已经显示了。    private void onViewShown() {        SnackbarManager.getInstance().onShown(mManagerCallback);        if (mCallback != null) {            mCallback.onShown(this);        }    }
  // SnackbarManager 中的函数    public void onShown(Callback callback) {        synchronized (mLock) {            if (isCurrentSnackbarLocked(callback)) {            //这里发送消息 在规定显示时长后取消显示Snackbar;                scheduleTimeoutLocked(mCurrentSnackbar);            }        }    }
// SnackbarManager 中的函数 用来在规定显示时长后取消Snackbar    private void scheduleTimeoutLocked(SnackbarRecord r) {        if (r.duration == Snackbar.LENGTH_INDEFINITE) {            // If we're set to indefinite, we don't want to set a timeout            return;        }        int durationMs = LONG_DURATION_MS;        if (r.duration > 0) {            durationMs = r.duration;        } else if (r.duration == Snackbar.LENGTH_SHORT) {            durationMs = SHORT_DURATION_MS;        }        mHandler.removeCallbacksAndMessages(r);        //发送出去的消息被处理后 第一次Snackbar显示与消失就完成啦!        mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);    }

2.如果先后连续显示两个Snackbar(记为A,B)又是如何控制的呢
第一次show 如同以上分析,第二次显示就不同了,第二次会依次调用SnackbarManager 中的show ③④,看到的现象是A在规定时长内取消显示接着显示B;
原因是:

//SnackbarManager的show() 中的④: private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {        Callback callback = record.callback.get();        if (callback != null) {            // Make sure we remove any timeouts for the SnackbarRecord            mHandler.removeCallbacksAndMessages(record);            callback.dismiss(event);//至此A就隐藏了。            return true;        }        return false;    }在调用callback.dismiss(event) 后会调用: public void onDismissed(Callback callback) {        synchronized (mLock) {            if (isCurrentSnackbarLocked(callback)) {                // If the callback is from a Snackbar currently show, remove it and show a new one                mCurrentSnackbar = null;                if (mNextSnackbar != null) {                //显示下一个Snackbar:至此B就显示出来了                    showNextSnackbarLocked();                }            }        }    }

我update 的 Tsnackbar:

/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the “License”);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an “AS IS” BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.ColorInt;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.SwipeDismissBehavior;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
* TSnackbar provides lightweight feedback about an operation. They show a brief message at the
* top of the screen on mobile. TSnackbar appear above all other
* elements on screen and only one can be displayed at a time.
*


* They automatically disappear after a timeout or after user interaction elsewhere on the screen,
* particularly after interactions that summon a new surface or activity. Snackbars can be swiped
* off screen.
*


* Snackbars can contain an action which is set via
* {@link #setAction(CharSequence, View.OnClickListener)}.
*


* To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback}
* via {@link #setCallback(Callback)}.


*/
public final class TSnackbar {
/** * Callback class for {@link TSnackbar} instances. * * @see TSnackbar#setCallback(Callback) */public static abstract class Callback {    /**     * Indicates that the TSnackbar was dismissed via a swipe.     */    public static final int DISMISS_EVENT_SWIPE = 0;    /**     * Indicates that the TSnackbar was dismissed via an action click.     */    public static final int DISMISS_EVENT_ACTION = 1;    /**     * Indicates that the TSnackbar was dismissed via a timeout.     */    public static final int DISMISS_EVENT_TIMEOUT = 2;    /**     * Indicates that the TSnackbar was dismissed via a call to {@link #dismiss()}.     */    public static final int DISMISS_EVENT_MANUAL = 3;    /**     * Indicates that the TSnackbar was dismissed from a new Snackbar being shown.     */    public static final int DISMISS_EVENT_CONSECUTIVE = 4;    @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT,            DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE})    @Retention(RetentionPolicy.SOURCE)    public @interface DismissEvent {    }    /**     * Called when the given {@link TSnackbar} has been dismissed, either through a time-out,     * having been manually dismissed, or an action being clicked.     *     * @param TSnackbar The snackbar which has been dismissed.     * @param event     The event which caused the dismissal. One of either:     *                  {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION},     *                  {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or     *                  {@link #DISMISS_EVENT_CONSECUTIVE}.     * @see TSnackbar#dismiss()     */    public void onDismissed(TSnackbar TSnackbar, @DismissEvent int event) {    }    /**     * Called when the given {@link TSnackbar} is visible.     *     * @param TSnackbar The snackbar which is now visible.     * @see TSnackbar#show()     */    public void onShown(TSnackbar TSnackbar) {    }}@IntDef({APPEAR_TOP, APPEAR_BOTTOM})@Retention(RetentionPolicy.SOURCE)public @interface OverSnackAppearDirection {}/** * Show the TSnackbar from top to down. */public static final int APPEAR_TOP = 0;/** * Show the TSnackbar from top to down. */public static final int APPEAR_BOTTOM = 1;@IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG})@Retention(RetentionPolicy.SOURCE)public @interface Duration {}/** * Show the TSnackbar indefinitely. This means that the TSnackbar will be displayed from the time * that is {@link #show() shown} until either it is dismissed, or another TSnackbar is shown. * * @see #setDuration */public static final int LENGTH_INDEFINITE = -2;/** * Show the TSnackbar for a short period of time. * * @see #setDuration */public static final int LENGTH_SHORT = -1;/** * Show the TSnackbar for a long period of time. * * @see #setDuration */public static final int LENGTH_LONG = 0;private static final int ANIMATION_DURATION = 250;private static final int ANIMATION_FADE_DURATION = 180;private static final Handler sHandler;private static final int MSG_SHOW = 0;private static final int MSG_DISMISS = 1;@OverSnackAppearDirectionprivate int appearDirection = APPEAR_TOP;static {    sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {        @Override        public boolean handleMessage(Message message) {            switch (message.what) {                case MSG_SHOW:                    ((TSnackbar) message.obj).showView();                    return true;                case MSG_DISMISS:                    ((TSnackbar) message.obj).hideView(message.arg1);                    return true;            }            return false;        }    });}private final ViewGroup mParent;private final Context mContext;private final SnackbarLayout mView;private int mDuration;private Callback mCallback;private final AccessibilityManager mAccessibilityManager;private TSnackbar(ViewGroup parent) {    appearDirection = APPEAR_TOP;    mParent = parent;    mContext = parent.getContext();    LayoutInflater inflater = LayoutInflater.from(mContext);    if (appearDirection == APPEAR_BOTTOM) {        mView = (SnackbarLayout) inflater.inflate(R.layout.view_bsnackbar_layout, mParent, false);    } else {        mView = (SnackbarLayout) inflater.inflate(R.layout.view_tsnackbar_layout, mParent, false);    }    mAccessibilityManager = (AccessibilityManager)            mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);    setSwipeListener();}private TSnackbar(ViewGroup parent, final @OverSnackAppearDirection int appearDirection) {    this.appearDirection = appearDirection;    mParent = parent;    mContext = parent.getContext();    LayoutInflater inflater = LayoutInflater.from(mContext);    if (appearDirection == APPEAR_BOTTOM) {        mView = (SnackbarLayout) inflater.inflate(R.layout.view_bsnackbar_layout, mParent, false);    } else {        mView = (SnackbarLayout) inflater.inflate(R.layout.view_tsnackbar_layout, mParent, false);    }    mAccessibilityManager = (AccessibilityManager)            mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);    if (appearDirection == APPEAR_TOP) {        setMinHeight(0, 0);    }    setSwipeListener();}private void setSwipeListener() {    mView.setOnTouchListener(new View.OnTouchListener() {        private float yStart;        private float yEnd;        @Override        public boolean onTouch(View v, MotionEvent event) {            if (event.getAction() == MotionEvent.ACTION_DOWN) {                yStart = event.getRawY();                Logger.d("onTouch:ACTION_DOWN", "yStart:" + yStart);            } else if (event.getAction() == MotionEvent.ACTION_MOVE) {                yEnd = event.getRawY();                Logger.d("onTouch:ACTION_MOVE", "yStart:" + yStart + "," + "yEnd:" + yEnd);                if (appearDirection == APPEAR_TOP) {                    if (yStart - yEnd > mView.getMinimumHeight() / 2) {                        Logger.d("onTouch:DISMISS_EVENT_SWIPE", "yStart:" + yStart + "," + "yEnd:" + yEnd);                        hideView(Callback.DISMISS_EVENT_SWIPE);                        yStart = 0;                        yEnd = 0;                        return true;                    }                } else {                    if (yEnd - yStart > mView.getMinimumHeight() / 2) {                        Logger.d("onTouch:DISMISS_EVENT_SWIPE", "yStart:" + yStart + "," + "yEnd:" + yEnd);                        hideView(Callback.DISMISS_EVENT_SWIPE);                        yStart = 0;                        yEnd = 0;                        return true;                    }                }            } else {                Logger.d("onTouch:ACTION_OTHER", "yStart:" + yStart + "," + "yEnd:" + yEnd);                yStart = 0;                yEnd = 0;            }            return false;        }    });}/** * @param stateBarHeight * @param actionBarHeight * @return */public TSnackbar setMinHeight(int stateBarHeight, int actionBarHeight) {    if (appearDirection == APPEAR_TOP) {        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {            if (stateBarHeight > 0 || actionBarHeight > 0) {                mView.setPadding(0, stateBarHeight, 0, 0);                mView.setMinimumHeight(stateBarHeight + actionBarHeight);            } else {                mView.setPadding(0, ScreenUtil.getStatusHeight(mContext), 0, 0);                mView.setMinimumHeight((int) mContext.getResources().getDimension(R.dimen.toolbar_height) + ScreenUtil.getStatusHeight(mContext));            }        } else {            if (stateBarHeight > 0 || actionBarHeight > 0) {                mView.setMinimumHeight(actionBarHeight);                ScreenUtil.setMargins(mView, 0, stateBarHeight, 0, 0);            } else {                mView.setMinimumHeight((int) mContext.getResources().getDimension(R.dimen.toolbar_height));                ScreenUtil.setMargins(mView, 0, ScreenUtil.getStatusHeight(mContext), 0, 0);            }        }    }    return this;}/** * Make a TSnackbar to display a message * <p/> * <p>TSnackbar will try and find a parent view to hold TSnackbar's view from the value given * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent, * which is defined as a {@link CoordinatorLayout} or the window decor's content view, * whichever comes first. * <p/> * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows TSnackbar to enable * certain features, such as swipe-to-dismiss and automatically moving of widgets like * * @param view     The view to find a parent from. * @param text     The text to show.  Can be formatted text. * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or {@link *                 #LENGTH_LONG} */@NonNullpublic static TSnackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration) {    TSnackbar tSnackbar = new TSnackbar(findSuitableParent(view), APPEAR_TOP);    tSnackbar.setText(text);    tSnackbar.setDuration(duration);    return tSnackbar;}@NonNullpublic static TSnackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration, @OverSnackAppearDirection int appearDirection) {    TSnackbar tSnackbar = new TSnackbar(findSuitableParent(view), appearDirection);    tSnackbar.setText(text);    tSnackbar.setDuration(duration);    return tSnackbar;}/** * Make a TSnackbar to display a message. * <p/> * <p>TSnackbar will try and find a parent view to hold TSnackbar's view from the value given * to {@code view}. TSnackbar will walk up the view tree trying to find a suitable parent, * which is defined as a {@link CoordinatorLayout} or the window decor's content view, * whichever comes first. * <p/> * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows TSnackbar to enable * certain features, such as swipe-to-dismiss and automatically moving of widgets like * * @param view     The view to find a parent from. * @param resId    The resource id of the string resource to use. Can be formatted text. * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or {@link *                 #LENGTH_LONG} */@NonNullpublic static TSnackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {    return make(view, view.getResources().getText(resId), duration);}private static ViewGroup findSuitableParent(View view) {    ViewGroup fallback = null;    do {        if (view instanceof CoordinatorLayout) {            // We've found a CoordinatorLayout, use it            return (ViewGroup) view;        } else if (view instanceof FrameLayout) {            if (view.getId() == android.R.id.content) {                // If we've hit the decor content view, then we didn't find a CoL in the                // hierarchy, so use it.                return (ViewGroup) view;            } else {                // It's not the content view but we'll use it as our fallback                fallback = (ViewGroup) view;            }        }        if (view != null) {            // Else, we will loop and crawl up the view hierarchy and try to find a parent            final ViewParent parent = view.getParent();            view = parent instanceof View ? (View) parent : null;        }    } while (view != null);    // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback    return fallback;    //return (ViewGroup) view;}/** * @param resource_id * @return */public TSnackbar addIcon(int resource_id) {    final TextView tv = mView.getMessageView();    tv.setCompoundDrawablesWithIntrinsicBounds(mContext.getResources().getDrawable(resource_id), null, null, null);    return this;}/** * @param resource_id image id * @param width       image width * @param height      image height * @return */public TSnackbar addIcon(int resource_id, int width, int height) {    final TextView tv = mView.getMessageView();    if (width > 0 || height > 0) {        tv.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(Bitmap.createScaledBitmap(((BitmapDrawable) (mContext.getResources().getDrawable(resource_id))).getBitmap(), width, height, true)), null, null, null);    } else {        addIcon(resource_id);    }    return this;}/** * show loading progressBar * * @param resource_id image id * @param left        show textview left * @param right       show textview right * @return TSnackbar */public TSnackbar addIconProgressLoading(int resource_id, boolean left, boolean right) {    Drawable drawable = mContext.getResources().getDrawable(R.drawable.rotate);    if (resource_id > 0) {        drawable = mContext.getResources().getDrawable(resource_id);    }    addIconProgressLoading(drawable, left, right);    return this;}/** * @param drawable * @param left * @param right * @return */public TSnackbar addIconProgressLoading(Drawable drawable, boolean left, boolean right) {    final ObjectAnimator animator = ObjectAnimator.ofInt(drawable, "level", 0, 10000);    animator.setDuration(1000);    animator.setInterpolator(new LinearInterpolator());    animator.setRepeatCount(ValueAnimator.INFINITE);    animator.setRepeatMode(ValueAnimator.REVERSE);    mView.setBackgroundColor(mContext.getResources().getColor(Prompt.SUCCESS.getBackgroundColor()));    if (left) {        mView.getMessageView().setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);    }    if (right) {        mView.getMessageView().setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null);    }    animator.addListener(new Animator.AnimatorListener() {        @Override        public void onAnimationStart(Animator animation) {        }        @Override        public void onAnimationEnd(Animator animation) {            if (mCallback != null) {                mCallback.onShown(TSnackbar.this);            }            SnackbarManager.getInstance().onShown(mManagerCallback);        }        @Override        public void onAnimationCancel(Animator animation) {        }        @Override        public void onAnimationRepeat(Animator animation) {        }    });    animator.start();    return this;}/** * default style {ERROR , WARNING , SUCCESS} * * @param prompt * @return */public TSnackbar setPromptThemeBackground(Prompt prompt) {    if (prompt == Prompt.SUCCESS) {        setBackgroundColor(mContext.getResources().getColor(Prompt.SUCCESS.getBackgroundColor()));        addIcon(Prompt.SUCCESS.getResIcon(), 0, 0);    } else if (prompt == Prompt.ERROR) {        setBackgroundColor(mContext.getResources().getColor(Prompt.ERROR.getBackgroundColor()));        addIcon(Prompt.ERROR.getResIcon(), 0, 0);    } else if (prompt == Prompt.WARNING) {        setBackgroundColor(mContext.getResources().getColor(Prompt.WARNING.getBackgroundColor()));        addIcon(Prompt.WARNING.getResIcon(), 0, 0);    }    return this;}/** * @param colorId * @return */public TSnackbar setBackgroundColor(int colorId) {    mView.setBackgroundColor(colorId);    return this;}/** * Set the action to be displayed in this {@link TSnackbar}. * * @param resId    String resource to display * @param listener callback to be invoked when the action is clicked */@NonNullpublic TSnackbar setAction(@StringRes int resId, View.OnClickListener listener) {    return setAction(mContext.getText(resId), listener);}/** * Set the action to be displayed in this {@link TSnackbar}. * * @param text     Text to display * @param listener callback to be invoked when the action is clicked */@NonNullpublic TSnackbar setAction(CharSequence text, final View.OnClickListener listener) {    final TextView tv = mView.getActionView();    if (TextUtils.isEmpty(text) || listener == null) {        tv.setVisibility(View.GONE);        tv.setOnClickListener(null);    } else {        tv.setVisibility(View.VISIBLE);        tv.setText(text);        tv.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                listener.onClick(view);                dispatchDismiss(Callback.DISMISS_EVENT_ACTION);            }        });    }    return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setActionTextColor(ColorStateList colors) {    final Button btn = mView.getActionView();    btn.setTextColor(colors);    return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setActionTextSize(int size) {    final Button btn = mView.getActionView();    btn.setTextSize(size);    return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setMessageTextSize(int size) {    final TextView tv = mView.getMessageView();    tv.setTextSize(size);    return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setActionTextColor(@ColorInt int color) {    final Button btn = mView.getActionView();    btn.setTextColor(color);    return this;}/** * Sets the text color of the action specified in */@NonNullpublic TSnackbar setTextColor(@ColorInt int color) {    setActionTextColor(color);    setMessageTextColor(color);    return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setColor(ColorStateList colors) {    setActionTextColor(colors);    setMessageTextColor(colors);    return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setMessageTextColor(@ColorInt int color) {    final TextView tv = mView.getMessageView();    tv.setTextColor(color);    return this;}/** * Sets the text color of the action specified in * {@link #setAction(CharSequence, View.OnClickListener)}. */@NonNullpublic TSnackbar setMessageTextColor(ColorStateList colors) {    final TextView tv = mView.getMessageView();    tv.setTextColor(colors);    return this;}/** * Update the text in this {@link TSnackbar}. * * @param message The new text for the Toast. */@NonNullpublic TSnackbar setText(@NonNull CharSequence message) {    final TextView tv = mView.getMessageView();    tv.setText(message);    return this;}/** * Update the text in this {@link TSnackbar}. * * @param resId The new text for the Toast. */@NonNullpublic TSnackbar setText(@StringRes int resId) {    return setText(mContext.getText(resId));}/** * Set how long to show the view for. * * @param duration either be one of the predefined lengths: *                 {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration *                 in milliseconds. */@NonNullpublic TSnackbar setDuration(@Duration int duration) {    mDuration = duration;    return this;}/** * @param direction {@link #APPEAR_BOTTOM}, {@link #APPEAR_TOP} * @return */@NonNullpublic TSnackbar setAppearDirection(int direction) {    appearDirection = direction;    return this;}/** * Return the duration. * * @see #setDuration */@Durationpublic int getDuration() {    return mDuration;}/** * Returns the {@link TSnackbar}'s view. */@NonNullpublic View getView() {    return mView;}/** * Show the {@link TSnackbar}. */public void show() {    SnackbarManager.getInstance().show(mDuration, mManagerCallback);}/** * Dismiss the {@link TSnackbar}. */public void dismiss() {    dispatchDismiss(Callback.DISMISS_EVENT_MANUAL);}private void dispatchDismiss(@Callback.DismissEvent int event) {    SnackbarManager.getInstance().dismiss(mManagerCallback, event);}/** * Set a callback to be called when this the visibility of this {@link TSnackbar} changes. */@NonNullpublic TSnackbar setCallback(Callback callback) {    mCallback = callback;    return this;}/** * Return whether this {@link TSnackbar} is currently being shown. */public boolean isShown() {    return SnackbarManager.getInstance().isCurrent(mManagerCallback);}/** * Returns whether this {@link TSnackbar} is currently being shown, or is queued to be * shown next. */public boolean isShownOrQueued() {    return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);}private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {    @Override    public void show() {        sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, TSnackbar.this));    }    @Override    public void dismiss(int event) {        sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, TSnackbar.this));    }};final void showView() {    if (mView.getParent() == null) {        final ViewGroup.LayoutParams lp = mView.getLayoutParams();        if (lp instanceof CoordinatorLayout.LayoutParams) {            // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior            final Behavior behavior = new Behavior();            behavior.setStartAlphaSwipeDistance(0.1f);            behavior.setEndAlphaSwipeDistance(0.6f);            behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);            behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {                @Override                public void onDismiss(View view) {                    view.setVisibility(View.GONE);                    dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);                }                @Override                public void onDragStateChanged(int state) {                    switch (state) {                        case SwipeDismissBehavior.STATE_DRAGGING:                        case SwipeDismissBehavior.STATE_SETTLING:                            SnackbarManager.getInstance().pauseTimeout(mManagerCallback);                            break;                        case SwipeDismissBehavior.STATE_IDLE:                            SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback);                            break;                    }                }            });            ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);            ((CoordinatorLayout.LayoutParams) lp).setMargins(0, -30, 0, 0);        }        mParent.addView(mView);    }    if (ViewCompat.isLaidOut(mView)) {        animateViewIn();    } else {        mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {            @Override            public void onLayoutChange(View view, int left, int top, int right, int bottom) {                animateViewIn();                mView.setOnLayoutChangeListener(null);            }        });    }    mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() {        @Override        public void onViewAttachedToWindow(View v) {        }        @Override        public void onViewDetachedFromWindow(View v) {            if (isShownOrQueued()) {                // If we haven't already been dismissed then this event is coming from a                // non-user initiated action. Hence we need to make sure that we callback                // and keep our state up to date. We need to post the call since removeView()                // will call through to onDetachedFromWindow and thus overflow.                sHandler.post(new Runnable() {                    @Override                    public void run() {                        onViewHidden(Callback.DISMISS_EVENT_MANUAL);                    }                });            }        }    });    if (ViewCompat.isLaidOut(mView)) {        if (shouldAnimate()) {            // If animations are enabled, animate it in            animateViewIn();        } else {            // Else if anims are disabled just call back now            onViewShown();        }    } else {        // Otherwise, add one of our layout change listeners and show it in when laid out        mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {            @Override            public void onLayoutChange(View view, int left, int top, int right, int bottom) {                mView.setOnLayoutChangeListener(null);                if (shouldAnimate()) {                    // If animations are enabled, animate it in                    animateViewIn();                } else {                    // Else if anims are disabled just call back now                    onViewShown();                }            }        });    }}private void animateViewIn() {    Animation anim;    if (appearDirection == APPEAR_TOP) {        anim = getAnimationInFromTopToDown();    } else {        anim = getAnimationInFromBottomToTop();    }    anim.setInterpolator(new FastOutSlowInInterpolator());    anim.setDuration(ANIMATION_DURATION);    anim.setAnimationListener(new Animation.AnimationListener() {        @Override        public void onAnimationEnd(Animation animation) {            onViewShown();        }        @Override        public void onAnimationStart(Animation animation) {        }        @Override        public void onAnimationRepeat(Animation animation) {        }    });    mView.startAnimation(anim);}private void animateViewOut(final int event) {    Animation anim;    if (appearDirection == APPEAR_TOP) {        anim = getAnimationOutFromTopToDown();    } else {        anim = getAnimationOutFromBottomToTop();    }    anim.setInterpolator(new FastOutSlowInInterpolator());    anim.setDuration(ANIMATION_DURATION);    anim.setAnimationListener(new Animation.AnimationListener() {        @Override        public void onAnimationEnd(Animation animation) {            onViewHidden(event);        }        @Override        public void onAnimationStart(Animation animation) {        }        @Override        public void onAnimationRepeat(Animation animation) {        }    });    mView.startAnimation(anim);}private Animation getAnimationInFromTopToDown() {    return AnimationUtils.loadAnimation(mView.getContext(), R.anim.top_in);}private Animation getAnimationInFromBottomToTop() {    return AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_in);}private Animation getAnimationOutFromTopToDown() {    return AnimationUtils.loadAnimation(mView.getContext(), R.anim.top_out);}private Animation getAnimationOutFromBottomToTop() {    return AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_out);}final void hideView(@Callback.DismissEvent final int event) {    if (shouldAnimate() && mView.isShown()) {        animateViewOut(event);    } else {        // If anims are disabled or the view isn't visible, just call back now        onViewHidden(event);    }}private void onViewShown() {    SnackbarManager.getInstance().onShown(mManagerCallback);    if (mCallback != null) {        mCallback.onShown(this);    }}private void onViewHidden(int event) {    // First tell the SnackbarManager that it has been dismissed    SnackbarManager.getInstance().onDismissed(mManagerCallback);    // Now call the dismiss listener (if available)    if (mCallback != null) {        mCallback.onDismissed(this, event);    }    // Lastly, remove the view from the parent (if attached)    final ViewParent parent = mView.getParent();    if (parent instanceof ViewGroup) {        ((ViewGroup) parent).removeView(mView);    }}/** * Returns true if we should animate the Snackbar view in/out. */private boolean shouldAnimate() {    return !mAccessibilityManager.isEnabled();}/** * @return if the view is being being dragged or settled by {@link SwipeDismissBehavior}. */private boolean isBeingDragged() {    final ViewGroup.LayoutParams lp = mView.getLayoutParams();    if (lp instanceof CoordinatorLayout.LayoutParams) {        final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) lp;        final CoordinatorLayout.Behavior behavior = layoutParams.getBehavior();        if (behavior instanceof SwipeDismissBehavior) {            return ((SwipeDismissBehavior) behavior).getDragState() != SwipeDismissBehavior.STATE_IDLE;        }    }    return false;}public static class SnackbarLayout extends LinearLayout {    private TextView mMessageView;    private Button mActionView;    private int mMaxWidth;    private int mMaxInlineActionWidth;    interface OnLayoutChangeListener {        void onLayoutChange(View view, int left, int top, int right, int bottom);    }    interface OnAttachStateChangeListener {        void onViewAttachedToWindow(View v);        void onViewDetachedFromWindow(View v);    }    private OnLayoutChangeListener mOnLayoutChangeListener;    private OnAttachStateChangeListener mOnAttachStateChangeListener;    public SnackbarLayout(Context context) {        this(context, null);    }    public SnackbarLayout(Context context, AttributeSet attrs) {        super(context, attrs);        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);        mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);        mMaxInlineActionWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_maxActionInlineWidth, -1);        if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {            ViewCompat.setElevation(this, a.getDimensionPixelSize(                    R.styleable.SnackbarLayout_elevation, 0));        }        a.recycle();        setClickable(true);        // Now inflate our content. We need to do this manually rather than using an <include>        // in the layout since older versions of the Android do not inflate includes with        // the correct Context.        LayoutInflater.from(context).inflate(R.layout.view_tsnackbar_layout_include, this);        ViewCompat.setAccessibilityLiveRegion(this,                ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);        ViewCompat.setImportantForAccessibility(this,                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);    }    @Override    protected void onFinishInflate() {        super.onFinishInflate();        mMessageView = (TextView) findViewById(R.id.snackbar_text);        mActionView = (Button) findViewById(R.id.snackbar_action);    }    TextView getMessageView() {        return mMessageView;    }    Button getActionView() {        return mActionView;    }    @Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) {            widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);            super.onMeasure(widthMeasureSpec, heightMeasureSpec);        }        final int multiLineVPadding = getResources().getDimensionPixelSize(                R.dimen.design_snackbar_padding_vertical_2lines);        final int singleLineVPadding = getResources().getDimensionPixelSize(                R.dimen.design_snackbar_padding_vertical);        final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1;        boolean remeasure = false;        if (isMultiLine && mMaxInlineActionWidth > 0                && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) {            if (updateViewsWithinLayout(VERTICAL, multiLineVPadding,                    multiLineVPadding - singleLineVPadding)) {                remeasure = true;            }        } else {            final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding;            if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) {                remeasure = true;            }        }        if (remeasure) {            super.onMeasure(widthMeasureSpec, heightMeasureSpec);        }    }    void animateChildrenIn(int delay, int duration) {        ViewCompat.setAlpha(mMessageView, 0f);        ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration)                .setStartDelay(delay).start();        if (mActionView.getVisibility() == VISIBLE) {            ViewCompat.setAlpha(mActionView, 0f);            ViewCompat.animate(mActionView).alpha(1f).setDuration(duration)                    .setStartDelay(delay).start();        }    }    void animateChildrenOut(int delay, int duration) {        ViewCompat.setAlpha(mMessageView, 1f);        ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration)                .setStartDelay(delay).start();        if (mActionView.getVisibility() == VISIBLE) {            ViewCompat.setAlpha(mActionView, 1f);            ViewCompat.animate(mActionView).alpha(0f).setDuration(duration)                    .setStartDelay(delay).start();        }    }    @Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        super.onLayout(changed, l, t, r, b);        if (changed && mOnLayoutChangeListener != null) {            mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);        }    }    @Override    protected void onAttachedToWindow() {        super.onAttachedToWindow();        if (mOnAttachStateChangeListener != null) {            mOnAttachStateChangeListener.onViewAttachedToWindow(this);        }    }    @Override    protected void onDetachedFromWindow() {        super.onDetachedFromWindow();        if (mOnAttachStateChangeListener != null) {            mOnAttachStateChangeListener.onViewDetachedFromWindow(this);        }    }    void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) {        mOnLayoutChangeListener = onLayoutChangeListener;    }    void setOnAttachStateChangeListener(OnAttachStateChangeListener listener) {        mOnAttachStateChangeListener = listener;    }    private boolean updateViewsWithinLayout(final int orientation,                                            final int messagePadTop, final int messagePadBottom) {        boolean changed = false;        if (orientation != getOrientation()) {            setOrientation(orientation);            changed = true;        }        if (mMessageView.getPaddingTop() != messagePadTop                || mMessageView.getPaddingBottom() != messagePadBottom) {            updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom);            changed = true;        }        return changed;    }    private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) {        if (ViewCompat.isPaddingRelative(view)) {            ViewCompat.setPaddingRelative(view,                    ViewCompat.getPaddingStart(view), topPadding,                    ViewCompat.getPaddingEnd(view), bottomPadding);        } else {            view.setPadding(view.getPaddingLeft(), topPadding,                    view.getPaddingRight(), bottomPadding);        }    }}final class Behavior extends SwipeDismissBehavior<SnackbarLayout> {    @Override    public boolean canSwipeDismissView(View child) {        return child instanceof SnackbarLayout;    }    @Override    public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child,                                         MotionEvent event) {        // We want to make sure that we disable any Snackbar timeouts if the user is        // currently touching the Snackbar. We restore the timeout when complete        if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {            switch (event.getActionMasked()) {                case MotionEvent.ACTION_DOWN:                    SnackbarManager.getInstance().pauseTimeout(mManagerCallback);                    break;                case MotionEvent.ACTION_UP:                case MotionEvent.ACTION_CANCEL:                    SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback);                    break;            }        }        return super.onInterceptTouchEvent(parent, child, event);    }}

}

1 0