> 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 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 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的俩种方法,如下:
直接引用,当然此方法需要全包名+自定义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
偌大的屋子 空无一人