前言
Android中的消息提示主要有一下几种:
- Toast: 用户无法交互。
- Dialog:用户可以交互,但是体验会打折扣,会阻断用户的连贯性操作。
- Snackbar既可以做到轻量级的用户提醒效果,又可以有交互的功能(必须是一种非必须的操作)。Snackbar的提出实际上是界于Toast和Dialog的中间产物。
自定义吐司
我们平常通过下面的代码进行弹出一个吐司的:
Toast.makeText(this, "吐司", Toast.LENGTH_SHORT).show();
深入makeText方法瞄一眼:
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
throws Resources.NotFoundException {
return makeText(context, context.getResources().getText(resId), duration);
}
另外一个重载:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
Toast result = new Toast(context);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
可以清晰地看到Toast的创建过程,因此我们也仿照这样就可以自定义吐司了:
Toast result = new Toast(SnackBarActivity.this);
LayoutInflater inflate = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View view = inflate.inflate(R.layout.my_toast, null);
TextView tv = (TextView) view.findViewById(R.id.message);
tv.setText("这是一个自定义吐司");
result.setView(view);
result.setDuration(Toast.LENGTH_SHORT);
result.show();
其中,mNextView以及mDuration是私有的成员,我们可以通过方法去设置。
最后看看show方法:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
可以看到Toast的管理是通过INotificationManager来实现的,这是一个与进程间通信有关的类。show方法调用的时候,并不是马上就显示的,而是加入了一个队列里面。
SnackBar的基本使用
下面先给出代码:
//其中View是一个锚点
Snackbar snackbar = Snackbar.make(v, "是否打开XXX模式", Snackbar.LENGTH_SHORT);
//只能设置一个Action
snackbar.setAction("打开", new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "打开XXX模式");
}
});
//监听打开与关闭
snackbar.setCallback(new Snackbar.Callback() {
@Override
public void onShown(Snackbar snackbar) {
super.onShown(snackbar);
Log.e(TAG, "显示");
}
@Override
public void onDismissed(Snackbar snackbar, int event) {
super.onDismissed(snackbar, event);
Log.e(TAG, "关闭");
}
});
snackbar.show();
Snackbar的Duration有三种:
- Snackbar.LENGTH_SHORT
- Snackbar.LENGTH_LONG
- Snackbar.LENGTH_INDEFINITE---无限长
make方法传入的是一个锚点,这里我传入了一个Button对象。然后还可以设置动作以及回调监听。
SnackBar源码分析
同理,我们先分析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;
}
@NonNull
public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {
return make(view, view.getResources().getText(resId), duration);
}
传入的view是一个锚点,SnackBar的显示需要依附于parent,就像我们创建View的时候需要传入一个context一样。(注意,这里的SnackBar只是一个简单的类,并不是继承于View的)我们需要的parent就是利用这个view通过findSuitableParent方法进行找到的:
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if (view instanceof CoordinatorLayout) {
// 如果是CoordinatorLayout直接返回
return (ViewGroup) view;
} else if (view instanceof FrameLayout) {
if (view.getId() == android.R.id.content) {
// 如归找到Android的根布局,那么直接返回
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;
}
如上面的代码所示,就是不断通过循环,view.getParent()不断获取parent去找到最合适的parent的。
下面我们继续分析构造方法:
private Snackbar(ViewGroup parent) {
mTargetParent = parent;
mContext = parent.getContext();
ThemeUtils.checkAppCompatTheme(mContext);
LayoutInflater inflater = LayoutInflater.from(mContext);
//渲染一个布局进来,但是并没有直接添加到parent里面去,最后Snackbar去通过addView或者通过动画显示出来
mView = (SnackbarLayout) inflater.inflate(R.layout.design_layout_snackbar, mTargetParent, false);
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
构造的时候就会先渲染一个布局然后设置进来,并且添加到parent里面,这个布局我们不能再修改了。其中SnackbarLayout是一个内部的类。
R.layout.design_layout_snackbar中就是引用了这个内部类,这是自定义控件的另外一种写法,通过使用view标签(小写),然后设置class属性:
在SnackbarLayout里面实质上会继续渲染一个布局到this(自己)里面来:
public SnackbarLayout(Context context, AttributeSet attrs) {
//...省略一些代码
LayoutInflater.from(context).inflate(R.layout.design_layout_snackbar_include, this);
}
同样,我们继续分析show方法:
public void show() {
SnackbarManager.getInstance().show(mDuration, mManagerCallback);
}
这里实际上是调用了SnackbarManager的show方法:
public void show(int duration, Callback callback) {
synchronized (mLock) {
if (isCurrentSnackbarLocked(callback)) {
// 如果是同一个,那么就更新时间参数
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)) {
// 如果是下一个
mNextSnackbar.duration = duration;
} else {
// 如果都不是,那么就创建一个记录。SnackbarRecord的作用相当于ActivityRecord
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;
// 显示
showNextSnackbarLocked();
}
}
}
这里进行了一次synchronized,就防止了同时显示多个的情况。这种队列(类似于消息机制)有效地防止了UI的ANR。
SnackbarRecord的定义如下,其中就包含了当前SnackBar的一些信息:
private static class SnackbarRecord {
final WeakReference callback;//弱引用防止内存泄漏
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;
}
}
继续分析showNextSnackbarLocked:
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对象,然后拿到callback对象,最后回调callback的show方法。
那么,我们的SnackBar是怎么显示出来的呢?还记得SnackbarManager的show方法传入的callback是在SnackBar中定义的(callback接口是Snackbar与SnackbarManager交互的接口):
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));
}
};
最后的显示是通过handler来实现的:
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();
return true;
case MSG_DISMISS:
((Snackbar) message.obj).hideView(message.arg1);
return true;
}
return false;
}
});
}
下面继续分析showView方法:
final void showView() {
if (mView.getParent() == null) {
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
if (lp instanceof CoordinatorLayout.LayoutParams) {
//如果parent是CoordinatorLayout的处理
}
mTargetParent.addView(mView);
}
mView.setOnAttachStateChangeListener(new SnackbarLayout.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {}
@Override
public void onViewDetachedFromWindow(View v) {
if (isShownOrQueued()) {
sHandler.post(new Runnable() {
@Override
public void run() {
onViewHidden(Callback.DISMISS_EVENT_MANUAL);
}
});
}
}
});
//如果parent以及显示出来了,那么直接animateViewIn
if (ViewCompat.isLaidOut(mView)) {
if (shouldAnimate()) {
animateViewIn();
} else {
onViewShown();
}
} else {
//否则的话需要设置setOnLayoutChangeListener等待布局显示出来以后才能animateViewIn
mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
mView.setOnLayoutChangeListener(null);
if (shouldAnimate()) {
animateViewIn();
} else {
onViewShown();
}
}
});
}
}
下面继续分析animateViewIn:
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) {
onViewShown();
}
}).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) {
onViewShown();
}
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationRepeat(Animation animation) {}
});
mView.startAnimation(anim);
}
}
这里根据不同的SDK版本,利用了不同的动画去实现。
如果觉得我的文字对你有所帮助的话,欢迎关注我的公众号:
我的群欢迎大家进来探讨各种技术与非技术的话题,有兴趣的朋友们加我私人微信huannan88,我拉你进群交(♂)流(♀)。