Google退出Matrial Design已经有一段时间了,其中有一款控件,叫做Snackbar。
那么它是做什么的呢?开发过Android的童鞋们应该对Toast不陌生了,Toast是一款用于在屏幕上显示提示信息的控件。而Snackbar,可以理解为Matrial Design风格的Toast,并且在功能上也有了一定的加强。
废话不多说,上图:
图中屏幕下方弹出的框就是Snackbar。其有着与Toast一样的特性(定时隐藏),也有一些新的特性(可按钮,不点击后隐藏等)。
Snackbar的用法并不复杂,使用时并不需要手动去创建Snackbar对象,其用法与Toast相似:
Snackbar.make(view,"显示的内容", Snackbar.LENGTH_LONG).show();
而如果要添加一个按钮并增加点击事件,只需要使用setAction方法即可。
Snackbar中的按钮被点击后,Snackbar会自动消失,并响应//点击事件:
Snackbar.make(view, "显示的内容", Snackbar.LENGTH_LONG)
.setAction("按钮", new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO: 点击按钮后的处理
}
}).show();
当然,Snackbar的每一种属性都可以单独设置,不过其构造方法是私有的,所以获取Snackbar对象的途径只有Snackbar.make方法:
//view可以为布局中任何控件或者跟布局。
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration)
public class MainActivity extends AppCompatActivity {
Snackbar snackbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
FloatingActionButton fab = findViewById(R.id.fab);
snackbar = Snackbar.make(fab, "显示的内容", Snackbar.LENGTH_LONG)
.setAction("按钮", new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO: 点击按钮后的处理
}
});
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
snackbar.setText("修改后的内容");
snackbar.setAction("修改后的按钮文字", new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO: 点击按钮后的处理
}
});
snackbar.show();
}
});
}
}
那么了解了Snackbar的用法,大家可能依然会有许多疑问,比如:
首先,我们需要知道,我们调用Snackbar的make方法后,再次调用show方法,即可将其显示到屏幕上。所以我们就从这两个方法入手:
make方法(ctrl+鼠标左键点进去):
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {
ViewGroup parent = findSuitableParent(view);
if (parent == null) {
throw new IllegalArgumentException("No suitable parent found from the given view. Please provide a valid view.");
} else {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
SnackbarContentLayout content = (SnackbarContentLayout)inflater.inflate(hasSnackbarButtonStyleAttr(parent.getContext()) ? layout.mtrl_layout_snackbar_include : layout.design_layout_snackbar_include, parent, false);
Snackbar snackbar = new Snackbar(parent, content, content);
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
}
从Snackbar的make方法可以看到,其通过我们传递的view
来获取到了一个ViewGroup parent
。之后又通过parent
创建了LayoutInflater
并取得了SnackbarContentLayout content
。接下来内部通过new创建了一个Snackbar
实例,并且设置显示的文字及显示时长。最终将设置好的Snackbar
返回。
看到这里我们可以确定每次调用Snackbar
就会新建一个Snackbar
对象。而其他代码的意义却不明朗。没关系,继续分析。
首先,我们看看第一行代码:ViewGroup parent = findSuitableParent(view);
。可以推测,findSuitableParent
方法通过view
找到了一个ViewGroup
。进入方法查看:
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
return (ViewGroup)view;
}
if (view instanceof FrameLayout) {
if (view.getId() == 16908290) {
return (ViewGroup)view;
}
fallback = (ViewGroup)view;
}
if (view != null) {
ViewParent parent = view.getParent();
view = parent instanceof View ? (View)parent : null;
}
} while(view != null);
return fallback;
}
emmm,由于编译器问题,暂时无法直观的看到一些值,这里做一下说明:16908290
是android.R.id.content
。其为界面的跟布局的id,用户自己写的布局都在这个布局下。
该方法循环遍历传入View及其父布局,直至找到CoordinatorLayout
或界面的跟布局。该方法中的CoordinatorLayout
略过,Snackbar
与CoordinatorLayout
配合可以达到滑动关闭Snackbar
的效果,有兴趣的童鞋自行研究。我们目前默认该方法返回所传view所在界面的跟布局即可。
make方法中还有一个布局:SnackbarContentLayout content
。其为Snackbar的布局。SnackbarContentLayout 代码片段如下:
public class SnackbarContentLayout extends LinearLayout implements ContentViewCallback {
private TextView messageView;
private Button actionView;
private int maxWidth;
private int maxInlineActionWidth;
···
可以看到,Snackbar的布局比较简单,继承自LinearLayout,内部包含了一个TextView
与Button
。根据其UI,我们可以确定TextView为其显示内容的控件,Button为点击按钮。
看到这里,make方法大致已经看完了,它主要是创建了一个Snackbar实例,入参为合适的值。并为其进行了基本数据的初始化。
那么,需要了解更多细节,我们还需要深入到构造方法中:
private Snackbar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) {
super(parent, content, contentViewCallback);
this.accessibilityManager = (AccessibilityManager)parent.getContext().getSystemService("accessibility");
}
尼玛,除了传了一个值this.accessibilityManager
,一个super完事。而Snackbar
是继承自BaseTransientBottomBar
的:
public final class Snackbar extends BaseTransientBottomBar
好吧,接着看BaseTransientBottomBar的构造方法:
protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content, @NonNull com.google.android.material.snackbar.ContentViewCallback contentViewCallback) {
if (parent == null) {
throw new IllegalArgumentException("Transient bottom bar must have non-null parent");
} else if (content == null) {
throw new IllegalArgumentException("Transient bottom bar must have non-null content");
} else if (contentViewCallback == null) {
throw new IllegalArgumentException("Transient bottom bar must have non-null callback");
} else {
this.targetParent = parent;
this.contentViewCallback = contentViewCallback;
this.context = parent.getContext();
ThemeEnforcement.checkAppCompatTheme(this.context);
LayoutInflater inflater = LayoutInflater.from(this.context);
this.view = (BaseTransientBottomBar.SnackbarBaseLayout)inflater.inflate(this.getSnackbarBaseLayoutResId(), this.targetParent, false);
this.view.addView(content);
ViewCompat.setAccessibilityLiveRegion(this.view, 1);
ViewCompat.setImportantForAccessibility(this.view, 1);
ViewCompat.setFitsSystemWindows(this.view, true);
ViewCompat.setOnApplyWindowInsetsListener(this.view, new OnApplyWindowInsetsListener() {
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), insets.getSystemWindowInsetBottom());
return insets;
}
});
ViewCompat.setAccessibilityDelegate(this.view, new AccessibilityDelegateCompat() {
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.addAction(1048576);
info.setDismissable(true);
}
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (action == 1048576) {
BaseTransientBottomBar.this.dismiss();
return true;
} else {
return super.performAccessibilityAction(host, action, args);
}
}
});
this.accessibilityManager = (AccessibilityManager)this.context.getSystemService("accessibility");
}
}
哇,这个方法中内容就比较多了。不过我们只需要带着 “Snackbar如何显示” 的问题去看代码,所以可以忽略掉其中一些内容。简化版如下:
protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content, @NonNull com.google.android.material.snackbar.ContentViewCallback contentViewCallback) {
``````
this.targetParent = parent;
this.contentViewCallback = contentViewCallback;
this.context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(this.context);
this.view = (BaseTransientBottomBar.SnackbarBaseLayout) inflater.inflate(this.getSnackbarBaseLayoutResId(), this.targetParent, false);
this.view.addView(content);
``````
}
可以看到,该方法主要时将入参赋值给全局变量。其中content
添加到了this.view
中。this.view
为BaseTransientBottomBar.SnackbarBaseLayout
。这个布局是一个Framelayout:
protected static class SnackbarBaseLayout extends FrameLayout
其内部设置了一些自己的监听,暂不考虑。
make方法看完了,接下来我们看一下show方法:
public void show() {
super.show();
}
好吧,接着点进去:
public void show() {
SnackbarManager.getInstance().show(this.getDuration(), this.managerCallback);
}
这里出现了另外一个类:SnackbarManager
。它是Snackbar
的管理类,管理其显示、隐藏、超时等逻辑。我们先看其show方法:
public void show(int duration, SnackbarManager.Callback callback) {
synchronized(this.lock) {
if (this.isCurrentSnackbarLocked(callback)) {
this.currentSnackbar.duration = duration;
this.handler.removeCallbacksAndMessages(this.currentSnackbar);
this.scheduleTimeoutLocked(this.currentSnackbar);
} else {
if (this.isNextSnackbarLocked(callback)) {
this.nextSnackbar.duration = duration;
} else {
this.nextSnackbar = new SnackbarManager.SnackbarRecord(duration, callback);
}
if (this.currentSnackbar == null || !this.cancelSnackbarLocked(this.currentSnackbar, 4)) {
this.currentSnackbar = null;
this.showNextSnackbarLocked();
}
}
}
}
这里有几个方法我们需要了解一下它的作用:isCurrentSnackbarLocked
、scheduleTimeoutLocked
、isNextSnackbarLocked
。
isCurrentSnackbarLocked
:
private boolean isCurrentSnackbarLocked(SnackbarManager.Callback callback) {
return this.currentSnackbar != null && this.currentSnackbar.isSnackbar(callback);
}
该方法在currentSnackbar
不为空且传入的SnackbarManager.Callback
为currentSnackbar
中的Callback是,返回true。
Callback
是什么?往下看:
private static class SnackbarRecord {
final WeakReference callback;
int duration;
boolean paused;
SnackbarRecord(int duration, SnackbarManager.Callback callback) {
this.callback = new WeakReference(callback);
this.duration = duration;
}
boolean isSnackbar(SnackbarManager.Callback callback) {
return callback != null && this.callback.get() == callback;
}
}
interface Callback {
void show();
void dismiss(int var1);
}
它是SnackManager中定义的一个接口,在BaseTransientBottomBar
中被实现,用于实现Snackbar的显示与消失。而其可以绑定在SnackbarRecord
下。而show方法中,该对象实例赋值给了this.nextSnackbar
:
this.nextSnackbar = new SnackbarManager.SnackbarRecord(duration, callback);
看到这里,我们大概可以猜到,this.currentSnackbar
与this.nextSnackbar
主要是用于操作Snackbar
排队显示逻辑的,这点先记下,后面的问题中探索,暂时略过。
我们看一下Callback
在BaseTransientBottomBar
中的实现:
final Callback managerCallback = new Callback() {
public void show() {
BaseTransientBottomBar.handler.sendMessage(BaseTransientBottomBar.handler.obtainMessage(0, BaseTransientBottomBar.this));
}
public void dismiss(int event) {
BaseTransientBottomBar.handler.sendMessage(BaseTransientBottomBar.handler.obtainMessage(1, event, 0, BaseTransientBottomBar.this));
}
};
可以看到,其回调方法都是向BaseTransientBottomBar
中的handle发送消息,而handle的实现如下:
handler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
public boolean handleMessage(Message message) {
switch(message.what) {
case 0:
((BaseTransientBottomBar)message.obj).showView();
return true;
case 1:
((BaseTransientBottomBar)message.obj).hideView(message.arg1);
return true;
default:
return false;
}
}
});
其中调用了showView与hideView,很容易猜到它们就是显示与隐藏Snackbar的真正方法了。(绕了这么久终于找到了)。
我们看看showView方法:
final void showView() {
LayoutParams lp = this.view.getLayoutParams();
if (lp instanceof androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams) {
``````
}
this.targetParent.addView(this.view);
``````
}
(动画相关代码省略)可以看到,在这里,关键代码只有一行,即this.targetParent.addView(this.view);
。将Snackbar的View添加到根布局中。
至此,Snackbar的显示逻辑看完············································了吗?
我们已经知道了Snackbar是动态添加到根布局的一个View,但是我们并没到看到哪里出发了Callback
中的show
回调。
秉承着做事有始有终的原则,我们接着看。回到SnackbarManager
的show
方法,其大意为:
若show的Snackbar是当前的对象(即当前Snackbar正在显示的时候再次调用其show方法),则设置超时时间,并重置超时时间;
若show的Snackbar不是当前对象,则判断排队逻辑后执行showNextSnackbarLocked
方法。
我们来看showNextSnackbarLocked
方法:
private void showNextSnackbarLocked() {
if (this.nextSnackbar != null) {
this.currentSnackbar = this.nextSnackbar;
this.nextSnackbar = null;
SnackbarManager.Callback callback = (SnackbarManager.Callback)this.currentSnackbar.callback.get();
if (callback != null) {
callback.show();
} else {
this.currentSnackbar = null;
}
}
}
其大意是执行排队逻辑后,调用callback.show();
。好的,至此,Snackbar的显示逻辑已经分析完成。
我们接下来分析第二个问题,这个问题在分析Snackbar显示逻辑的时候,与我们擦肩而过。我们回忆一下,显示逻辑中,SnackbarManager
的show
方法涉及了两个全局对象:
private SnackbarManager.SnackbarRecord currentSnackbar;
private SnackbarManager.SnackbarRecord nextSnackbar;
SnackbarRecord 的构造方法会绑定一个SnackbarManager.Callback
:
private static class SnackbarRecord {
final WeakReference callback;
int duration;
boolean paused;
SnackbarRecord(int duration, SnackbarManager.Callback callback) {
this.callback = new WeakReference(callback);
this.duration = duration;
}
···
}
而这个SnackbarManager.Callback
又来源于Snackbar的父类BaseTransientBottomBar
。该回调接口掌管的Snackbar的显示与隐藏,而显示与隐藏的真正逻辑在BaseTransientBottomBar
的handler中。我们重温一下SnackbarManager
的show
方法及其相关方法:
public void show(int duration, SnackbarManager.Callback callback) {
synchronized(this.lock) {
if (this.isCurrentSnackbarLocked(callback)) {
this.currentSnackbar.duration = duration;
this.handler.removeCallbacksAndMessages(this.currentSnackbar);
this.scheduleTimeoutLocked(this.currentSnackbar);
} else {
if (this.isNextSnackbarLocked(callback)) {
this.nextSnackbar.duration = duration;
} else {
this.nextSnackbar = new SnackbarManager.SnackbarRecord(duration, callback);
}
if (this.currentSnackbar == null || !this.cancelSnackbarLocked(this.currentSnackbar, 4)) {
this.currentSnackbar = null;
this.showNextSnackbarLocked();
}
}
}
}
private boolean isCurrentSnackbarLocked(SnackbarManager.Callback callback) {
return this.currentSnackbar != null && this.currentSnackbar.isSnackbar(callback);
}
private boolean isNextSnackbarLocked(SnackbarManager.Callback callback) {
return this.nextSnackbar != null && this.nextSnackbar.isSnackbar(callback);
}
private final Handler handler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
public boolean handleMessage(Message message) {
switch(message.what) {
case 0:
SnackbarManager.this.handleTimeout((SnackbarManager.SnackbarRecord)message.obj);
return true;
default:
return false;
}
}
});
void handleTimeout(SnackbarManager.SnackbarRecord record) {
synchronized(this.lock) {
if (this.currentSnackbar == record || this.nextSnackbar == record) {
this.cancelSnackbarLocked(record, 2);
}
}
}
private boolean cancelSnackbarLocked(SnackbarManager.SnackbarRecord record, int event) {
SnackbarManager.Callback callback = (SnackbarManager.Callback)record.callback.get();
if (callback != null) {
this.handler.removeCallbacksAndMessages(record);
callback.dismiss(event);
return true;
} else {
return false;
}
}
private void scheduleTimeoutLocked(SnackbarManager.SnackbarRecord r) {
if (r.duration != -2) {
int durationMs = 2750;
if (r.duration > 0) {
durationMs = r.duration;
} else if (r.duration == -1) {
durationMs = 1500;
}
this.handler.removeCallbacksAndMessages(r);
this.handler.sendMessageDelayed(Message.obtain(this.handler, 0, r), (long)durationMs);
}
}
方法看起来挺多的,这里对一些子方法一一做下解释:
currentSnackbar
中的Callback
是否与传入的Callback
相同,是则返回true,否则返回false。nextSnackbar
中的Callback
是否与传入的Callback
相同,是则返回true,否则返回false。handleTimeout
)cancelSnackbarLocked
好的,了解了上述代码,我们再次看一下SnackbarManager
的show
方法:
首先,代码最外层有一个if判断,其条件为this.isCurrentSnackbarLocked(callback)
。那么通过刚刚对该方法的理解,我们可以释义为:若需要show的Snackbar的Callback与当前SnackManager绑定的Callback相同,则进入if,否则进入else。
我们首先分析if中的代码:
this.currentSnackbar.duration = duration;
this.handler.removeCallbacksAndMessages(this.currentSnackbar);
this.scheduleTimeoutLocked(this.currentSnackbar);
可见,if中的代码做的事情就是重置了一遍使Snackbar
延时消失的时间。可见如果对同一个Snackbar
短时间内多次show
的话,每次show
方法都会重置其显示时间。
接下来看看重头戏,else中的代码:
if (this.isNextSnackbarLocked(callback)) {
this.nextSnackbar.duration = duration;
} else {
this.nextSnackbar = new SnackbarManager.SnackbarRecord(duration, callback);
}
if (this.currentSnackbar == null || !this.cancelSnackbarLocked(this.currentSnackbar, 4)) {
this.currentSnackbar = null;
this.showNextSnackbarLocked();
}
若nextSnackbar
的Callback
与传入的相同,则更新nextSnackbar
的duration
值(用于控制延时时间)。否则将传入的Callback
与duration
用于初始化nextSnackbar
。可见执行到此处,nextSnackbar
中的Callback
一定与传入的Callback
相同。
接下来,会判断this.currentSnackbar == null || !this.cancelSnackbarLocked(this.currentSnackbar, 4)
。当currentSnackbar
为空或关闭当前Snackbar失败时,会走进该分支执行关闭逻辑。否则不做任何操作。
emmm,看到这里,大家是不是有一个大大的疑惑:这里正常情况只做了关闭操作,那新的Snackbar在哪里打开呢?
我看代码的时候也产生了这样的疑惑,后来调试发现,在BaseTransientBottomBar
的onViewHidden
里,有这么一行代码:SnackbarManager.getInstance().onDismissed(this.managerCallback);
。而onDismissed
方法的实现如下:
public void onDismissed(SnackbarManager.Callback callback) {
synchronized(this.lock) {
if (this.isCurrentSnackbarLocked(callback)) {
this.currentSnackbar = null;
if (this.nextSnackbar != null) {
this.showNextSnackbarLocked();
}
}
}
}
可以看到,这里在关闭Snackbar后,紧接着会调用showNextSnackbarLocked
。而showNextSnackbarLocked
在显示逻辑中提到过,其主要作用是this.currentSnackbar = this.nextSnackbar;
且展示Snackbar。这里顺带在currentSnackbar
的Callback为空时清理掉了currentSnackbar
。
看到这里,Snackbar的排队逻辑差不多看完了:
emmm,看了这么多就得出这么两句话?WTF···
其实我们看源码,看的也是Google的设计理念。利用Callback来管理两个Snackbar,这样的思路,可以在我们自己的项目中使用。
这个问题在显示逻辑中已经讨论过了,所以这里就不再赘述了哈,大家可以在显示逻辑中查看findSuitableParent
方法的作用。
不得不说,Google的程序猿写代码还是相当严谨的,其中可以看到各种兼容性判断。
Snackbar属于动态添加至布局中的UI,其添加的思路、代码结构、暴露的接口,都值得我们借鉴。
后续我会补上仿Snackbar思路写的自定义UI控件的Demo。