本篇博客将会给大家带来一个轻量级控件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的源码我们就基本分析完毕了。