项目地址:https://github.com/JoanZapata/base-adapter-helper
1. 功能介绍
1.1. base-adapter-helper
base-adapter-helper 是对传统的 BaseAdapter ViewHolder 模式的一个封装。主要功能就是简化我们书写 AbsListView 的 Adapter 的代码,如 ListView,GridView。
1.2 基本使用
mListView.setAdapter(mAdapter = new QuickAdapter<Bean>(MainActivity.this, R.layout.item_list, mDatas) {
@Override
protected void convert(BaseAdapterHelper helper, Bean item) {
helper.setText(R.id.tv_title, item.getTitle());
helper.setImageUrl(R.id.id_icon, item.getUrl());
helper.setText(R.id.tv_describe, item.getDesc());
helper.setText(R.id.tv_phone, item.getPhone());
helper.setText(R.id.tv_time, item.getTime());
}
});
1.3 长处
(1) 提供 QucikAdapter,省去类似 getCount() 等抽象函数的书写,仅仅需关注 Model 到 View 的显示。
(2) BaseAdapterHelper 中封装了大量用于为 View 操作的辅助方法,比如从网络载入图片:helper.setImageUrl(R.id.iv_photo, item.getPhotoUrl());
1.4 缺点
(1) 与 Picasso 耦合,想替换为其它图片缓存须要改动源代码。
可通过接口方式。供三方依据自己的图片缓存库实现图片获取,或者直接去掉helper.setImageUrl(…)
函数。
(2) 与内部加入的进度条偶尔,导致不支持多种类型布局
在本文最后给出不修改进度条的解决方法。更好的实现方式应该是通过接口方式暴露,供三方自己设置。
(3) 眼下的方案也不支持HeaderViewListAdapter
。
整体来说这个库比較简单,实现也有待改进。
2. 整体设计
因为 base-adapter-helper 本质上仍然是 ViewHolder 模式,以下各自是 base-adapter-helper 的整体设计图和 ViewHolder 模式的设计图,通过两图的比較。能够看出 base-adapter-helper 对传统的BaseAdapter
进行了初步的实现(QuickAdapter
),而且其子类仅需实现convert(…)
方法,在convert(…)
中能够拿到BaseAdapterHelper
,BaseAdapterHelper
就相当于ViewHolder
。但其内部提供了大量的辅助方法。用于设置
View 上的数据及事件等。
base-adapter-helpr
ViewHolder Pattern
3. 具体设计
3.1 类关系图
这是 base-adapter-helper 库的主要类关系图。
(1) 在 BaseQucikAdapter 中实现了 BaseAdapter 中通用的抽象方法。
(2) BaseQuickAdapter 中两个泛型,当中 T 表示数据实体类(Bean)类型,H 表示 BaseAdapterHelper 或其子类。
(3) QucikAdapter 继承自 BaseQuickAdapter,而且传入 BaseAdapterHelper 作为 H 泛型;
(4) EnhancedQuickAdapter 主要为convert(…)
方法加入了一个 itemChanged 參数。表示 item 相应数据是否发生变化;
(5) BaseAdapterHelper 为用于获取 View 并进行内容、事件设置等相关操作的辅助类。当中多数用于设置的方法都採用链式编程,方便书写。
(6) 能够依据自己须要继承 BaseAdapterHelper 来扩展,做为 BaseQuickAdapter 子类的 H 泛型。
3.2 核心类源代码分析
3.2.1 BaseQucikAdapter.java
该类继承自 BaseAdapter。完毕 BaseAdapter 中部分通用抽象方法的实现,类似ArrayAdapter
。
该类声明了两个泛型,当中 T 表示数据实体类(Bean)类型。H 表示 BaseAdapterHelper 或其子类,主要在扩展BaseAdapterHelper
时使用。
(1) 构造方法
public BaseQuickAdapter(Context context, int layoutResId) {
this(context, layoutResId, null);
}
public BaseQuickAdapter(Context context, int layoutResId, List<T> data) {
this.data = data == null ?
new ArrayList<T>() : new ArrayList<T>(data);
this.context = context;
this.layoutResId = layoutResId;
}
Adapter 的必须元素 ItemView 的布局文件通过 layoutResId 指定,待展示数据通过 data 指定。
(2) 已经实现的主要方法
@Override
public int getCount() {
int extra = displayIndeterminateProgress ? 1 : 0;
return data.size() + extra;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int position) {
return position >= data.size() ? 1 : 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (getItemViewType(position) == 0) {
final H helper = getAdapterHelper(position, convertView, parent);
T item = getItem(position);
helper.setAssociatedObject(item);
convert(helper, item);
return helper.getView();
}
return createIndeterminateProgressView(convertView, parent);
}
上面列出了 BaseQucikAdapter 中已经实现的主要方法,跟一般 BaseAdapter 类似,我们重点看下面几个点:
a. 重写了getViewTypeCount()
和getItemViewType()
,这里 type 为 2,通过getView(…)
能够看出,主要是为了在 AbsListView 最后显示一个进度条。这里也暴露了一个弊端,无法支持多种 Item 样式的布局;
b. getView(…)
方法的实现中首先通过抽象函数getAdapterHelper(…)
得到 BaseAdapterHelper 及 item,然后通过抽象函数convert(…)
实现 View 和 数据的绑定。
这样BaseQucikAdapter
子类仅仅须要实现抽象函数getAdapterHelper(…)
和convert(…)
就可以。
(3) 待实现的抽象方法
protected abstract void convert(H helper, T item);
protected abstract H getAdapterHelper(int position, View convertView, ViewGroup parent);
a. convert(H helper, T item)
通过helper
将 View 和 数据绑定。helper
參数表示 BaseQuickAdapter 或其子类。用于获取 View 并进行内容、事件设置等相关操作,由getAdapterHelper(…)
函数返回;item
表示相应的数据。
b. getAdapterHelper(int position, View convertView, ViewGroup parent)
返回 BaseQuickAdapter 或其子类,绑定 item,然后返回值传递给上面的convert(…)
函数。
关于getAdapterHelper(…)
的实现见以下QuickAdapter
的介绍。
3.2.2 QucikAdapter.java
这个类继承自BaseQuickAdapter
,没什么代码,主要用于提供一个可高速使用的 Adapter。
对于getAdapterHelper(…)
函数直接返回了BaseAdapterHelper
。普通情况下直接用此类作为 Adapter 就可以,如1.2 基本使用
的演示样例。
但假设你扩展了BaseAdapterHelper
,重写getAdapterHelper(…)
函数将其返回。就可以实现自己的 Adapter。
3.2.3 EnhancedQuickAdapter.java
继承自QuickAdapter
,不过为convert(…)
加入了一个參数itemChanged
。表示 item 相应数据是否发生变化。
@Override
protected final void convert(BaseAdapterHelper helper, T item) {
boolean itemChanged = helper.associatedObject == null || !helper.associatedObject.equals(item);
helper.associatedObject = item;
convert(helper, item, itemChanged);
}
protected abstract void convert(BaseAdapterHelper helper, T item, boolean itemChanged);
能够看到它的实现是通过helper.associatedObject
的equals()
方法推断数据是否发生变化,associatedObject 即我们的 bean。
在BaseQuickAdapter.getView(…)
能够看到其赋值的代码。
3.2.4 BaseAdapterHelper.java
可用于获取 View 并进行内容设置等相关操作的辅助类,该类的功能有:
(1) 充当了 ViewHolder 角色,KV 形式保存 convertView 中子 View 的 id 及其引用,方便查找。
和 convertView 通过 tag 关联;
(2) 提供了一堆辅助方法,用于为子 View 设置内容、样式、事件等。
(1) 构造相关方法
protected BaseAdapterHelper(Context context, ViewGroup parent, int layoutId, int position) {
this.context = context;
this.position = position;
this.views = new SparseArray<View>();
convertView = LayoutInflater.from(context) //
.inflate(layoutId, parent, false);
convertView.setTag(this);
}
public static BaseAdapterHelper get(Context context, View convertView, ViewGroup parent, int layoutId) {
return get(context, convertView, parent, layoutId, -1);
}
/** This method is package private and should only be used by QuickAdapter. */
static BaseAdapterHelper get(Context context, View convertView, ViewGroup parent, int layoutId, int position) {
if (convertView == null) {
return new BaseAdapterHelper(context, parent, layoutId, position);
}
// Retrieve the existing helper and update its position
BaseAdapterHelper existingHelper = (BaseAdapterHelper) convertView.getTag();
existingHelper.position = position;
return existingHelper;
}
在QuickAdapter
中,通过上面的 5 个參数的静态函数get(…)
得到BaseAdapterHelper
的实例。
4 个參数的get(…)
方法,仅仅是将 position 默认传入了 -1。即不关注 postion 方法。
这里能够对照下我们平时在getView
中编写的 ViewHolder 模式的代码。在一般的 ViewHolder 模式中,先推断convertView
是否为空:
a. 假设是,则通过LayoutInflater
inflate 一个布局文件。然后新建 ViewHolder 存储布局中各个子 View,通过 tag 绑定该 ViewHolder 到convertView
,返回我们的convertView
;
b. 否则直接得到 tag 中的 ViewHolder。
结合BaseQuickAdapter
的getView(…)
代码。看下 base-adapter-helper 的实现。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (getItemViewType(position) == 0) {
final H helper = getAdapterHelper(position, convertView, parent);
T item = getItem(position);
helper.setAssociatedObject(item);
convert(helper, item);
return helper.getView();
}
return createIndeterminateProgressView(convertView, parent);
}
先利用getAdapterHelper(…)
得到BaseAdapterHelper
或其子类,对于QuickAdapter
而言,这个函数直接调用上面BaseAdapterHelper
的get(…)
函数。
我们能够看到相同是先推断convertView
是否为空。以确定是否须要新建BaseAdapterHelper
,否则从 tag
中获取更新 position 后重用。
在构造方法中 inflate 了一个布局作为convertView
。而且保存 context 及 postion,将convertView
与BaseAdapterHelper
通过tag
关联。
(2) 几个重要的方法
普通情况下,在我们重写BaseQuickAdapter
的convert(…)
时,须要得到 View。这时我们能够通过其入參BaseAdapterHelper
的getView(int viewId)
得到该View
。代码例如以下:
public <T extends View> T getView(int viewId) {
return retrieveView(viewId);
}
@SuppressWarnings("unchecked")
protected <T extends View> T retrieveView(int viewId) {
View view = views.get(viewId);
if (view == null) {
view = convertView.findViewById(viewId);
views.put(viewId, view);
}
return (T) view;
}
通过 viewId 去 views 中进行寻找,找到则返回,找不到则加入并返回。
views 是一个 SparseArray。key为 view id,value 为 view。缓存已经查找到的子 view。
每一个convertView
与一个BaseAdapterHelper
绑定。每一个BaseAdapterHelper
中包括一个views
属性。views
中存储convertView
的子 View 的引用。
(3) 辅助方法
普通情况下,通过getView(int viewId)
拿到该View
,然后进行赋值就能够了。
可是此库考虑:既然是拿到 View 然后赋值。不如直接提供一些赋值的辅助方法。于是产生了一堆类似setText(int viewId, String value)
的代码,内部首先通过 viewId 找到该 View,转为TextView
然后调用setText(value)
。部分代码例如以下:
public BaseAdapterHelper setText(int viewId, String value) {
TextView view = retrieveView(viewId);
view.setText(value);
return this;
}
public BaseAdapterHelper setImageResource(int viewId, int imageResId) {…}
public BaseAdapterHelper setBackgroundRes(int viewId, int backgroundRes) {…}
public BaseAdapterHelper setTextColorRes(int viewId, int textColorRes) {…}
public BaseAdapterHelper setImageDrawable(int viewId, Drawable drawable) {…}
public BaseAdapterHelper setImageUrl(int viewId, String imageUrl) {…}
public BaseAdapterHelper setImageBitmap(int viewId, Bitmap bitmap) {…}
@SuppressLint("NewApi")
public BaseAdapterHelper setAlpha(int viewId, float value) {…}
public BaseAdapterHelper setVisible(int viewId, boolean visible) {…}
public BaseAdapterHelper linkify(int viewId) {…}
public BaseAdapterHelper setProgress(int viewId, int progress, int max) {…}
public BaseAdapterHelper setRating(int viewId, float rating, int max) {…}
public BaseAdapterHelper setTag(int viewId, int key, Object tag) {…}
public BaseAdapterHelper setChecked(int viewId, boolean checked) {…}
public BaseAdapterHelper setAdapter(int viewId, Adapter adapter) {…}
……
实现都是依据 viewId 找到 View。然后为 View 赋值的代码。
这里仅仅要注意下:setImageUrl(int viewId, String imageUrl)
这种方法,默认是通过Picasso
去载入图片的,当然你能够更改成你项目中使用的图片载入框架 Volley,UIL 等,假设不希望继续耦合,可參考1.4 缺点
的建议改法。
也能够为子 View 去设置一个事件监听,部分代码例如以下:
public BaseAdapterHelper setOnClickListener(int viewId, View.OnClickListener listener) {
View view = retrieveView(viewId);
view.setOnClickListener(listener);
return this;
}
public BaseAdapterHelper setOnTouchListener(int viewId, View.OnTouchListener listener) {…}
public BaseAdapterHelper setOnLongClickListener(int viewId, View.OnLongClickListener listener) {…}
这里只列出一些经常使用的方法,假设有些控件的方法这里没有封装。能够通过BaseAdapterHelper.getView(viewId)
得到控件去操作,或者继承BaseAdapterHelper
实现自己的BaseAdapterHelper
。
4. 杂谈
4.1 耦合严重
(1) 与 Picasso 耦合,想替换为其它图片缓存须要改动源代码
可通过新增接口方式,供三方自己依据自己的图片缓存库实现图片获取,或者直接去掉helper.setImageUrl(…)
函数。
(2) 与内部加入的进度条耦合。导致不支持多种类型布局
在以下给出不修改进度条的解决方法。更好的实现方式应该是通过接口方式暴露,供三方自己设置。
整体来说这个库比較简单。实现也有待改进。
4.2 眼下的方案也不支持HeaderViewListAdapter
4.3 扩展多种 Item 布局
通过3.2.1 BaseQucikAdapter.java
的分析,能够看出 base-adapter-helper 并不支持多种布局 Item 的情况。尽管大多数情况下一个种样式就可以。可是要是让我用这么简单的方式写 Adapter,忽然来个多种布局 Item 的 ListView 又要 按传统的方式去写,这反差就太大了。以下我们介绍。怎样在本库的基础上加入多布局 Item 的支持。
(1) 分析
对于多种布局的 Item,大家都清楚。须要去复写BaseAdapter
的getViewTypeCount()
和getItemViewType()
。而且须要在getView()
里面进行推断并选取不同布局文件。不同的布局也须要採用不同的ViewHolder
。
我们能够在构造QucikAdapter
时。去设置getViewTypeCount()
和getItemViewType()
的值,进一步将其抽象为一个接口。提供几个方法,假设须要使用多种 Item 布局,进行设置就可以。
(2) 扩展
-
加入接口
MultiItemTypeSupport
public interface MultiItemTypeSupport<T> { int getLayoutId(int position, T t); int getViewTypeCount(); int getItemViewType(int postion, T t);
} 分别在
QuickAdapter
和BaseQuickAdapter
中加入新的构造函数
BaseQuickAdapter
新增构造函数例如以下:
protected MultiItemTypeSupport<T> multiItemSupport;
public BaseQuickAdapter(Context context, ArrayList<T> data,
MultiItemTypeSupport<T> multiItemSupport) {
this.multiItemSupport = multiItemSupport;
this.data = data == null ? new ArrayList<T>() : new ArrayList<T>(data);
this.context = context;
}
QuickAdapter
新增构造函数例如以下:
public QuickAdapter(Context context, ArrayList<T> data,
MultiItemTypeSupport<T> multiItemSupport) {
super(context, data, multiItemSupport);
}
同一时候肯定须要改写BaseQuickAdapter
的getViewTypeCount()
和getItemViewType()
以及getView()
函数。
@Override
public int getViewTypeCount() {
return multiItemSupport != null ? (mMultiItemSupport.getViewTypeCount() + 1) : 2);
}
@Override
public int getItemViewType(int position) {
if (position >= data.size()) {
return 0;
}
return (mMultiItemSupport != null) ?
mMultiItemSupport.getItemViewType(position, data.get(position)) : 1;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (getItemViewType(position) == 0) {
return createIndeterminateProgressView(convertView, parent);
}
final H helper = getAdapterHelper(position, convertView, parent);
T item = getItem(position);
helper.setAssociatedObject(item);
convert(helper, item);
return helper.getView();
}
为了保留其原本提供的加入滚动栏的功能,我们在其基础上进行改动。
- 改写
BaseAdapterHelper
的构造方法
由于我们不同的布局,肯定要相应不同的ViewHolder
,这里BaseAdapterHelper
事实上就扮演了ViewHolder
的角色。
我们的BaseAdapterHelper
是在QuickAdapter
的getAdapterHelper
中构造的。改动后代码:
QuickAdapter
protected BaseAdapterHelper getAdapterHelper(int position,
View convertView, ViewGroup parent) {
if (mMultiItemSupport != null){
return get(
context,
convertView,
parent,
mMultiItemSupport.getLayoutId(position, data.get(position)),
position);
} else {
return get(context, convertView, parent, layoutResId, position);
}
}
BaseAdapterHelper
的get
方法也须要改动。
/** This method is package private and should only be used by QuickAdapter. */
static BaseAdapterHelper get(Context context, View convertView,
ViewGroup parent, int layoutId, int position) {
if (convertView == null) {
return new BaseAdapterHelper(context, parent, layoutId, position);
}
// Retrieve the existing helper and update its position
BaseAdapterHelper existingHelper = (BaseAdapterHelper)convertView
.getTag();
if (existingHelper.layoutId != layoutId) {
return new BaseAdapterHelper(context, parent, layoutId, position);
}
existingHelper.position = position;
return existingHelper;
}
我们在 helper 中存储了当前的 layoutId。假设 layoutId 不一致。则又一次创建。
(3) 測试
以下展示核心代码
mListView = (ListView) findViewById(R.id.id_lv_main);
MultiItemTypeSupport<ChatMessage> multiItemTypeSupport = new MultiItemTypeSupport<ChatMessage>() {
@Override
public int getLayoutId(int position, ChatMessage msg) {
return msg.isComMeg() ? R.layout.main_chat_from_msg : R.layout.main_chat_send_msg;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public int getItemViewType(int postion, ChatMessage msg) {
return msg.isComMeg() ?
ChatMessage.RECIEVE_MSG : ChatMessage.SEND_MSG;
}
};
initDatas();
mAdapter = new QuickAdapter<ChatMessage>(ChatActivity.this, mDatas,
multiItemTypeSupport) {
@Override
protected void convert(BaseAdapterHelper helper, ChatMessage item) {
switch (helper.layoutId) {
case R.layout.main_chat_from_msg:
helper.setText(R.id.chat_from_content, item.getContent());
helper.setText(R.id.chat_from_name, item.getName());
helper.setImageResource(R.id.chat_from_icon, item.getIcon());
break;
case R.layout.main_chat_send_msg:
helper.setText(R.id.chat_send_content, item.getContent());
helper.setText(R.id.chat_send_name, item.getName());
helper.setImageResource(R.id.chat_send_icon, item.getIcon());
break;
}
}
};
mListView.setAdapter(mAdapter);
当遇到多种布局 Item 的时候,首先构造一个MultiItemTypeSupport
接口对象,然后在convert
中依据 layoutId。获取不同的布局进行设置。
贴张效果图: