本文主要是介绍轻量级控件SnackBar使用以及源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本篇博客将会给大家带来一个轻量级控件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() {@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this, "确定啦", Toast.LENGTH_SHORT).show();}});snackbar.setCallback(new Snackbar.Callback() {@Overridepublic void onDismissed(Snackbar snackbar, int event) {Toast.makeText(MainActivity.this, "SnackBar消失了", Toast.LENGTH_SHORT).show();}@Overridepublic 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 itreturn (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 fallbackfallback = (ViewGroup) view;}}if (view != null) {// Else, we will loop and crawl up the view hierarchy and try to find a parentfinal 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 fallbackreturn 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 parentfinal 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"><TextViewandroid: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"/><TextViewandroid: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 Behaviorfinal 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() {@Overridepublic void onDismiss(View view) {dispatchDismiss(Callback.DISMISS_EVENT_SWIPE);}@Overridepublic void onDragStateChanged(int state) {switch (state) {case SwipeDismissBehavior.STATE_DRAGGING:case SwipeDismissBehavior.STATE_SETTLING:// If the view is being dragged or settling, cancel the timeoutSnackbarManager.getInstance().cancelTimeout(mManagerCallback);break;case SwipeDismissBehavior.STATE_IDLE:// If the view has been released and is idle, restore the timeoutSnackbarManager.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 nowanimateViewIn();} else {// Otherwise, add one of our layout change listeners and animate it in when laid outmView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {@Overridepublic 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() {@Overridepublic void onAnimationStart(View view) {mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,ANIMATION_FADE_DURATION);}@Overridepublic 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() {@Overridepublic void onAnimationEnd(Animation animation) {if (mCallback != null) {mCallback.onShown(Snackbar.this);}SnackbarManager.getInstance().onShown(mManagerCallback);}@Overridepublic void onAnimationStart(Animation animation) {}@Overridepublic 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 durationmCurrentSnackbar.duration = duration;// If this is the Snackbar currently being shown, call re-schedule it's// timeoutmHandler.removeCallbacksAndMessages(mCurrentSnackbar);scheduleTimeoutLocked(mCurrentSnackbar);return;} else if (isNextSnackbar(callback)) {// We'll just update the durationmNextSnackbar.duration = duration;} else {// Else, we need to create a new record and queue itmNextSnackbar = 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 linereturn;} else {// Clear out the current snackbarmCurrentSnackbar = null;// Otherwise, just show it nowshowNextSnackbarLocked();}}}
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 durationmCurrentSnackbar.duration = duration;// If this is the Snackbar currently being shown, call re-schedule it's// timeoutmHandler.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 timeoutreturn;}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() {@Overridepublic 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 linereturn;} else {// Clear out the current snackbarmCurrentSnackbar = null;// Otherwise, just show it nowshowNextSnackbarLocked();}
如果当前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 SnackbarmCurrentSnackbar = 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() {@Overridepublic void show() {sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));}@Overridepublic 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() {@Overridepublic 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 parentmParent.removeView(mView);// Now call the dismiss listener (if available)if (mCallback != null) {mCallback.onDismissed(this, event);}// Finally, tell the SnackbarManager that it has been dismissedSnackbarManager.getInstance().onDismissed(mManagerCallback);
}
首先mParent会把mView进行移除,然后如果mCallback!= null,会调用mCallback的onDismissed方法,最后调用SnackbarManager的onDismissed的方法,将callback移除出队列,到这里SnackBar和SnackbarManager的源码我们就基本分析完毕了。
这篇关于轻量级控件SnackBar使用以及源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!