名字有点唬人,其实就是组合了几个封装类可以方便实现RecyclerView
的多视图,毕竟“框架”这个词在我看来仍是指具备必定规模量级及重点技术的代码体系,但仅就解决特定问题而言也不妨被冠以这个名号。同时它真的是“超轻量”总共不过4个类,不超过130行代码~java
咱们已经有了一个无需类型强转的通用ViewHolder(ItemViewHolder),一个ViewHolder对象能够找到全部视图实例。并且它是彻底独立的, 没有引入任何自定义类或者任何第三方依赖;即便没有这个“框架”,也彻底能够拆出来用在其余地方。android
适配器(Adapter)是与控件关联的, 是控件对其子视图列表的一种抽象。抽象了什么?由具体定义决定。好比列表控件的适配器(不管是之前的ListView
, 如今RecyclerView
, 以及其它的如ViewPager
)通常抽象了三个属性:git
控件适配是SDK关联的,框架的ItemAdapter
也是基于RecyclerView.Adapter
github
适配器(Adapter) 是容器控件对子控件的总体抽象,相应位置的元素没有做出任何限制,position
对应的元素能够是接口返回的一个具体数据,也能够是从本地获取的应用数据。框架要作的一个工做就是对元素数据类型进行抽象, 可是数据类型千差万别,没法对数据元素自己的属性作统一操做,结果就是变成像MultiType
库那样,用范型抽象全部的数据元素,而后经过注册数据类型(.class
)到数据绑定器类型(ItemViewBinder.class
)的映射,反射获得绑定器实例,其中有大量的对象类型强转。微信
框架不对数据元素作抽象,而是针对操做做抽象,即adapter对每一个position
元素的操做做抽象;用一个简单的List
数据结构持有抽象实例;由于一样有绑定操做,因此姑且也叫作绑定器ItemBinder
。ViewHolder就用咱们以前的通用ViewHolder(ItemViewHolder
),结合前面说到adapter有三个重要属性,因而有:数据结构
public interface ItemBinder {
void onBindViewHolder(ItemViewHolder holder, int position);
int getViewType();
}
public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
private final List<ItemBinder> mBinders = new ArrayList<>(10);
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
ItemBinder binder = mBinders.get(position);
binder.onBindViewHolder(holder, position);
}
@Override
public int getItemCount() {
return mBinders.size();
}
@Override
public int getItemViewType(int position) {
return mBinders.get(position).getViewType();
}
public void setBinders(List<ItemBinder> binders) {
mBinders.clear();
mBinders.addAll(binders);
}
}
复制代码
对于Adapter而言,元素仅仅是ItemBinder
,它不关心ItemBinder
是用哪一种数据类型,又是怎样把数据填充到ViewHolder中。app
RecyclerView
经过RecyclerView.Adapter
的getItemViewType
接口返回的数值来标识一个视图类型。与ListView
不一样的是这个viewType能够不是连续的,RecyclerView
能够本身感知设置了多少种viewType
(内部其实就是用了SparseArray
)。经过viewType
的标识, RecyclerView.Adapter
的onCreateViewHolder
来建立相应的视图类型。一般咱们不得不本身创建viewType
和RecyclerView.ViewHolder
的映射关系,除了稍有点烦琐以外并无多大的问题。框架
注意:咱们走到了到框架的一个关键点,就是创建viewType
和视图实例建立之间的关系。ide
已经找不到是在哪一个库里,当看到把视图资源id(layoutId)直接做为viewType
返回的时候,被这种天才想法折服了。首先就是用资源id自己就能够建立视图;其次是充分利用了viewType
能够不连续的性质;再次是不一样的资源id自然的对应不一样的视图类型,也就是说,自己就是多视图类型的;最后的最后就是这种实现提供了巨大的灵活性,包括代码复用和资源的复用,这点后面专门说一下。因而有:函数
public interface ItemBinder {
void onBindViewHolder(ItemViewHolder holder, int position);
@LayoutRes
int getLayoutId();
}
public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
private final List<ItemBinder> mBinders = new ArrayList<>(10);
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) {
return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate(
viewType, container, false));
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
ItemBinder binder = mBinders.get(position);
binder.onBindViewHolder(holder, position);
}
@Override
public int getItemCount() {
return mBinders.size();
}
@Override
public int getItemViewType(int position) {
return mBinders.get(position).getLayoutId();
}
public void setBinders(List<ItemBinder> binders) {
mBinders.clear();
mBinders.addAll(binders);
}
}
复制代码
咱们以前被getItemViewType
的默认值0给误导了,思惟惯性让咱们认为viewType
能够和ViewHolder
是割裂的,但其实它们能够是统一的!
剩下的工做简单明了,实现具体的ItemBinder
类型,将具体的数据填充到视图,好比:
public HomeBannerBinder implements ItemBinder {
private final HomeBanner mItem;
HomeBannerBinder(HomeBanner banner) {
mItem = banner;
}
void onBinderViewHolder(ItemViewHolder holder, int position) {
ImageView bg = holder.findViewById(R.id.background);
if (bg != null) {
ImageManager.load(bg, mItem.bg_url);
}
}
}
复制代码
这里的复用不是recyclerView
对视图内存对象的复用,而是代码层面的复用,包括声明资源的xml代码。
把layoutId做为viewType
到底带来怎样的灵活复用呢?
能够先举例常见的微信朋友圈列表:显然,不少朋友圈内容都是不一样的,有视频有图片有文本,或者它们的结合,处理2张图片的布局和处理9张图片的布局显示也是不一样的;可是每一条朋友圈布局有不少相同的地方:都有顶部的用户头像与用户名称,都有底部点赞和评论布局。那么问题来了:怎样声明不一样的视图类型,但没必要重复书写这些同样的地方?
这固然不是难事,好比一个视频朋友圈布局可写成这样circle_item_video.xml
<RelativeLayout>
<include layout="@layout/circle_item_top" />
<include layout="@layout/circle_item_layer_video" />
<include layout="@layout/circle_item_bottom" />
</RelativeLayout>
复制代码
音频朋友圈布局circle_item_audio.xml
就把@layout/circle_item_layer_video
换成@layout/circle_item_layer_audio
,依次类推。
这么作彻底能够实现,随着类型的增多,布局文件相应增长便可;然而一旦发生变动呢?只要涉及相同布局的部分都必须改一遍!(好比把RelativeLayout
变成android.support.constraint.ConstraintLayout
)并且实际的状况不必定这么简单,可能由于各类缘由视图的层次比较深,而且都没办法放在include中,一旦视图对象变多,视图层次变深, 这种冗余就让人难以忍受了,对一个有追求的码畜来讲,确定但愿只更改一处地方便可。
若是layoutId做为viewType要如何实现刚才的复用呢?显然他们必须是不一样的viewType
(若是同样会发生什么?),那么他们固然是不一样的layoutId,但不一样的layoutId就没法避免上面那样的问题,这时候就用到android的匿名资源(anonymous),就是对一个资源声明一个引用,而这个引用自己做为一个资源,即<item name="def" type="drawable">@drawable/abc</item>
,结合以上的例子就是 circle_item.xml
:
<RelativeLayout>
<include layout="@layout/circle_item_top" />
<ViewStub />
<include layout="@layout/circle_item_bottom" />
</RelativeLayout>
复制代码
中间的部分可经过延迟加载的方式设置成不一样的View,甚至全部不一样的部分均可以以ViewStub
的形式嵌在布局当中。 refs.xml
:
<resources>
<item name="circle_item_video" type="layout">@layout/circle_item</item>
<item name="circle_item_audio" type="layout">@layout/circle_item</item>
<item name="circle_item_pic_1" type="layout">@layout/circle_item</item>
<item name="circle_item_pic_9" type="layout">@layout/circle_item</item>
</resources>
复制代码
也就是说都引用同一份的布局资源!可他们由于不一样的layoutId
进而能够被recyclerView
看成不一样的viewType
!
按照以前的思路也必然但愿只在一处更改点赞和评论功能。因此有一个基类:
public class CircleItemBinder implements ItemBinder {
@Override
public getLayoutId() {
return R.layout.circle_item;
}
@Override
void onBindViewHolder(ItemViewHolder holder, int position) {
bindComment(holder);
bindLike(holder);
}
private void bindComment(ItemViewHolder holder) {
}
private void bindLike(ItemViewHolder holder) {
}
}
复制代码
各种型的binder相似:
public class CircleVideoBinder extends CircleItemBinder {
private final YourVideoData mItem;
public CircleVideoBinder(YourVideoData data) {
mItem = data;
}
@Override
public getLayoutId() {
return R.layout.circle_item_video;
}
@Override
void onBindViewHolder(ItemViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
TextView title = holder.findViewById(R.id.video_title);
if (title != null) {
title.setText(mItem.title);
}
...
}
}
public class CircleAudioBinder extends CircleItemBinder {
private final YourAudioData mItem;
public CircleAudioBinder(YourAudioData data) {
mItem = data;
}
@Override
public getLayoutId() {
return R.layout.circle_item_audio;
}
@Override
void onBindViewHolder(ItemViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
ImageView album = holder.findViewById(R.id.audio_album);
if (album != null) {
ImageLoader.load(album, mItem.album_background);
}
...
}
}
复制代码
点赞和评论功能的代码就可彻底复用!这一切只是用了layoutId做为了viewType! 至此,框架的全貌已呈现:
public interface ItemBinder {
@LayoutRes
int getLayoutId();
void onBindViewHolder(ItemViewHolder holder, int position);
}
public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
private final List<ItemBinder> mBinders = new ArrayList<>(10);
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) {
return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate(
viewType, container, false));
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
ItemBinder binder = mBinders.get(position);
binder.onBindViewHolder(holder, position);
}
@Override
public int getItemCount() {
return mBinders.size();
}
@Override
public int getItemViewType(int position) {
return mBinders.get(position).getLayoutId();
}
public void setBinders(List<ItemBinder> binders) {
mBinders.clear();
appendBinders(binders);
}
}
复制代码
咱们以前的通用ViewHolder也罗列在这里:
public class ItemViewHolder extends RecyclerView.ViewHolder {
private final SparseArrayCompat<View> mCached = new SparseArrayCompat<>(10);
public ItemViewHolder(View itemView) {
super(itemView);
}
public <T extends View> T findViewById(@IdRes int resId) {
int pos = mCached.indexOfKey(resId);
View v;
if (pos < 0) {
v = itemView.findViewById(resId);
mCached.put(resId, v);
} else {
v = mCached.valueAt(pos);
}
@SuppressWarnings("unchecked")
T t = (T) v;
return t;
}
}
复制代码
通常都还要定义一个基础类ItemBaseBinder
,全部派生类的可能会共享某个操做, 这个基础类接收资源id做为构造函数参数:
public class ItemBaseBinder implements ItemBinder {
private final int mLayoutId;
public ItemBaseBinder(@layoutRes int layoutId) {
mLayoutId = layoutId;
}
@Override
public void onBindViewHolder(ItemViewHolder holder, int position) {
}
@Override
public int getLayoutId() {
return mLayoutId;
}
}
复制代码
其他的工做就只是派生具体的业务类了,就像以前举例那样!这一切不过130行代码!
MutiType
的差别MutiType
库一样有绑定器ItemViewBinder
但注意他的绑定是只有一个实例,而咱们的ItemAdapter
是把绑定器做为元素对象,一个数据对应一个绑定器因此他有多个实例,实际上这个绑定器是对数据的抽象。
真的,MutiType
把这一切搞的太复杂了!可悲的是还有不少人在用……
有了这个框架,灵活性不只一点没有损失,并且更加简洁,MutiType
那坨类型强转和反射操做能够进博物馆了。
一大篇说下来有点累赘,直接上代码就能看明白的,关键是思考的过程与解决问题的思路。全部的框架到底解决了什么问题,这才是最须要了解和学习的,不然框架是学不完的。而一旦咱们有了思路与目标,实现一个框架也并非难事。这套小框架实践已经很长时间了,能够覆盖绝大多数状况,效果出奇的好,比MutiType
那坨“不知道高到哪里去了”。
须要注意的有2点
onBindViewHolder
方法只作数据填充不该该作数据处理 这点其实和框架没有关系,照样仍是有许多人在Adapter的onBindViewHolder
作着数据处理getLayoutId
是接口,意味着在运行时能够返回不一样的layoutId,从而动态的更改视图类型,不过须要与Adatper的notifyItemChanged
配合使用ItemAdapter.setBinders
的方法实现体在更新了实例后没有调用notifyDataSetChanged
, 这个操做应该由外部决定,虽然此处是必要的,但很容易形成冗余的更新。框架也很是容易根据具体的须要和场景进行扩展。
列表嵌套列表的状况下,要如何抽象呢,其实只要对应视图就行。最外层的列表(一级列表)有一个特殊ItemBinder
类型,这个类型自己也能够持有多个ItemBinder
提供给内层列表(二级列表):
public class ItemContainerBinder extends ItemBaseBinder {
private final ItemAdapter mAdapter = new ItemAdapter();
@Override
public void onBinderViewHolder(ItemViewHolder holder, int position) {
RecyclerView secondary = holder.findViewById(R.id.secondary);
if (secondary != null) {
if (secondary.getAdapter() != mAdapter) {
secondary.setAdapter(mAdapter);
}
if (secondary.getLayoutManager() == null) {
secondary.setLayoutManager(new LinearLayoutManager(secondary.getContext());
}
}
}
public void setBinders(List<ItemBinder> binders) {
mAdapter.setBinders(binders);
}
...
}
复制代码
在这里还能够利用之前提过的重用LayoutManager!
在运行过程当中只须要更新列表某一项的状况其实很是常见,不少时候不能只经过调用视图对象的方法来直接更新视图,还要调用Adapter.notifyItemChanged
(像前文所提的动态更新列表视图类型)。也就是Adapter持有ItemBinder
,而ItemBinder
须要再调用Adapter的方法,若是再让ItemBinder
去引用Adapter
,这种强耦合必然不是一个好的设计。
针对这个框架的实现,这时候首先须要将ItemBinder
内部的变化通知出来,可是通知的时机应该由ItemBinder
实现体来决定,外部去被动响应。这固然是最简单的观察者模式了,因而有:
public interface ItemBinder {
...
void setOnChangeListener(OnChangeListener listener);
interface OnChangeListener {
void onItemChanged(ItemBinder item, int payload);
}
}
public class ItemBaseBinder implements ItemBinder {
...
private OnChangeListener mChangeListener;
@Override
publi final void setChangeListener(OnChangeListener listener) {
mChangeListener = listener;
}
public final void notifyItemChange(int payload) {
if (mChangeListener != null) {
mChangeListener.onItemChanged(this, payload);
}
}
}
复制代码
这里的payload
借鉴了RecyclerView.Adapter
,只不过类型由Object
变成了int
,表明了局部更新须要携带的信息。在ItemBinder
实现体内部,由于某项数据变动须要通知到外部就只需调用notifyItemChange
方法,将变动传递出去,由外部做出具体响应:
List<ItemBinder> binders = new ArrayList<>();
...
ItemBinder special = new XXXYYYBinder(...);
specail.setChangeListener(new ItemBinder.OnChangeListener() {
@Override
public void onItemChanged(ItemBinder item, int payload) {
int pos = mAdapter.indexOf(item);
if (pos >= 0) {
mAdapter.notifyItemChanged(pos,...);
}
}
});
binders.add(special);
...
mAdapter.setBinders(binders);
复制代码