本篇博客将会给大家带来一个轻量级控件SnackBar,为什么要讲SnackBar?Snackbar:的提出实际上是界于Toast和Dialog的中间产物。因为Toast与Dialog各有一定的不足,使用Toast的时候, 用户无法交互;使用Dialog:用户可以交互,但是体验会打折扣,会阻断用户的连贯性操作;但是使用Snackbar既可以做到轻量级的用户提醒效果,又可以有交互的功能,本博客将会从SnackBar的使用和源码分析两个方面进行介绍。
SnackBar的使用
SnackBar的使用十分简单,其实和Toast的使用方法差不多,我们写一个很简单的例子,来看一下SnackBar的使用,布局上有一个按钮,点击后弹出SnackBar,弹出的逻辑如下,布局代码很简单就不贴了。
public void showSnackBar(View view) {
//LENGTH_INDEFINITE:无穷
Snackbar snackbar = Snackbar.make(view,"您的Wifi已经开启!",Snackbar.LENGTH_INDEFINITE);
snackbar.setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "确定啦", Toast.LENGTH_SHORT).show();
}
});
snackbar.setCallback(new Snackbar.Callback() {
@Override
public void onDismissed(Snackbar snackbar, int event) {
Toast.makeText(MainActivity.this, "SnackBar消失了", Toast.LENGTH_SHORT).show();
}
@Override
public void onShown(Snackbar snackbar) {
Toast.makeText(MainActivity.this, "SnackBar出现了", Toast.LENGTH_SHORT).show();
}
});
snackbar.setActionTextColor(Color.BLUE);
snackbar.show();
}
可以看到上面代码,setAction方法用于给SnackBar设置按钮,setCallback方法用于设置回调,当SnackBar出现时或者消失时都会有相应的回调,同时setActionTextColor方法可以给改变SnackBar中按钮的颜色。
SnackBar的源码分析
SnackBar是通过make方法进行创建的,所以我们首先需要查看SnackBar的make方法
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方法,Snackbar内部把view传递给了这个方法,查看该方法的逻辑
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;
}
发现这里竟然是一个do while的循环,只要view!= null,就会一直循环下去,里面会对view进行判断,是CoordinatorLayout,则直接返回,如果是FrameLayout,并且当view.getId() == android.R.id.content时候,也将view进行返回,大家都知道R.id.content就是decorView下的content部分,否则就会将这个view赋值给fallback,这个fallback就是一个viewGroup。下面这一句非常关键
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;
}
取出view的Parent并且只要这个parent是View,就将其赋值给我门的view,到这里我们明白了,这个死循环就是为了无限的从传进来的这个view开始无限的向上寻找view的父亲,直到没有父亲为止,最后会返回fallback。然后我们自然会先去查看Snackbar构造函数,看它里面是进行了什么逻辑
private Snackbar(ViewGroup parent) {
mParent = parent;
mContext = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(mContext);
mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mParent, false);
}
在这里面最重要的一句就是渲染了一个R.layout.design_layout_snackbar的布局,很明显这个布局是系统自带的,很明显在这里已经写死了,所以我们想修改这个SnackBar显然是不行的,而且它还强转成了SnackbarLayout布局,我们可以查看一下这个布局的代码,这个布局在design包的layout下
<view xmlns:android="http://schemas.android.com/apk/res/android"
class="android.support.design.widget.Snackbar$SnackbarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
style="@style/Widget.Design.Snackbar" />
在这里我们可以学到2点,一是如何引用某个类里面的内部类,就是通过class=“”,第二点就是自定义控件的第二种引用方法,使用View标签,然后内部使用class进行引用。我们看一下SnackbarLayout的代码:
<pre name="code" class="java"><pre name="code" class="java">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.design_layout_snackbar_include, this);
}
里面会创建一个TypedArray,然后取出里面的属性进行设置,最后会渲染一个布局:R.layout.design_layout_snackbar_include,它被渲染到当前SnackbarLayout之中
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/snackbar_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingTop="@dimen/snackbar_padding_vertical"
android:paddingBottom="@dimen/snackbar_padding_vertical"
android:paddingLeft="@dimen/snackbar_padding_horizontal"
android:paddingRight="@dimen/snackbar_padding_horizontal"
android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
android:maxLines="@integer/snackbar_text_max_lines"
android:layout_gravity="center_vertical|left|start"
android:ellipsize="end"/>
<TextView
android:id="@+id/snackbar_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/snackbar_extra_spacing_horizontal"
android:layout_marginStart="@dimen/snackbar_extra_spacing_horizontal"
android:layout_gravity="center_vertical|right|end"
android:background="?attr/selectableItemBackground"
android:paddingTop="@dimen/snackbar_padding_vertical"
android:paddingBottom="@dimen/snackbar_padding_vertical"
android:paddingLeft="@dimen/snackbar_padding_horizontal"
android:paddingRight="@dimen/snackbar_padding_horizontal"
android:visibility="gone"
android:textAppearance="@style/TextAppearance.Design.Snackbar.Action"/>
</merge>
Snackbar的布局里面果然是使用了这个布局,如果我们要改变布局的样式,我们就修改这个文件里面的相关属性就可以了,就比如这里的textAppearance。我们回到Snackbar的构造方法中,同时它还把parent传了进去, 看过LayoutInflater源码的都知道,只有同时满足root不为空,而且attachToRoot为真的时候,root才会去添加这个渲染的temp,也就是我们上面传进来的R.layout.design_layout_snackbar,明显没有添加进mParent中去,那么Snackbar到底是在哪里addView的呢?我们一定要去追寻出这个添加Snackbar的地方。
if (root != null && attachToRoot) {
root.addView(temp, params);
}
我们跟踪mView这个变量,终于在showView方法中,找到了addView的足迹
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) {
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;
}
}
});
((CoordinatorLayout.LayoutParams) lp).setBehavior(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);
}
});
}
}
这里的代码比较长,我们一点一点进行分析,当mView.getParent() == null时,就是mView已经没有父View的时候,会取出它的LayoutParams,如果这个LayoutParams instanceofCoordinatorLayout.LayoutParams,然后是new一个Behavior,给Behavior设置各种参数以及监听,最后这个Behavior会设置给LayoutParams,然后这个mView最终会添加mParent的ViewGroup容器之中。
当view已经绘制完毕后,会给它设置一个出现的动画animateViewIn,否则会给mView设置布局变化的监听,每一次布局改变都会调用动画,并把监听设置为null,这里设置为null也是非常巧妙的,如果不这样设置,这个监听就会一直回调。
我们粗略查看一下animateViewIn的内部逻辑:
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);
}
}
其实就是进行判断,如果编译的版本大于3.0,就是用属性动画进行一系列的动画设置,否则就是用传统的动画设置。
接着我们查看一下Show方法的逻辑:
public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
这里用到了SnackbarManager,我们查看一下它的源码,看到getInstance就知道它肯定使用了单例的设计模式
static SnackbarManager getInstance() {
if (sSnackbarManager == null) {
sSnackbarManager = new SnackbarManager();
}
return sSnackbarManager;
}
直接查看show方法
synchronized (mLock) {
if (isCurrentSnackbar(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 (isNextSnackbar(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();
}
}
}
Show方法中会传进来一个callback,这个callback是一个接口,里面有两个抽象方法show和dismiss
interface Callback {
void show();
void dismiss(int event);
}
再回到show方法内部,可以发现首先是加了一个同步锁,这样的目的,我们也可以猜出来,就是防止多次对SnackBar调用show方法,只有当一个SnackBar show完事了之后,下一个SnackBar才能show,也可以看出来SnackbarManager是对SnackBar起到管理作用的。通过isCurrentSnackbar(callback)方法判断传入show方法的callback是否在队列之中,其中有一个SnackbarRecord类型的变量mCurrentSnackbar用于记录时间。
if (isCurrentSnackbar(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;
}
如果当前的Snackbar已经展示完毕,同时它的展示时间已经到了,mHandler就会发送一个消息,移除这个Snackbar的callback,同时调用scheduleTimeoutLocked方法,我们查看一下该方法的内部逻辑:
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);
mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
}
首先是根据给SnackBar设置的不同显示时长来进行相应处理,然后是调用mHandler的removeCallbacksAndMessages和sendMessageDelayed方法,进行消息的发送,接着我们可以看一下handler做了什么处理
mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_TIMEOUT:
handleTimeout((SnackbarRecord) message.obj);
return true;
}
return false;
}
});
当时间到了,会调用handleTimeout方法,SnackbarRecord会被传入这个方法之中
private void handleTimeout(SnackbarRecord record) {
synchronized (mLock) {
if (mCurrentSnackbar == record || mNextSnackbar == record) {
cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
}
}
}
在handleTimeout中同样会同步的调用cancelSnackbarLocked方法
private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
final Callback callback = record.callback.get();
if (callback != null) {
callback.dismiss(event);
return true;
}
return false;
}
这方法内部会从SnackbarRecord内部把callback取出来,如果callback不为空的时候,会调用callback的dismiss方法,回到show方法中,如果调用show方法的是下一个Snackbar就更新一下mNextSnackbar的duration,否则就new 一个SnackbarRecord。
接下来是判定,如果当前有一个Snackbar,就不做处理。
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();
}
如果当前SnackbarRecord不为空,而且其中的callback正在dismiss时,return,否则会清空当前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;
}
}
}
showNextSnackbarLocked其中的逻辑也很简单,把下一个SnackbarRecord赋值给当前的,取出里面的callback,不为空时调用show方法。我们再查看一下SnackbarRecord的源码:
private static class SnackbarRecord {
private final WeakReference<Callback> callback;
private int duration;
SnackbarRecord(int duration, Callback callback) {
this.callback = new WeakReference<>(callback);
this.duration = duration;
}
boolean isSnackbar(Callback callback) {
return callback != null && this.callback.get() == callback;
}
}
里面使用了一个弱引用来包裹callback,这里是很值得我们学习的,使用WeakReference可以较好的避免内存泄漏的问题。Callback我们之前说过是一个接口,我们需要找一下它的实现类,既然是在show方法中把callback传进来的,所以我们要寻找一下SnackBarManager的show方法是在哪里调用的。本篇之前我们就看过SnackBar的show方法,里面调用了SnackbarManager的show方法
public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
该方法内的参数mManagerCallback就是SnackBarManager内部Callback的实现类
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));
}
};
可以发现,其内部实现show与dismiss方法,使用sHandler发送不同的消息,查看sHandler的实现
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;
}
});
当message为MSG_SHOW时,会调用Snackbar的showView方法,当message为MSG_DISMISS时,会调用Snackbar的hideView,showView方法内部逻辑我们之前已经分析过了,再看一下hideView方法:
final void hideView(int event) {
if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) {
onViewHidden(event);
} else {
animateViewOut(event);
}
}
hideView方法内调用onViewHidden方法:
private void onViewHidden(int event) {
// First remove the view from the parent
mParent.removeView(mView);
// Now call the dismiss listener (if available)
if (mCallback != null) {
mCallback.onDismissed(this, event);
}
// Finally, tell the SnackbarManager that it has been dismissed
SnackbarManager.getInstance().onDismissed(mManagerCallback);
}
首先mParent会把mView进行移除,然后如果mCallback!= null,会调用mCallback的onDismissed方法,最后调用SnackbarManager的onDismissed的方法,将callback移除出队列,到这里SnackBar和SnackbarManager的源码我们就基本分析完毕了。