前言
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属性:
<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" />
在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> 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版本,利用了不同的动画去实现。