#Snackbar源码分析
##相关类
Snackbar, Snackbar.Callback, SnackbarManager, SnackbarManager.Callback
##常用API
Snackbar.make(view, text, duration)
setAction(text, listener)
setCallback(callback)
show()
dismiss()
##作用
Snackbar
是android support包中,用来在屏幕底部弹出消息的控件,和Toast
类似,用法也比较接近。
##用法
一般采用如下的代码,当然下面是一个完整的例子,包括了添加点击事件,以及显示
Snackbar snackbar = Snackbar.
make(mRootView, getString(R.string.show_message),Snackbar.LENGTH_INDEFINITE)
.setAction(getString(R.string.open), mSnackBarClickListener);
snackbar.show();
##原理分析 本篇主要是想要对源码进行分析,用法之类的可以参考其他相关的博客,或者到github上找相关的代码
准备从两个方法讲起,一块是show()
, 一个是dismiss()
###show()
在我们调用snackbar.show()
的时候,我们能够马上看到屏幕底部显示出了一个黑色的弹出框。但是这一过程是如何完成的,不看源码还是不太清楚的
想要把snackbar
显示到屏幕上,需要调用show()
,那么show
方法究竟做了什么工作呢。
翻开源码我们看到,show
方法内部只有一行代码
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
一开始我还在想,就这一行代码能够把snackbar
这个控件显示到屏幕上,真让人惊讶,同时也让人一脸懵逼,完全不知这是个什么情况,只好跟代码,到SnackbarManager
里面,看看SnackbarManager.show(mDuration, mManagerCallback)
方法是干嘛的。对于SnackbarManager.getInstance()
先看做单例吧,目前只分析关键代码。
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
mNextSnackbar.duration = duration;
} else {
// Else, we need to create a new record and queue it
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
return;
} else {
// Clear out the current snackbar
mCurrentSnackbar = null;
// Otherwise, just show it now
showNextSnackbarLocked();
}
}
}
在SnackbarManager.show()
方法内部,通过加锁机制保证同一时间只显示一个snackbar
,假设当前是第一次显示snackbar
,那么初始状态的mCurrentSnackbar
,mNextSnackbar
均为null
,代码第一次会执行showNextSnackbarLocked()
方法。
private void showNextSnackbarLocked() {
if (mNextSnackbar != null) {
...
final Callback callback = mCurrentSnackbar.callback.get();
if (callback != null) {
callback.show();
}
...
}
}
在showNextSnackbarLocked()
方法中,是会执行callback.show()
的,这个callback
是来自于mCurrentSnackbar
的属性callback
,我们知道mCurrentSnackbar
是SnackbarRecord
类型的对象.只需要找找代码中什么时候SnackbarRecord
被实例化了。搜索代码发现是在我们的show
方法中,也就是这一行代码mNextSnackbar = new SnackbarRecord(duration, callback);
mCurrentSnackbar
的callback
就是这个时候传递进去的,而这个callback
是调用SnackbarManager.show(duration, callback)
方法的时候传递进去的参数,而在最开始的调用过程中,在Snackbar.show()
方法内部,调用了SnackbarManager.show(duration, callback)
,同时也是这个时候传递的参数mManagerCallback
,这个mManagerCallback
是Snackbar
里面的一个成员变量,一开始就初始化了。
回到showNextSnackbarLocked()
方法中,我们调用的callback.show()
,其实就是mManagerCallback.show()
.具体执行的就是下面的
private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
@Override
public void show() {
sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
}
@Override
public void dismiss(int event) {
sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, Snackbar.this));
}
};
sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, event, 0, Snackbar.this));
这段代码就是做的要显示的操作了。绕了一大圈又回到了这个地方,原来还是通过hanlder
发消息,切换到主线程刷新页面啊,本质的东西还是没有变。
sHandler
是通过静态代码块初始化的,传递的也是主线程的looper
。这样就肯定会切换到主线程刷新页面的。
接着看看handler
发消息后怎么走的,这个时候消息进入队列中等待调度。主线程调度到这个MSG_SHOW
的消息时,会执行((Snackbar) message.obj).showView();
message.obj
就是当前的Snackbar
对象,调用的也是当前的showView
方法,历经千辛万苦终于到了目的地了,太不容易了。
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
...
}
mParent.addView(mView);
}
if (ViewCompat.isLaidOut(mView)) {
// If the view is already laid out, animate it now
animateViewIn();
} else {
// Otherwise, add one of our layout change listeners and animate it in when laid out
mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
animateViewIn();
mView.setOnLayoutChangeListener(null);
}
});
}
}
showView()代码还是挺多的,显示的逻辑执行的还是animateViewIn()
方法,里面是通过view
动画或者是属性动画来实现动画效果。
这个方法还是需要详细分析的,通过debug发现mView
需要设置Behavior
,这样才能在CoordinatorLayout
控件下产生效果,比如说snackbar
跟FloatingActionButton
的联动效果。 默认情况下,ViewCompat.isLaidOut(mView)
返回的结果是false
,所以还是mView
自己设置的listener
来监听布局的变化,来执行animateViewIn()
animateViewIn()
这个方法会判断当前的sdk
版本,大于等于14
,直接采用属性动画实现效果,小于14
,采用的就是view
动画。当动画结束的时候,会执行SnackbarManager.getInstance().onShown(mManagerCallback)
,做的是发一个延迟消息MSG_TIMEOUT
,用来隐藏view
的事情,这一块最终是又走到了((Snackbar) message.obj).hideView(message.arg1)
这块就是下文dismiss
的内容了。
private void animateViewIn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ViewCompat.setTranslationY(mView, mView.getHeight());
ViewCompat.animate(mView).translationY(0f)
.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
.setDuration(ANIMATION_DURATION)
.setListener(new ViewPropertyAnimatorListenerAdapter() {
@Override
public void onAnimationStart(View view) {
mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,
ANIMATION_FADE_DURATION);
}
@Override
public void onAnimationEnd(View view) {
if (mCallback != null) {
mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(mManagerCallback);
}
}).start();
} else {
Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.design_snackbar_in);
anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
anim.setDuration(ANIMATION_DURATION);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
if (mCallback != null) {
mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(mManagerCallback);
}
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
});
mView.startAnimation(anim);
}
}
###dismiss()
dismiss()
方法内部是dispatchDismiss(Callback.DISMISS_EVENT_MANUAL)
,有了上一部分的基础后,dispatchDismiss
最终会走到sHandler
的handleMessage(message)
中,也就是MSG_DISMISS
消息的部分。((Snackbar) message.obj).hideView(message.arg1)
final void hideView(int event) {
if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) {
onViewHidden(event);
} else {
animateViewOut(event);
}
}
hideview
里面是个条件语句,满足的话执行onViewHidden(event)
,不满足的话执行animateViewOut(event)
。默认情况下会执行animateViewOut(event)
,snackbar
是缓慢的从屏幕中移除,在这个方法内部执行完动画后,还是会触发onViewHidden(event)
,这个方法做的是事情包括,告诉SnackbarManager
当前的snackbar
已经dismiss,准备显示下一个snackbar
;执行当前snackbar
的mCallback.onDismissed
方法,一般由应用开发者自己添加的;从视图中移除view
源码分析到此已经结束了。
##碰到过的问题 另外需要提几点注意事项,个人在开发过程中碰到的。
@NonNull
public Snackbar 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);
// Now dismiss the Snackbar
dispatchDismiss(Callback.DISMISS_EVENT_ACTION);
}
});
}
return this;
}
1 Snackbar
与FloatingActionButton
配合使用,父布局也的确是CoordinatorLayout
snackBar.setAction(R.string.snackbar_action, v -> { snackBar.dismiss(); });
在产品开发中,写过一段这样的代码。导致了一个明显的问题,当我们在弹出snackbar
后,点击Action
事件,发现floatingActionButton
不会再回到原来的位置了。当点击Action
后,一依次发送了两次Dismiss_event
事件,DISMISS_EVENT_MANUAL
和DISMISS_EVENT_ACTION
,仔细翻阅了Snackbar
相关的代码,还是没有看出来为啥,估计是只能发送一次吧,多次的话,Snackbar
已经不存在了,同时也没有办法通过CoordinatorLayout
改变FloatingActionButton
的布局了
2 Snackbar
的存在时间分析,在这篇文章开头的地方,使用的Snackbar.LENGTH_INDEFINITE
,这样产生的效果是snackbar
一直显示,而用其他两个常量,就会过一段时间消失。在Snackbar
的animateViewIn
方法中,动画执行完毕,接着执行了SnackbarManager.getInstance().onShown(mManagerCallback);
这段代码调用了SnackbarManager
的scheduleTimeoutLocked(SnackbarRecord)
,方法内部对LENGTH_INDEFINITE
类型的显示时间做了判断,过滤这种情况,只对另外两种类型,发送延迟消息MSG_TIMEOUT
,主线程收到此类型的消息调用handleTimeout(snackbarRecord)
,又会发送DISMISS_EVENT_TIMEOUT
的消息到sHanlder
中,这样就实现了过一会自动hide snackbar