Android Study Material Design 五 番外篇 之:深入分析SnackBar源码
LZ-Says:兜兜转转,似乎再次回到起点。。。
前言
一个人,还是会有些寂寥,孤僻。。。
今天,怀着不知名的心情,一起来分析下SnackBar源码,看看从源码中,我们能get到什么技能。
本文目标
通过源码的角度来了解谷歌大牛是如何Coding的。
分析
这里,我们主要分析SnackBar暴露的俩个部分,一为make(),二为show()。
make()分析
我们在上一篇文章中,简单的了解了SnackBar使用,通过解决部分答疑,让你不知不觉中get一项又一项技能。
SnackBar的易用,我们再次回顾下,示例化SnackBar,我们只需要调用make()方法即可,类似于Toast,今天我们就从它入手,看看人家是怎么玩的。
@NonNull // 注解 非空
public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {
// 初始化SnackBar
Snackbar snackbar = new Snackbar(findSuitableParent(view));
// 赋值
snackbar.setText(text);
// 设置显示时间
snackbar.setDuration(duration);
return snackbar;
}
而接下来,我们看下@NonNull这里面又写了什么。
/**
* Denotes that a parameter, field or method return value can never be null.
* <p>
* This is a marker annotation and it has no specific attributes.
*/
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD})
public @interface NonNull {
}
这里不得不说谷歌大牛,相当易读哈。can never be null,不能为空。
这里不知大家有没有注意到在初始化SnackBar中,还有一个神秘的家伙,还不知道它在搞什么?
private static ViewGroup findSuitableParent(View view) {
ViewGroup fallback = null;
do {
if(view instanceof CoordinatorLayout) {
return (ViewGroup)view; // 当前View属于CoordinatorLayout 直接返回
}
if(view instanceof FrameLayout) { // 当前View属于FrameLayout
if(view.getId() == 16908290) { // id等于content内容 当前activity包含的view
return (ViewGroup)view;
}
fallback = (ViewGroup)view;
}
if(view != null) {
ViewParent parent = view.getParent(); // 获取根布局
view = parent instanceof View?(View)parent:null; // 当前父布局属于view 直接return 否则return null
}
} while(view != null); // 一直在找根布局 找到最外层
return fallback;
}
由此可见,此方法的作用便是一直查找传入View的父布局,直到找到为止。
这里不得不说CoordinatorLayout,这家伙,是个好玩意,后期重点介绍下~
我们接下来看看SnackBar构造中又干了些什么鬼。
private Snackbar(ViewGroup parent) {
this.mParent = parent;
this.mContext = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(this.mContext);
// 渲染布局到View
this.mView = (Snackbar.SnackbarLayout)inflater.inflate(layout.design_layout_snackbar, this.mParent, false);
}
这里看到,谷歌大牛直接把SnackBar布局写死了。。。写死了。。。
关于inflate,这点LZ说明下,当最后参数为false时,此时布局是不会被加载到父布局中。当然,你可以理解为他们之间是独立的,这也就是为什么,当SnackBar弹出的时候,你可以选择与其交互,也可以继续你的操作的原因。
而关于为什么LZ知道,LZ这里放出源码内关键代码,大家一看便知:
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
同时这里也用到synchronized机制,这点不得不说人家谷歌大牛思考确实挺全面,值得一学。
make()源码分析到此,接下来分析下show(),看看他是如何被Show出来的呢?
show()分析
public void show() {
SnackbarManager.getInstance().show(this.mDuration, this.mManagerCallback);
}
点击进去可以看到关键点如下:
首先,其内部通过SnackbarManager管理类去对Snackbar进行管理;
其次,show的时候需要传入时间以及一个Callback
我们以此进行分解说明,一起探索未知世界。
public void show(int duration, SnackbarManager.Callback callback) {
Object var3 = this.mLock;
synchronized(this.mLock) { // 这里再次用到同步锁 保证每次只有一个进行
if(this.isCurrentSnackbar(callback)) {
this.mCurrentSnackbar.duration = duration;
// remove当前Callback
// 这里通过handler进行消息分发 又是handler 看来改天得分析下handler了
this.mHandler.removeCallbacksAndMessages(this.mCurrentSnackbar);
this.scheduleTimeoutLocked(this.mCurrentSnackbar);
} else {
if(this.isNextSnackbar(callback)) { // 这里大致猜测可能校验是否是下一个 如果是 设置时间 反之重新实例化
this.mNextSnackbar.duration = duration;
} else {
this.mNextSnackbar = new SnackbarManager.SnackbarRecord(duration, callback);
}
// 这里相当于进行了回收操作 这点感觉很nice
if(this.mCurrentSnackbar == null || !this.cancelSnackbarLocked(this.mCurrentSnackbar, 4)) {
this.mCurrentSnackbar = null;
this.showNextSnackbarLocked();
}
}
}
}
而实际分发中,还需要对用户传入显示值进行校验,校验通过后,开始分发消息。
private void scheduleTimeoutLocked(SnackbarManager.SnackbarRecord r) {
if(r.duration != -2) {
int durationMs = 2750;
// 如果大于0 便是用户设置值 反之设置默认
if(r.duration > 0) {
durationMs = r.duration;
} else if(r.duration == -1) {
durationMs = 1500;
}
// 移除
this.mHandler.removeCallbacksAndMessages(r);
// 分发
this.mHandler.sendMessageDelayed(Message.obtain(this.mHandler, 0, r), (long)durationMs);
}
}
下面瞅瞅是如何通过Handler进行消息分发的,到底在搞什么?
private final Handler mHandler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
public boolean handleMessage(Message message) {
switch(message.what) {
case 0: // 接收到hanlder消息进行处理
SnackbarManager.this.handleTimeout((SnackbarManager.SnackbarRecord)message.obj);
return true;
default:
return false;
}
}
});
private void handleTimeout(SnackbarManager.SnackbarRecord record) {
Object var2 = this.mLock;
synchronized(this.mLock) { // 同步锁
if(this.mCurrentSnackbar == record || this.mNextSnackbar == record) {
this.cancelSnackbarLocked(record, 2); // 关闭
}
}
}
private boolean cancelSnackbarLocked(SnackbarManager.SnackbarRecord record, int event) {
// 获取到回调
SnackbarManager.Callback callback = (SnackbarManager.Callback)record.callback.get();
if(callback != null) {
// dismiss
callback.dismiss(event);
return true;
} else {
return false;
}
}
看看人这套逻辑,思路,佩服。
噢,刚才遗漏一点,如下:
private static class SnackbarRecord {
// 弱引用 程序没有内存时 回收此类内存
private final WeakReference<SnackbarManager.Callback> callback;
private int duration;
SnackbarRecord(int duration, SnackbarManager.Callback callback) {
// 实例化
this.callback = new WeakReference(callback);
this.duration = duration;
}
boolean isSnackbar(SnackbarManager.Callback callback) {
return callback != null && this.callback.get() == callback;
}
}
大牛使用了弱引用,此类内存将在程序内存不足时自动回收~666
接下来我们关注一波文中常出现的Callback,看看这玩意是个什么鬼。
private static class SnackbarRecord {
// 弱引用 程序没有内存时 回收此类内存
private final WeakReference<SnackbarManager.Callback> callback;
private int duration;
SnackbarRecord(int duration, SnackbarManager.Callback callback) {
// 实例化
this.callback = new WeakReference(callback);
this.duration = duration;
}
boolean isSnackbar(SnackbarManager.Callback callback) {
return callback != null && this.callback.get() == callback;
}
}
提供显示以及dismiss俩种方式,正好配套。
还记得当我们调用show的时候,内部需要传入一个Callback么?感觉有关SnackBar真正显示的干货要来了。嘿嘿嘿~
private final android.support.design.widget.SnackbarManager.Callback mManagerCallback = new android.support.design.widget.SnackbarManager.Callback() {
public void show() {
Snackbar.sHandler.sendMessage(Snackbar.sHandler.obtainMessage(0, Snackbar.this));
}
public void dismiss(int event) {
Snackbar.sHandler.sendMessage(Snackbar.sHandler.obtainMessage(1, event, 0, Snackbar.this));
}
};
可以看到在实例化中,实现了俩个方法,而在显示的时候仅仅发了一个消息,一起来看看。
private static final Handler sHandler = new Handler(Looper.getMainLooper(), new android.os.Handler.Callback() {
public boolean handleMessage(Message message) {
switch(message.what) {
case 0:
((Snackbar)message.obj).showView(); // 显示
return true;
case 1:
((Snackbar)message.obj).hideView(message.arg1); // 隐藏
return true;
default:
return false;
}
}
});
final void showView() {
if(this.mView.getParent() == null) { // 当前view父容器为null
LayoutParams lp = this.mView.getLayoutParams(); // 获取LayoutParams
if(lp instanceof android.support.design.widget.CoordinatorLayout.LayoutParams) { // 如果包含
Snackbar.Behavior behavior = new Snackbar.Behavior();
// 设置透明度
behavior.setStartAlphaSwipeDistance(0.1F);
behavior.setEndAlphaSwipeDistance(0.6F);
behavior.setSwipeDirection(0);
// 设置监听
behavior.setListener(new OnDismissListener() {
public void onDismiss(View view) {
Snackbar.this.dispatchDismiss(0);
}
public void onDragStateChanged(int state) {
switch(state) {
case 0:
SnackbarManager.getInstance().restoreTimeout(Snackbar.this.mManagerCallback);
break;
case 1:
case 2:
SnackbarManager.getInstance().cancelTimeout(Snackbar.this.mManagerCallback);
}
}
});
((android.support.design.widget.CoordinatorLayout.LayoutParams)lp).setBehavior(behavior);
}
// 不管如何 直接添加到父布局
this.mParent.addView(this.mView);
}
// 如果没显示出来 那么就通过动画加载进来
if(ViewCompat.isLaidOut(this.mView)) {
this.animateViewIn();
} else { // 否则就等待 启动时再次调用动画进入
this.mView.setOnLayoutChangeListener(new Snackbar.SnackbarLayout.OnLayoutChangeListener() {
public void onLayoutChange(View view, int left, int top, int right, int bottom) {
Snackbar.this.animateViewIn();
Snackbar.this.mView.setOnLayoutChangeListener((Snackbar.SnackbarLayout.OnLayoutChangeListener)null);
}
});
}
}
而有关动画,谷歌大拿也是废了脑细胞。
private void animateViewIn() {
if(VERSION.SDK_INT >= 14) { // 校验当前系统版本 因为属性动画
ViewCompat.setTranslationY(this.mView, (float)this.mView.getHeight());
ViewCompat.animate(this.mView).translationY(0.0F).setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR).setDuration(250L).setListener(new ViewPropertyAnimatorListenerAdapter() {
public void onAnimationStart(View view) {
Snackbar.this.mView.animateChildrenIn(70, 180);
}
public void onAnimationEnd(View view) {
if(Snackbar.this.mCallback != null) {
Snackbar.this.mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(Snackbar.this.mManagerCallback);
}
}).start();
} else {
// 如果小于14,直接加载动画
Animation anim = android.view.animation.AnimationUtils.loadAnimation(this.mView.getContext(), anim.design_snackbar_in);
anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
anim.setDuration(250L);
anim.setAnimationListener(new AnimationListener() {
public void onAnimationEnd(Animation animation) {
if(Snackbar.this.mCallback != null) {
Snackbar.this.mCallback.onShown(Snackbar.this);
}
SnackbarManager.getInstance().onShown(Snackbar.this.mManagerCallback);
}
public void onAnimationStart(Animation animation) {
}
public void onAnimationRepeat(Animation animation) {
}
});
this.mView.startAnimation(anim);
}
}
看完之后只觉得相当Nice~!以后我们做这个的时候 也需要多加考虑,虽说目前市面主流差不多适配4.0就好,但是个别机型上还是会出现各种奇异问题。
同样在出来也就是消失的时候,也会有相应的动画呈现,具体大家可以参考文档进行参考学习了解即可。
接下来我们来看下有关其引用布局,看看能get什么技能。
<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" />
<!-- From: file:/usr/local/google/buildbot/repo_clients/https___googleplex-android.googlesource.com_a_platform_manifest.git/mnc-release/frameworks/support/design/res/layout/design_layout_snackbar.xml -->
这里可以了解到有关使用自定义View的俩种方法,如下:
直接引用,当然此方法需要全包名+自定义View名;
通过父节点为View,设置其class同样也可以。
在这里,我们还能学习到如何引用内部类,便是通过如下方式:
class="android.support.design.widget.Snackbar$SnackbarLayout"
这里大家如果看到LZ上一篇写的博文,可能会有疑问,LZ是如何知道它的id呢?很easy,下面直接贴出:
<?xml version="1.0" encoding="utf-8"?>
<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"/>
<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><!-- From: file:/usr/local/google/buildbot/repo_clients/https___googleplex-android.googlesource.com_a_platform_manifest.git/mnc-release/frameworks/support/design/res/layout/design_layout_snackbar_include.xml -->
而关于内部自定义View,简单了解下即可:
public static class SnackbarLayout extends LinearLayout
LinearLayout,控制content以及按钮。GG了
总结
1) 首先,明确SnackBar是与SnackBarManager配合使用;
2) 其次,内部采用Hanlder进行消息分发;
3) 随后,初始化时不断查找父布局,知道找到为止,由此可见,SnackBar外部依赖于CoordinatorLayout,而我们实际的布局的最终父节点是FrameLayout。查找完毕后通过一些操作直接添加布局;
4) 虽说布局写死了,但是我们能通过getView去设置其写死内部样式等等你想要的效果;
5) 通过同步锁,保证同一时间内只能有一个进行操作,通过了handler分发机制以及栈还有弱引用,使我们的View更加人性化。
当然,也不仅仅如上几点,具体大家还可以自行发掘~
End
真正孤身一人了。。。MMP
偌大的屋子 空无一人