-
概述
LoadSir是一个用来处理页面中不同状态时的不同UI展示的框架,比如一个页面在初始的时候有一个占位布局来改善用户第一次进入页面的视觉体验,在请求接口失败后需要展示一个接口失败的UI来提示并提供按钮重新加载,在未查询到数据时可能会需要另一种UI来提示当前没有数据并提供再次查询的按钮等。
我们可以在每个布局中通过硬编码的方式添加以上这些特殊场景下才会展示的View,但是可想而知,这会有多麻烦,而且你还要手动的控制它显示和隐藏。但是,实际上,基本的原理就是这样处理,那LoadSir框架做的事情就是把能抽出来的逻辑进行封装,减少冗余的模版代码。
-
使用
依赖如下:
api('com.kingja.loadsir:loadsir:1.3.6')
在需要使用的页面中(Activity或者Fragment,甚至是Dialog或者其他有布局的类中)初始化:
LoadSir.getDefault().register()
register方法是重载函数,最终都会调用有三个参数的register方法,它返回一个LoadService对象:
public <T> LoadService register(Object target, OnReloadListener onReloadListener, Convertor<T> convertor) { TargetContext targetContext = LoadSirUtil.getTargetContext(target); return new LoadService(convertor, targetContext, onReloadListener, this.builder); }
target是需要替换的View,这个View是业务场景中正常显示的View,比如一个RecyclerView,如果在RecyclerView加载出数据之前需要一个占位的View或者无数据时显示一个空提示View时,LoadService就会用对应状态下的View来替换RecyclerView。
onReloadListener是暴露出来重新请求数据的一个接口,指定你的网络请求等操作方法,如果指定了的话则会在点击替换后的View时响应这个回调,当然你可以不指定,完全可以通过在替换View中加一个按钮来完成同样的事情。
convertor是一个用于返回一个Callback的接口,可以用它来封装根据不同状态返回不同的Callback的逻辑,也可以指定为空。
LoadService初始化之后就可以根据不同场景用它来显示不同的View了。
-
源码解析
从构造入手很难理解每个东西是用来干嘛的,所以我们从使用着手。
LoadService最直接的方式就是调用showCallback方法,它直接传入一个Callback对象:
public void showCallback(Class<? extends Callback> callback) { this.loadLayout.showCallback(callback); }
loadLayout是LoadLayout对象,它的showCallback方法如下:
public void showCallback(Class<? extends Callback> callback) { this.checkCallbackExist(callback); if (LoadSirUtil.isMainThread()) { this.showCallbackView(callback); } else { this.postToMainThread(callback); } }
这里首先会调用checkCallbackExist方法检查callback存不存在:
private void checkCallbackExist(Class<? extends Callback> callback) { if (!this.callbacks.containsKey(callback)) { throw new IllegalArgumentException(String.format("The Callback (%s) is nonexistent.", callback.getSimpleName())); } }
callbacks是在addCallback方法中添加的,但是我们使用过程中并没有显示地调用过这个方法,那么这里检查的时候不是就崩溃了嘛,其实,在LoadService的构造方法里构造LoadLayout时调用了一次setupSuccessLayout方法:
LoadService(Convertor<T> convertor, TargetContext targetContext, OnReloadListener onReloadListener, Builder builder) { this.convertor = convertor; Context context = targetContext.getContext(); View oldContent = targetContext.getOldContent(); LayoutParams oldLayoutParams = oldContent.getLayoutParams(); this.loadLayout = new LoadLayout(context, onReloadListener); this.loadLayout.setupSuccessLayout(new SuccessCallback(oldContent, context, onReloadListener)); if (targetContext.getParentView() != null) { targetContext.getParentView().addView(this.loadLayout, targetContext.getChildIndex(), oldLayoutParams); } this.initCallback(builder); }
public void setupSuccessLayout(Callback callback) { this.addCallback(callback); ... ... }
可见,在setupSuccessLayout方法中调用了addCallback方法把SuccessCallback设置进去了,所以默认会有一个SuccessCallback存在的,所以不会崩溃。但是如果要使用showCallback方法显示其他场景View的就需要提前调用LoadLayout的addCallback方法事先把需要的Callback存进去,对此,LoadService提供了getLoadLayout方法获取loadLayout对象。也可以通过构造LoadService时传入的builder批量传入Callback。
再回到showCallback方法中,会有一个是否在主线程的判断,这没什么好讲的,操作UI必须要在主线程,postToMainThread方法中:
private void postToMainThread(final Class<? extends Callback> status) { this.post(new Runnable() { public void run() { LoadLayout.this.showCallbackView(status); } }); }
调用了View的post方法推到主线程中去执行,因为LoadLayout继承自FrameLayout,所以可以调用,但是LoadLayout如果没有加载到界面上的话也没用,所以回到LoadService构造方法中看一下LoadLayout是怎么加到UI树中去的。
我们发现是通过调用tagetContext的父容器的addView方法来添加的,添加的位置就是targetContext的位置,targetContext是什么?这得回到register方法中找。
在register方法中,通过LoadSirUtil.getTargetContext(target)方法创建的TargetContext对象:
public static TargetContext getTargetContext(Object target) { ViewGroup contentParent; Object context; if (target instanceof Activity) { Activity activity = (Activity)target; context = activity; contentParent = (ViewGroup)activity.findViewById(16908290); } else { if (!(target instanceof View)) { throw new IllegalArgumentException("The target must be within Activity, Fragment, View."); } View view = (View)target; contentParent = (ViewGroup)((ViewGroup)view.getParent()); context = view.getContext(); } int childIndex = 0; int childCount = contentParent == null ? 0 : contentParent.getChildCount(); View oldContent; if (target instanceof View) { oldContent = (View)target; for(int i = 0; i < childCount; ++i) { if (contentParent.getChildAt(i) == oldContent) { childIndex = i; break; } } } else { oldContent = contentParent != null ? contentParent.getChildAt(0) : null; } if (oldContent == null) { throw new IllegalArgumentException(String.format("enexpected error when register LoadSir in %s", target.getClass().getSimpleName())); } else { if (contentParent != null) { contentParent.removeView(oldContent); } return new TargetContext((Context)context, contentParent, oldContent, childIndex); } }
这个方法相比起来稍微有点长,但是不复杂,我们来分析一下:
首先,如果target是Activity的话,会直接按照id值找到它的父容器,我为什么这么确定呢?因为在PhoneWindow的generateLayout方法中有这么一句:
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
ID_ANDROID_CONTENT的值就是16908290,所以这实际上取的就是Activity的根布局。
其他的就是获取target的上一级父容器,oldContent保存的就是target,childIndex保存的是target位于其父容器的位置,这里还会调用removeView方法移除原来的target,其实target只是作为SuccessCallback的一部分被传入了LoadLayout中,我们知道之前调用了setupSuccessLayout方法设置了SuccessLayout作为默认的必须存在的Callback,在这个方法中:
public void setupSuccessLayout(Callback callback) { ... ... View successView = callback.getRootView(); successView.setVisibility(8); this.addView(successView); this.curCallback = SuccessCallback.class; }
callback.getRootView()得到的就是前面的target,它被设置了不可见(8是View.GONE的值),然后它被添加到LoadLayout中,成为了LoadLayout的子View。
现在我们知道LoadLayout被加到了UI树中,也就理解了post会起作用。post最终也会走到showCallbackView方法:
private void showCallbackView(Class<? extends Callback> status) { if (this.preCallback != null) { if (this.preCallback == status) { return; } ((Callback)this.callbacks.get(this.preCallback)).onDetach(); } if (this.getChildCount() > 1) { this.removeViewAt(1); } Iterator var2 = this.callbacks.keySet().iterator(); while(var2.hasNext()) { Class key = (Class)var2.next(); if (key == status) { SuccessCallback successCallback = (SuccessCallback)this.callbacks.get(SuccessCallback.class); if (key == SuccessCallback.class) { successCallback.show(); } else { successCallback.showWithCallback(((Callback)this.callbacks.get(key)).getSuccessVisible()); View rootView = ((Callback)this.callbacks.get(key)).getRootView(); this.addView(rootView); ((Callback)this.callbacks.get(key)).onAttach(this.context, rootView); } this.preCallback = status; } } this.curCallback = status; }
这个方法一开始会尝试复用之前已经显示过的callback实例,如果新场景的callback和之前的不一样,则还会回调之前callback的onDetach方法,所以如果有需要的话你可以重写相关场景的Callback的onDetach方法来处理。然后会便利callbacks中的所有Callback,找到对应的Callback来完成对应的方法调用来显示对应UI。需要注意的是,SuccessCallback的显示和其他Callback的显示是不一样的,先来看SuccessCallback的显示。
show方法如下:
public void show() { this.obtainRootView().setVisibility(0); }
obtainRootView获取的是rootView,我们前面知道,SuccessCallback的rootView就是LoadLayout中的target,也就是我们布局代码中的具体View,这里就是给它设置可见即可显示出来(View.VISIBLE的值是0)。
如果是非SuccessCallback的显示是怎么做的呢?
public void showWithCallback(boolean successVisible) { this.obtainRootView().setVisibility(successVisible ? 0 : 4); }
首先会把SuccessCallback中的target设置为View.INVISIBLE(View.INVISIBLE的值是4),因为LoadLayout继承自FrameLayout,所以GONE和INVISIBLE在效果上没有区别,在没有调用setSuccessVisible方法设置过的前提下,这里getSuccessVisible得到的总是默认的false,所以这里就是设置不可见的作用。然后会拿到其他场景Callback的rootView,添加到LoadLayout中,从而达到显示其他场景的效果。
那如果之前添加过其他场景的View之后再显示目标View的话岂不是其他场景的View还存在嘛,所以在showCallbackView方法的while遍历之前会移除之前添加的其他场景的View(如果存在的话):
//目标View总是在0的位置设置是否可见,而其他场景的View都是在1的位置添加/删除 if (this.getChildCount() > 1) { this.removeViewAt(1); }
到现在我们知道它是如何工作的了,那么我们如果要自定义一个新场景的Callback的话,只需要继承Callback类,重写onCreateView方法即可,因为getRootView方法会调用它:
public View getRootView() { int resId = this.onCreateView(); if (resId == 0 && this.rootView != null) { return this.rootView; } else { if (this.onBuildView(this.context) != null) { this.rootView = this.onBuildView(this.context); } if (this.rootView == null) { this.rootView = View.inflate(this.context, this.onCreateView(), (ViewGroup)null); } this.rootView.setOnClickListener(new OnClickListener() { public void onClick(View v) { if (!Callback.this.onReloadEvent(Callback.this.context, Callback.this.rootView)) { if (Callback.this.onReloadListener != null) { Callback.this.onReloadListener.onReload(v); } } } }); this.onViewCreate(this.context, this.rootView); return this.rootView; } }
这里可以看到,前面设置的onReloadListener就是在点击场景View的时候会触发,如果不想要这个效果也可以设置为null或者重写onReloadEvent方法并返回true。
至此,我们就了解完LoadSir的整个原理了。
最后说一下,显示不同场景UI的有两种方式,一个是调用showCallback方法传入对应场景的Callback的class对象。还有一种方式就是在构造LoadService的时候传入一个Convertor,通过它来动态获取不同的Callback。比如:
LoadSir.getDefault().register(rv_list, null, { state: PageState -> val callback: Class<out Callback> = when (state) { CaTripDetailPageState.NO_DAYS -> { EmptyCallback::class.java } CaTripDetailPageState.NET_WRONG -> { ErrorCallback::class.java } CaTripDetailPageState.DATA -> { SuccessCallback::class.java } CaTripDetailPageState.NO_ACTIVITIES -> { SuccessCallback::class.java } CaTripDetailPageState.INIT->{ PlaceholderCallback::class.java } } callback }) as LoadService<PageState>
这里自定义了PageState枚举来表示不同页面场景,是用这种方式需要通过调用showWithConvertor方法来显示不同UI。后一种的优势在于可以更明显地通过自定义状态知道设置的是什么,但是用哪一种都能实现。
-
总结
总的来说,LoadSir是通过把目标View给替换成一个FrameLayout,并把目标View放入FrameLayout中,然后通过控制目标View的可见性、添加或删除其他场景的View共同配合来做不同场景切换的逻辑,也就是说,目标View总是在0的位置设置是否可见,而其他场景的View都是在1的位置添加/删除。
网友评论