Snackbar源码解析
来源:互联网 发布:电脑软件开发工具 编辑:程序博客网 时间:2024/05/21 14:32
引言
2015年5月,Google发布了Design Support Library
,添加了很多组件用于支持Material Design。至今过去已经两年了,版本也由当初的22.2.0
到现在的26.0.0 Alpha 1
。想要了解其中控件的实现原理,当然是从最简单的开始,那就是这篇文章的主角——Snackbar
。
基本使用
- 只有文本提示
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG).show();
- 有点击按钮
Snackbar.make(view, "This is a message", Snackbar.LENGTH_LONG) .setAction("UNDO", new View.OnClickListener() { @Override public void onClick(View v) { //TODO do something } }) .show();
当然还有其他的属性及方法,具体的可参考Google官方文档。
带着问题去阅读
- Snackbar是如何添加到界面上的?
- Snackbar的显示位置如何修改?
- Snackbar的布局是否可以修改?
- 多个连续的Snackbar是如何管理显示的?
- 在CoordinatorLayout中使用FloatingActionButton和SnackBar时,为什么Snackbar不会遮挡FloatingActionButton?
源码解析
源码基于25.3.0
解读源码,应该从什么地方下手呢?当然是从我们使用SnackBar
最常用的方法下手了,第一个使用到的那就是make
方法了。
make方法
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration) { final ViewGroup parent = findSuitableParent(view); if (parent == null) { throw new IllegalArgumentException("No suitable parent found from the given view. " + "Please provide a valid view."); } ... // 后面代码省略 }
SnackBar中有两个make方法,区别是提示文字传递的类型,一个是CharSequence
,一个是Resouse id
。传Resouse id
最终也会走到上述方法中。
先来看看方法内第一行代码,调用了findSuitableParent(View 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;}
代码量不大,而且注释也很清楚。此方法的作用就是循环查找view的上层ViewGroup,直到找到CoordinatorLayout或到根布局结束,返回找到的ViewGroup。
根布局:id为android.R.id.content的布局实际上就是我们setContentView设置自己写的布局的父ViewGroup,类型是FrameLayout,具体的可以去了解DecorView。
再回过头来看看Snackbar的make方法:
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, @Duration int duration) { ....//前面代码省略 final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); final SnackbarContentLayout content = (SnackbarContentLayout) inflater.inflate( R.layout.design_layout_snackbar_include, parent, false); final Snackbar snackbar = new Snackbar(parent, content, content); snackbar.setText(text); snackbar.setDuration(duration); return snackbar; }
通过inflate获取到SnackBarContentLayout布局,SnackBarContentLayout实际上是一个LinearLayout,再来看看R.layout.design_layout_snackbar_include:
<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/design_snackbar_padding_vertical" android:paddingBottom="@dimen/design_snackbar_padding_vertical" android:paddingLeft="@dimen/design_snackbar_padding_horizontal" android:paddingRight="@dimen/design_snackbar_padding_horizontal" android:textAppearance="@style/TextAppearance.Design.Snackbar.Message" android:maxLines="@integer/design_snackbar_text_max_lines" android:layout_gravity="center_vertical|left|start" android:ellipsize="end" android:textAlignment="viewStart"/> <Button android:id="@+id/snackbar_action" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal" android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal" android:layout_gravity="center_vertical|right|end" android:paddingTop="@dimen/design_snackbar_padding_vertical" android:paddingBottom="@dimen/design_snackbar_padding_vertical" android:paddingLeft="@dimen/design_snackbar_padding_horizontal" android:paddingRight="@dimen/design_snackbar_padding_horizontal" android:visibility="gone" android:textColor="?attr/colorAccent" style="?attr/borderlessButtonStyle"/></merge>
没错,这就是Snackbar的主要布局了,一个TextView和一个Button。
获取到的SnackbarContentLayout,通过实例化Snackbar,传进了Snackbar的构造方法中,最后到了Snackbar的父类BaseTransientBottomBar的构造方法中:
protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content, @NonNull ContentViewCallback contentViewCallback) { ... //省略不重要代码 mTargetParent = parent; //之前findSuitableParent方法找到的ViewGroup //callback传进来的SnackbarContentLayout,其实现了ContentViewCallback接口 mContentViewCallback = contentViewCallback; mContext = parent.getContext(); ThemeUtils.checkAppCompatTheme(mContext); LayoutInflater inflater = LayoutInflater.from(mContext); // Note that for backwards compatibility reasons we inflate a layout that is defined // in the extending Snackbar class. This is to prevent breakage of apps that have custom // coordinator layout behaviors that depend on that layout. mView = (SnackbarBaseLayout) inflater.inflate( R.layout.design_layout_snackbar, mTargetParent, false); mView.addView(content);//将SnackbarContentLayout添加到SnackbarLayout中 ...//省略剩余代码 } /** * Returns the {@link BaseTransientBottomBar}'s view. */ @NonNull public View getView() { return mView; }
已经备注很详细了,接着来看看R.layout.design_layout_snackbar:
<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" />
注意到class了吗,没错,此View是Snackbar中定义的SnackbarLayout,继承自BaseTransientBottomBar中的SnackbarBaseLayout,而SnackbarBaseLayout继承自FrameLayout。SnackbarLaout中只重新了onMeasure方法,其他实现都在SnackbarBaseLayout中。
还有一个关键的地方,layout_gravity被设置成了bottom,这也是为什么Snackbar总显示在底部的原因。
到这里我们已经知道了Snackbar的布局实际上是一个FrameLayout,其内容是一个LinearLayout。BaseTransientBottomBar提供了getView方法来获取mView,mView即为Snackbar的根布局FrameLayout。既然能获取到根布局,那往此布局中addView肯定是没问题了,之前提到的问题3也可以利用这一点解决了。
show方法
为什么不是action,而是show。因为我关心的是Snackbar如何显示的。
public void show() { SnackbarManager.getInstance().show(mDuration, mManagerCallback); }
看到这里是不是有点蒙蔽了,怎么到了SnackbarManager的show方法了,不着急,我们先来看看mManagerCallback:
final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() { @Override public void show() { sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this)); } @Override public void dismiss(int event) { sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0, BaseTransientBottomBar.this)); } };
mManagerCallback内部使用了Handler来控制show和dismiss,最终sHandler会调用showView:
final void showView() { if (mView.getParent() == null) { final ViewGroup.LayoutParams lp = mView.getLayoutParams(); if (lp instanceof CoordinatorLayout.LayoutParams) { // 如果LayoutParams是CoordinatorLayout的,就设置Behavior final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp; final Behavior behavior = new Behavior(); behavior.setStartAlphaSwipeDistance(0.1f); behavior.setEndAlphaSwipeDistance(0.6f); //设置SwipeDismissBehavior,具体作用就是滑动删除view behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END); behavior.setListener(new SwipeDismissBehavior.OnDismissListener() { @Override public void onDismiss(View view) { view.setVisibility(View.GONE); dispatchDismiss(BaseCallback.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, pause the timeout SnackbarManager.getInstance().pauseTimeout(mManagerCallback); break; case SwipeDismissBehavior.STATE_IDLE: // If the view has been released and is idle, restore the timeout SnackbarManager.getInstance() .restoreTimeoutIfPaused(mManagerCallback); break; } } }); clp.setBehavior(behavior); // Also set the inset edge so that views can dodge the bar correctly clp.insetEdge = Gravity.BOTTOM; } //重点来了,mView被添加到了mTargetParent中,之前向上遍历view获取的ViewGroup mTargetParent.addView(mView); } mView.setOnAttachStateChangeListener( new BaseTransientBottomBar.OnAttachStateChangeListener() { @Override public void onViewAttachedToWindow(View v) {} @Override public void onViewDetachedFromWindow(View v) { if (isShownOrQueued()) { // If we haven't already been dismissed then this event is coming from a // non-user initiated action. Hence we need to make sure that we callback // and keep our state up to date. We need to post the call since // removeView() will call through to onDetachedFromWindow and thus overflow. sHandler.post(new Runnable() { @Override public void run() { onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL); } }); } } }); if (ViewCompat.isLaidOut(mView)) { if (shouldAnimate()) { // If animations are enabled, animate it in animateViewIn(); } else { // Else if anims are disabled just call back now onViewShown(); } } else { // Otherwise, add one of our layout change listeners and show it in when laid out mView.setOnLayoutChangeListener(new BaseTransientBottomBar.OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom) { mView.setOnLayoutChangeListener(null); if (shouldAnimate()) { // If animations are enabled, animate it in animateViewIn(); } else { // Else if anims are disabled just call back now onViewShown(); } } }); }}
通过showView中的代码,终于了解到Snackbar是如何显示的。Snackbar被直接添加到mTargetParent中,就是make方法传递进来的View的父CoordinatorLayout或根布局。
根据Snackbar的布局文件我们知道其layout_gravity为bottom,也就是会显示在mTargetParent的底部。那我们是不是只要将一个有固定高度的CoordinatorLayout传递个给Snackbar的make,就可以改变Snackbar的显示位置了?答案是肯定的!
到这里我们的问题1和问题2都明了了。那问题4应该如何解决呢?
SnackbarManager
根据上面的分析,我们知道show方法会调用SnackbarManager的show方法,那我们就来看看SnackBarManager的源码:
class SnackbarManager { static final int MSG_TIMEOUT = 0; private static final int SHORT_DURATION_MS = 1500; private static final int LONG_DURATION_MS = 2750; private static SnackbarManager sSnackbarManager; //单例模式 static SnackbarManager getInstance() { if (sSnackbarManager == null) { sSnackbarManager = new SnackbarManager(); } return sSnackbarManager; } private final Object mLock; private final Handler mHandler; //用来存储当前显示Snackbar的duration和Callback private SnackbarRecord mCurrentSnackbar; //用来存储接下来要显示的Snackbar的duration和Callback private SnackbarRecord mNextSnackbar; private SnackbarManager() { mLock = new Object(); 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; } }); } interface Callback { void show(); void dismiss(int event); } public void show(int duration, Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback)) { //判断是否是当前显示的Snackbar,更新duration // 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);//移除Callback,避免内存泄露 scheduleTimeoutLocked(mCurrentSnackbar);//重新关联设置duration和Callback return; } else if (isNextSnackbarLocked(callback)) { //判断是否是接下来要显示的Snackbar,更新duration // 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(); } } } public void dismiss(Callback callback, int event) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback)) { cancelSnackbarLocked(mCurrentSnackbar, event); } else if (isNextSnackbarLocked(callback)) { cancelSnackbarLocked(mNextSnackbar, event); } } } /** * Should be called when a Snackbar is no longer displayed. This is after any exit * animation has finished. */ public void onDismissed(Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback)) { // If the callback is from a Snackbar currently show, remove it and show a new one mCurrentSnackbar = null; if (mNextSnackbar != null) { showNextSnackbarLocked(); } } } } /** * Should be called when a Snackbar is being shown. This is after any entrance animation has * finished. */ public void onShown(Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback)) { scheduleTimeoutLocked(mCurrentSnackbar); } } } public void pauseTimeout(Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback) && !mCurrentSnackbar.paused) { mCurrentSnackbar.paused = true; mHandler.removeCallbacksAndMessages(mCurrentSnackbar); } } } public void restoreTimeoutIfPaused(Callback callback) { synchronized (mLock) { if (isCurrentSnackbarLocked(callback) && mCurrentSnackbar.paused) { mCurrentSnackbar.paused = false; scheduleTimeoutLocked(mCurrentSnackbar); } } } public boolean isCurrent(Callback callback) { synchronized (mLock) { return isCurrentSnackbarLocked(callback); } } public boolean isCurrentOrNext(Callback callback) { synchronized (mLock) { return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback); } } private static class SnackbarRecord { final WeakReference<Callback> callback; int duration; boolean paused; SnackbarRecord(int duration, Callback callback) { this.callback = new WeakReference<>(callback); this.duration = duration; } boolean isSnackbar(Callback callback) { return callback != null && this.callback.get() == callback; } } 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; } } } //具体的cancel方法,回调callback的dismiss private boolean cancelSnackbarLocked(SnackbarRecord record, int event) { final Callback callback = record.callback.get(); if (callback != null) { // Make sure we remove any timeouts for the SnackbarRecord mHandler.removeCallbacksAndMessages(record); callback.dismiss(event); return true; } return false; } private boolean isCurrentSnackbarLocked(Callback callback) { return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback); } private boolean isNextSnackbarLocked(Callback callback) { return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback); } 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); } void handleTimeout(SnackbarRecord record) { synchronized (mLock) { if (mCurrentSnackbar == record || mNextSnackbar == record) { cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT); } } }}
单例模式,持有两个SnackbarRecord对象来存储当前显示和接下来要显示的Snackbar的duration和Callback,也就是说最多只会保有两个Snackbar的管理。
看其中的show方法可知,当mCurrentSnackbar不为null的时候,后面来的Snackbar都会存储在mNextSnackbar中。只有当当前显示的Snackbarduration到了后,调用onDismissed方法,清空mCurrentSnackbar,然后才会显示下一个Snackbar。
也就是说,当一个Snackbar显示期间,多次创建其他Snackbar show,到当前显示的Snackbar结束后,也只会显示最后一个创建的Snackbar。
到这里,问题4也清楚了。至于问题5,涉及到CoordinatorLayout Behavior,可阅读我的另一篇文章——《CoordinatorLayout源码解析》。
- SnackBar源码解析
- Snackbar源码解析
- SnackBar 源码bug修复、源码解析
- Snackbar新版Toast 从源码角度完全解析
- SnackBar源码解析-了解它的工作原理
- Snackbar源码分析
- Android Snackbar简单解析
- snackBar
- SnackBar
- Snackbar
- SnackBar
- Snackbar
- Snackbar
- Snackbar
- Snackbar
- Snackbar
- Snackbar
- Snackbar
- iOS——Storyboard使用
- PAT(Python)-1020:月饼(25)
- kafka源码分析之一server启动分析
- 欢迎使用CSDN-markdown编辑器
- nyoj-背包问题(贪心)
- Snackbar源码解析
- 关于Jdbc调用存储过程得到返回值为0(null)的问题
- c语言文件的读写操作
- bzoj3527 [Zjoi2014]力
- 在现有系统基础上扩展storm
- JavaScript学习one
- Java启动参数
- Tricky Sum
- vim设置