单布局
ListView/GridView + Adapter可以说是Android程序员最常用的组件了. 我们应该尽量简化Adapter的代码以提高开发效率. 首先来看看不做任何封装的Adapter需要哪些步骤:
在getView中判断convertView是否为空, 为空则inflate一个布局, 初始化ViewHolder, 初始化控件, 并将ViewHolder通过setTag设置到convertView中.
如果不为空则通过getTag将ViewHolder取出来
可以看到需要做的事情还是很多的. utils包中有一个ViewHolder类, 此类可以代替自己声明的ViewHolder, 并且不用写将ViewHolder与convertView绑定的代码. 控件可以直接通过如下方式声明:
Copy TextView textView = ViewHolder.get(convertView, R.id.textView);
ViewHolder中使用了SparseArray来缓存视图, 内部也是用过给convertView设置tag来达到缓存的目的. 并且通过泛型来避免类型转换. 整体逻辑很简单:
Copy public class ViewHolder {
@SuppressWarnings("unchecked")
public static <T extends View> T get(View view, int id) {
SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
if (viewHolder == null) {
viewHolder = new SparseArray<>();
view.setTag(viewHolder);
}
View childView = viewHolder.get(id);
if (childView == null) {
childView = view.findViewById(id);
viewHolder.put(id, childView);
}
return (T) childView;
}
}
下面的内容详细可以参考 打造万能的ListView GridView 适配器
虽然有了ViewHolder能减少很多代码量, 但是这不是极限, 还可以继续减少. 比如内部维护的List数据, 另外三个抽象方法等都是可以不用写的. 这里我们引用GitHub上的一个开源工具 base adapter helper . 现在来看看如何使用base adapter helper:
Copy public class HomeAdapter extends QuickAdapter<Attraction> {
public HomeAdapter(Context context, int layoutResId) {
super(context, layoutResId);
}
@Override
protected void convert(BaseAdapterHelper helper, Attraction item) {
helper.setText(R.id.tv_name, item.name)
.setText(R.id.tv_des, item.profile)
.setText(R.id.tv_distance,
String.format(context.getResources().getString(R.string.item_home_KM),
Attraction.formatDistance(item.distance)))
}
}
可以调用如下方法修改Adapter的数据
Copy homeAdapter.add(data);
homeAdapter.addAll(list);
homeAdapter.removeAll();
homeAdapter.remove(0);
homeAdapter.set(0, data);
...
base adapter helper的原理在此处就不赘述了, 参考 Android base-adapter-helper 源码分析与扩展 . 使用base adapter helper可以减少大量重复的代码, 也可以通过扩展BaseAdapterHelper以适应自己的编程习惯.
接下来看看base adapter helper的getItemId方法的实现:
Copy @Override
public long getItemId(int position) {
return position;
}
很多时候, getItemId的返回值都是position. 如果我们需要拿到实体类的id, 就必须这么写:
Copy adapter.getItem(position).id;
通常情况下, ListView中的某些控件是需要与网络API交互的, 而基本上网络API需要的只是一条数据的id. 因此我改造了getItemId方法, 在某些情况下, 此方法能直接返回实体类中的id.
Copy /**
* 配合{@link QuickAdapter}使用, 传入{@link QuickAdapter}中的数据类型如果实现此接口,
* 则可以直接调用{@link QuickAdapter#getItemId(int)}来获取数据的id.
*/
public interface IdObject {
long getId();
}
Copy @Override
public long getItemId(int position) {
T data = this.data.get(position);
return data instanceof IdObject ? ((IdObject) data).getId() : position;
}
上述的某些情况就是指T类型实现了IdObject接口. 以上就是对Android适配器的简化及优化. 但是base adapter helper使用虽然简单, 也还是有不足的地方. 比如不支持多布局, 只支持BaseAdapter, 不支持其他类型比如ViewPager的Adapter等.
以上就是单布局的基本原理与使用方法. 单布局是APP中最常用的, 下面介绍不是很常见的多布局的封装.
多布局
多布局是指一个适配器中使用一个以上的布局文件. 主要是依赖于覆写BaseAdapter
中的getViewTypeCount
与getItemViewType
方法. 多布局使用了代理, 将convert方法代理给多个代理类去实现, 有兴趣的可以看这里 , 这里只讲使用方法. 直接上代码:
Copy final QuickMultiAdapter<Integer> adapter = new QuickMultiAdapter<>(this);
adapter.addItemViewDelegate(new LeftDelegate());
adapter.addItemViewDelegate(new RightDelegate());
listView.setAdapter(adapter);
adapter.replaceAll(getData());
Copy class LeftDelegate implements ItemViewDelegate<Integer> {
@Override public int getItemViewLayoutId() {
return R.layout.i_text;
}
@Override public boolean isForViewType(Integer item, int position) {
return item % 2 == 0;
}
@Override public void convert(BaseAdapterHelper helper, Integer item, int position) {
helper.setText(R.id.text, item + "");
}
}
Copy class RightDelegate implements ItemViewDelegate<Integer> {
@Override public int getItemViewLayoutId() {
return R.layout.i_text_right;
}
@Override public boolean isForViewType(Integer item, int position) {
return item % 2 != 0;
}
@Override public void convert(BaseAdapterHelper helper, Integer item, int position) {
helper.setText(R.id.text, item + "");
}
}
效果:
两个布局的差别就是一个TextView的gravity是left, 同时有margin值, 另一个的gravity为right, 没有margin值. 如果是双数则显示gravity是left的TextView, 否则显示gravity是right的TextView. 代码也比较好理解, 接下来看看Delegate接口的声明以及每个方法的含义:
Copy /**
* Adapter中多布局代理
* @param <T> 数据源类型
* @param <H> ViewHolder类型
*/
public interface BaseItemViewDelegate<T, H extends BaseAdapterHelper> {
/** 布局资源id **/
int getItemViewLayoutId();
/** 判断该position是否要加载此类型的布局 **/
boolean isForViewType(T item, int position);
/**
* 当需要条目将被展示到界面上时, 通过此方法适配界面
* @param helper ViewHolder
* @param item 数据
* @param position 位置
*/
void convert(H helper, T item, int position);
}
ItemViewDelegate继承了BaseItemViewDelegate, 同时声明H为BaseAdapterHelper, 所以在不需要自定义AdapterHelper的情况下, 建议直接使用ItemViewDelegate:
Copy /**
* ViewHolder类型为BaseAdapterHelper的快捷代理接口
*/
public interface ItemViewDelegate<T> extends BaseItemViewDelegate<T, BaseAdapterHelper> {}
RecyclerView
作为目前超级灵活超级NB的杀手级控件 - RecyclerView, 怎么能少了它呢? 直接上代码:
Copy final RecyclerAdapter<Integer> adapter = new RecyclerAdapter<Integer>(this, R.layout.i_text) {
@Override protected void convert(BaseAdapterHelper helper, Integer item, int position) {
helper.setText(R.id.text, item + "");
}
};
使用方法与ListView/GridView一样, 只不过继承从QuickAdapter
变成RecyclerAdapter
. 多布局也一样, 区别只是从QuickMultiAdapter
换成RecyclerMultiAdapter
. 下面看一个多布局的例子:
首先是效果:
示例中有两种布局, 一种是日期, 一种是课程详情. 日期占满一行, 详情占半行. XML以及实体省略:
Copy RecyclerMultiAdapter<Playback> adapter = new RecyclerMultiAdapter<>(getActivity());
adapter.addItemViewDelegate(new DateDelegate());
adapter.addItemViewDelegate(new ContentDelegate());
GridLayoutManager manager = new GridLayoutManager(getActivity(), 2);
manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override public int getSpanSize(int position) {
return adapter.getItem(position).isDate ? 2 : 1;
}
});
recycler.setLayoutManager(manager);
recycler.addItemDecoration(new PlaybackItemDecoration());
recycler.setAdapter(adapter);
Copy class DateDelegate implements ItemViewDelegate<Playback> {
@Override public int getItemViewLayoutId() {
return R.layout.i_playback_date;
}
@Override public boolean isForViewType(Playback item, int position) {
return item.isDate;
}
@Override public void convert(BaseAdapterHelper helper, Playback item, int position) {
helper.setText(R.id.tv_date, item.formattedDate);
}
}
class ContentDelegate implements ItemViewDelegate<Playback> {
@Override public int getItemViewLayoutId() {
return R.layout.i_playback;
}
@Override public boolean isForViewType(Playback item, int position) {
return !item.isDate;
}
@Override public void convert(BaseAdapterHelper helper, Playback item, int position) {
helper.setText(R.id.tv_class_index, item.getClassNumberHint())
.setText(R.id.tv_teacher, "教师: " + item.teacher)
.setText(R.id.tv_time, "时间: " + item.getFormattedTimePeriod())
.setTag(R.id.parent, position)
.setOnClickListener(R.id.parent, listener);
}
}
Copy class PlaybackItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
Playback playback = adapter.getItem(position);
if (playback.isDate) {
outRect.set(0, divider, 0, divider);
if (position == 0) outRect.top = 0;
} else {
outRect.set(divider, divider, divider, divider);
if (playback.subPosition % 2 == 0)
outRect.left = divider * 2;
else
outRect.right = divider * 2;
}
}
}
除了Adapter的代码外, 其他均是RecyclerView的基本用法, 如使用SpanSizeLookup来设置每个条目占用的单元数, 使用ItemDecoration来控制条目之间的间隔等.