主目录见:Android高级进阶知识(这是总目录索引)
一.目标
首先我们来明确一下这次源码解析的目标:
1.巩固上一篇《View的绘制(1)-setContentView源码分析》的源码机制.
2.同时为下一篇《利用decorView机制实现底部弹出框》做准备.
二.SnackBar源码分析
1.SnackBar的基本使用
1)只显示文本:
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG).show();
2)有点击按钮:
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG)
.setAction("UNDO", new View.OnClickListener() {
@Override
public void onClick(View v) {
//TODO do something
}
})
.show();
这两个就是SnackBar的基本使用,其他的使用方式可以查看文档,在这里不是重点,最后我们放上一张上篇分析源码得出的结论图,在这里会用到,以此来镇贴:
2.make 方法(注意:这里我的源代码版本是android-25)
我们遵循一贯查看源码的套路,从第一个使用到的方法make进入:
@NonNull
public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
@Duration int duration) {
Snackbar snackbar = new Snackbar(findSuitableParent(view));
snackbar.setText(text);
snackbar.setDuration(duration);
return snackbar;
}
方法很简单,这里有个关键方法是findSuitableParent(view)【这个方法很重要!!!】,这个方法的参数是我们传进来的视图,那他的作用是啥呢?我们跟进这个方法瞅瞅:
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
//如果找到的父节点是CoordinatorLayout则返回这个父节点
// We've found a CoordinatorLayout, use it
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
//如果找到的id为content的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);//循环向上遍历
return fallback;
}
这个方法里面的 if (view.getId() == android.R.id.content)用到的知识就是我们上次分析setContentView得出的结论,我们的视图是放在id为Content的Framelayout中即如下图,重要的事情贴两遍:
到这里我们的父视图已经找到, 后面我们自己的视图会添加到父视图下面。然后我们跟进SnackBar的构造方法里。
3.SnackBar构造方法
构造函数不是很麻烦,我们直接贴代码:
private Snackbar(ViewGroup parent) {
mTargetParent = parent;
mContext = parent.getContext();
//检查主题
ThemeUtils.checkAppCompatTheme(mContext);
LayoutInflater inflater = LayoutInflater.from(mContext);
mView = (SnackbarLayout) inflater.inflate(
R.layout.design_layout_snackbar, mTargetParent, false);
//获取无障碍辅助服务
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
我们看到源码里面会先调用ThemeUtils.checkAppCompatTheme(mContext);来检查主题,具体怎么检查这里不深究。我们直接看到下面一句会inflate一个design_layout_snackbar的layout来得到SnackBarLayout(这里的inflate方法干了什么在上一篇setContentView源码分析中有说过),那我们关注下两个东西:
1)design_layout_snackbar到底是啥样的
我们看到view标签里面有class="android.support.design.widget.Snackbar$SnackbarLayout"
说明这个view对应的布局就是SanckBarLayout,所以我们直接就看SnackBar的内部类SnackbarLayout是个啥:
2)SnackBarLayout
public static class SnackbarLayout extends LinearLayout {
}
看到这里顿时豁然开朗,原来inflate的这个视图是个LinearLayout呀。一万只草泥马奔腾而过.....
那接下来我们分部分来看SnackBarLayout的构造函数,看看这家伙干了些神马事:
2.1)第一部分是去获取属性,大家看代码应该是老友了
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);
2.2)然后就是我们的主要方法了,这里会去加载布局design_layout_snackbar_include布局
// Now inflate our content. We need to do this manually rather than using an
// in the layout since older versions of the Android do not inflate includes with
// the correct Context.
//睁大眼睛认真看!!!!!!,这里加载了的layout作为linearlayout的布局
LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
//底下省略一些代码
..................
return insets;
}
});
所以我们顺其自然地去看这个布局到底是何方神圣:
这个就是我们snackBar的主布局了,一个TextView一个Button,是不是到现在明白了为啥snackbar长那样:
这里做个总结:我们的make方法会根据用户传进去的锚点view进行查找父视图(CoordinateLayout或者id为content的framelayout),然后往父视图添加SnackBarLayout这个LinearLayout.
4.show方法
现在我们分析完make方法,我们就继续分析我们的show方法了。
public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
头一热,倒地休息五分钟......这里怎么又蹦出SnackbarManager和mManagerCallback这个未知生物。What a fucking source code!!!!
吐槽完默默继续,我们看下mManagerCallback是个什么东西:
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));
}
};
原来这个是一个回调,显示和隐藏,同时我们看到show和dismiss方法里面分别往Handler里面发送一个信息。我们直接跳到Handler里面看做了些啥动作:
sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_SHOW:
((Snackbar) message.obj).showView();
return true;
case MSG_DISMISS:
((Snackbar) message.obj).hideView(message.arg1);
return true;
}
return false;
}
});
我们看到Handler里面又调用了SnackBar类的showView和hideView方法,我们继续转到showView方法:
final void showView() {
//首先判断SnackbarLayout没有挂到其他的父视图上面
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 CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
//新建一个Behavior,有用过MD库的人都知道这个Behavior,主要是配合CoordinateLayout使用,在以后的文章会重点介绍
final Behavior behavior = new Behavior();
behavior.setStartAlphaSwipeDistance(0.1f);
behavior.setEndAlphaSwipeDistance(0.6f);
behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
//设置一个SwipeDismissBehavior,用来滑动删除
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:
// If the view is being dragged or settling, cancel the timeout
SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
break;
case SwipeDismissBehavior.STATE_IDLE:
// If the view has been released and is idle, restore the timeout
SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
break;
}
}
});
clp.setBehavior(behavior);
// Also set the inset edge so that views can dodge the snackbar correctly
clp.insetEdge = Gravity.BOTTOM;
}
//这个地方是重点mTargetParent就是我们刚才用锚点View查找到的父视图
mTargetParent.addView(mView);
}
//省略一些代码
.....................
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();
}
}
});
}
}
到这里我们已经把我们的SnackBar显示出来了,关键代码就是将视图添加进父视图Id为content的FrameLayout里面或者是CoordinateLayout里面(mTargetParent.addView(mView);)。然后就会判断需不需要有动画效果显示即 if (shouldAnimate()) {}.
5.SnackbarManager show方法
上面我们已经看完mManagerCallback 是啥了,我们是时候来看看SnackbarManager 的show方法了。首先我们看下SnackBarManager的getInstance():
static SnackbarManager getInstance() {
if (sSnackbarManager == null) {
sSnackbarManager = new SnackbarManager();
}
return sSnackbarManager;
}
其实就是个单例,我们就不去说明单例模式了,我们直接看show方法吧:
public void show(int duration, Callback callback) {
//这个地方加了个同步代码块
synchronized (mLock) {
//这个地方判断是不是就是目前的SnackBar
if (isCurrentSnackbarLocked(callback)) {
// Means that the callback is already in the queue. We'll just update the duration
//如果要显示的snackBar已经在显示队列里面则更新duration
mCurrentSnackbar.duration = duration;
// If this is the Snackbar currently being shown, call re-schedule it's
// timeout//移除Callback,避免内存泄露
mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
//);//重新关联设置duration和Callback
scheduleTimeoutLocked(mCurrentSnackbar);
return;
} else if (isNextSnackbarLocked(callback)) {
// //判断是否是接下来要显示的Snackbar,是则更新duration
// 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)) {
//取消当前的snackbar显示
// 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
//显示我们的snackBar
showNextSnackbarLocked();
}
}
}
从显示的代码中可以知道当目前的mCurrentSnackbar不为空的话,则后面显示的snackBar都会存储在mNextSnackbar中,只有当当前显示的Snackbar duration到了后,调用onDismissed方法,清空mCurrentSnackbar,然后才会显示下一个Snackbar。其中onDismissed方法就是在cancelSnackbarLocked中调用的,源码如下:
private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
final Callback callback = record.callback.get();
if (callback != null) {
// Make sure we remove any timeouts for the SnackbarRecord
mHandler.removeCallbacksAndMessages(record);
callback.dismiss(event);
return true;
}
return false;
}
dismiss完之后会把视图从父视图中删除。如果当前的snackBar为空则就显示我们新创建的snackBar:
private void showNextSnackbarLocked() {
if (mNextSnackbar != null) {
mCurrentSnackbar = mNextSnackbar;
mNextSnackbar = null;
final Callback callback = mCurrentSnackbar.callback.get();
if (callback != null) {
callback.show();
} else {
// The callback doesn't exist any more, clear out the Snackbar
mCurrentSnackbar = null;
}
}
}
到这里我们的snackBar源码已经分析完成,希望在下一篇我们能找到感觉。