【转载请注明出处】
笔者:DrkCore
博客:http://blog.csdn.net/drkcore/article/details/51120570
App 的首页向来是个寸土寸金的地方,如何在首页吸引用户是 PM 的工作,而对于我们开发者而言要做的就是适配。想必大家一定遇到过类似如下的设计稿:
在设计稿上我们可以看到一些重点:
- 页面可以上下滚动
- 元素比较繁多
- 不同页之间有分割线或者间隙
基于上面的几点我们的思路无非分为两条,第一条思路是:
使用ScrollView包裹LinearLayout并动态add多个布局
这种方法的是可以解决问题的,各个板块可以写成子布局,之间的风格线和间隙可以通过添加黑色的高度为 1 像素的 View 和设置 MarginTop 来解决,重复的元素使用动态修改高度的 ListView 来适配。
确实简单粗暴,但缺点是无法复用 View 比较浪费内存。
第二条思路,也就是本篇博文将要论述的思路:
将包括顶部的轮播图、中间的活动版面甚至是分割线和间隙在内的东西都当成 Item 布局,并使用单个ListView适配
较之前者,该方法不但解决了复用 View 的问题而且视图的层次更少。
使用 RecyclerView 也是可以的不过思路其实和 ListView 一样,这里不再赘述。具体实现同样也在给出的源码中,请自行查阅。
之后的章节笔者会详细地阐述实现和代码,篇幅略长。如果你想直接看源代码的话可以直接跳转到文末。
BaseAdapter 的初步封装
考虑到原有的 BaseAdapter 功能太弱,在正式开始工作之前有必要对之进行初步的封装。首先我们在 BaseAdapter 中实现类似 RecyclerView 的 ViewHolder 机制,这里先定义一个 AbsViewHolder 的基类,如下:
public static abstract class AbsViewHolder<Item> {
// 这里使用一个泛型来引用该ViewHolder对应的数据项
private Item item;
private int position;
private final View view;
// 这里省略get和set方法,完整的代码参见文末GitHub
}
之后我们定义 BaseAdapter 的基类代码如下:
// 每个Adapter都要确定自己能适配的数据类型和使用的ViewHolder
public abstract class CoreAdapter<Item, Holder extends AbsViewHolder<Item>> extends BaseAdapter {
// 将数据源封装进Adapter中方便后续的开发
private final List<Item> data = new ArrayList<>();
/* 继承 */
// inflater的单例
private LayoutInflater inflater;
@Override
public final int getCount () {
// 由于数据源在内部并且是final的,这里可以写死
return data.size();
}
@Override
public final Item getItem (int position) {
// 同上,写死
return data.get(position);
}
@Override
public long getItemId (int position) {
return position;
}
@Override
public final View getView (int position, View convertView, ViewGroup parent) {
// 获取inflater的单例
if (inflater == null) {
//当getView方法被调用时必定已经被set进ListView了
//此时parent.getContext()不会空指针
inflater = LayoutInflater.from(parent.getContext());
}
//获取position对应的数据和viewType
int viewType = getItemViewType(position);
Item data = getItem(position);
//实现ViewHolder机制
Holder holder;
if (convertView == null) {
holder = createViewHolder(inflater, parent, viewType);
convertView = holder.getView();
convertView.setTag(holder);// 绑定ViewHolder
} else {
holder = (Holder) convertView.getTag();
}
// 将数据和position绑定到ViewHolder上
holder.setPosition(position);
holder.setItem(data);
bindViewData(holder, position, data, viewType);
return convertView;
}
/* 回调 */
//创建ViewHolder实例
//如果你使用过RecyclerView的Adapter的话这里你会发现这里的思想与之基本一致
protected abstract Holder createViewHolder (LayoutInflater inflater, ViewGroup parent, int viewType);
//绑定数据到ViewHolder实例上
//也是RecyclerView.Adapter的思想
protected abstract void bindViewData (Holder holder, int position, Item data, int viewType);
/* 数据展示 */
//从外部添加数据项到Adapter内部的数据源,并刷新视图
public final void display (Item... items) {
this.data.clear();
Collections.addAll(this.data, items);
notifyDataSetChanged();
}
public final void display (Collection<? extends Item> items) {
this.data.clear();
this.data.addAll(items);
notifyDataSetChanged();
}
//后续还可以拓展上add,remove之类的方法,这里略去。
//完整代码请参阅GitHub
}
经过以上的修改我们已经将 ViewHolder 机制封装进了 Adapter 之中,为后续的开发剩下了大量的代码。
具体实现
由于我们将所有的控件都当成Item布局不用想也知道这里会有数不清的 if-else 要写,这显然也是不够优雅的。要解决这个问题我们就需要用到策略模式和策略工厂。
我们首先将每个 Item 的操作逻辑封装成对应的策略,一个策略处理一种 Item 的实例化、绑定数据和事件回调等逻辑。如下:
/*单个Item布局的策略*/
public abstract class AbsItemOperator<Item, Holder extends AbsViewHolder<Item>> {
//该策略支持的数据类型的单例
private Type type;
//判断该策略是否能够适配obj的实例
public final boolean canHandleObject (Object obj) {
if (type == null) {
//获取实现类的第一个泛型参数作为该策略的可适配类型
//ClassUtil请参见GitHub源代码
Type types[] = ClassUtil.getGenericParametersType(getClass());
type = types[0];
}
if (obj instanceof Collection) {
//出于安全和可维护性考虑这里禁止直接适配Collection的子类。
throw new IllegalStateException("不允许直接适配Collection或者其子类");
}
return type == obj.getClass();
}
/*回调*/
//创建ViewHolder
public abstract Holder createViewHolder (LayoutInflater inflater, ViewGroup parent);
//绑定数据到ViewHolder
public abstract void bindViewData (Holder holder, Item data);
}
我们可以在这个基类中看到两个抽象方法:createViewHolder()
和 bindViewData()
,只要在这两个方法里面实现对 Item 的处理即可。
之后我们需要将 Adapter 封装一个策略工厂,负责策略的匹配和调度。代码如下:
public final class FlexibleAdapter extends CoreAdapter<Object, AbsViewHolder<Object>> {
//使用被final修饰的List来保存所有支持的布局策略
private final List<AbsItemOperator> operators = new ArrayList<>();
//在构造的时候必须指明所有支持的策略
public FlexibleAdapter (AbsItemOperator<?, ?>... operators) {
Collections.addAll(this.operators, operators);
}
/* 继承 */
@Override
protected AbsViewHolder<Object> createViewHolder (LayoutInflater inflater, ViewGroup parent, int viewType) {
//createViewHolder方法是在之前的CoreAdapter之中定义的
//其中viewType参数来自getItemViewType(),其值也是目标策略在策略列表operators中的序数
//取出策略,直接调度
return (AbsViewHolder<Object>) operators.get(viewType).createViewHolder(inflater, parent);
}
@Override
protected void bindViewData (CoreAdapter.AbsViewHolder<Object> viewHolder, int position, Object data, int viewType) {
//同上,根据viewType取出策略并调度
operators.get(viewType).bindViewData(viewHolder, data);
}
@Override
public int getItemViewType (int position) {
//获取position对应的数据实例
//并通过AbsItemOperator.canHandleObject()方法逐一匹配合适的策略
Object obj = getItem(position);
for (int i = 0, count = operators.size(); i < count; i++) {
if (operators.get(i).canHandleObject(obj)) {
return i;// 可处理
}
}
// 没有策略能够适配该数据
throw new IllegalStateException("指定数据类型不存在可用的operator");
}
@Override
public int getViewTypeCount () {
//策略列表的大小就是布局类型数
return operators.size();
}
}
至此整体的框架就已经搭建完毕,往后如果有新增的Item布局只需要新增一个独立的策略实现类即可,十分灵活。
实现一个策略
这里我们写一个策略以加深对这种思路的理解。布局文件很简单:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/core_white">
<View
android:layout_width="24dp"
android:layout_height="16dp"
android:layout_gravity="center_vertical"
android:background="@color/core_holo_green_light"/>
<TextView
android:id="@+id/textView_listView_tip"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"/>
</FrameLayout>
有点经验的开发者应该立马就能想象到它实例化后的样子,主体只是一个用来显示文字的 TextView 而已。接下来是它对应的策略:
public class TipOperator extends AbsItemOperator<Tip, TipViewHolder> implements View.OnClickListener {
//写一个AbsViewHolder的基类,ViewHolder本身其实还可以深挖不少东西,这里篇幅有限就不再赘述
public static class TipViewHolder extends AbsViewHolder<Tip> {
public TipViewHolder(View v) {
super(v);
}
}
/*继承*/
@Override
public TipViewHolder createViewHolder(LayoutInflater inflater, ViewGroup parent) {
//实例化布局
View view = inflater.inflate(R.layout.activity_listview_tip, parent, false);
//绑定事件
view.setOnClickListener(this);
return new TipViewHolder(textView);
}
@Override
public void bindViewData(TipViewHolder holder, Tip data) {
//绑定数据
TextView textView = (TextView) holder.getView().findViewById(R.id.textView_listView_tip);
textView.setText(data.tip);
}
/*回调*/
@Override
public void onClick(View v) {
//因为CoreAdapter中已经实现了ViewHolder机制因而itemView.getTag返回的必定是对应的ViewHolder,在这里就是TipViewHolder
//在设计上ViewHolder和ItemView的实例是绑定在一起的,只是携带者的position和data一直在变化
//因此只要获得ViewHolder就能获得当前的position和data
TipViewHolder holder = (TipViewHolder) v.getTag();
if(onTipClickListener != null){
//回调监听者
onTipClickListener.onTipClick(v,holder.getPosition(),holder.getItem());
}
}
/*接口*/
//监听者接口
public interface OnTipClickListener{
void onTipClick(View v,int position,Tip tip);
}
private OnTipClickListener onTipClickListener;
public TipOperator setOnTipClickListener(OnTipClickListener onTipClickListener) {
this.onTipClickListener = onTipClickListener;
return this;
}
}
实际使用时如下:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView listView = new ListView(this);
listView.setDivider(null);
listView.setDividerHeight(0);
setContentView(listView);
setTitle(getClass().getSimpleName());
//实例话FlexibleAdapter并填入所有的布局策略
FlexibleAdapter adapter = new FlexibleAdapter(new RotateOperator(), new TipOperator(), new PanelOperator(), new MsgOperator(), new DividerOperator(), new SpanOperator());
listView.setAdapter(adapter);
//逐一添加每个数据项
List<Object> list = new ArrayList<>();
Rotate rotate = new Rotate(R.layout.activity_listview_rotate_1, R.layout.activity_listview_rotate_2, R.layout.activity_listview_rotate_3);
list.add(rotate);
list.add(new Divider());
list.add(new Span());
int times = 5;
while (times-- > 0) {
list.add(new Divider());
Tip tip = new Tip("这是一个Tip");
list.add(tip);
list.add(new Divider());
Panel panel = new Panel("这个是内容,我就随便输入一些什么东西");
list.add(panel);
list.add(new Divider());
list.add(new Span());
}
list.add(new Divider());
times = 20;
while (times-- > 0) {
list.add(new Msg("这是文本35435435453545354"));
}
//刷新到界面上
adapter.display(list);
}
以上代码运行起来后效果如下:
之后新增一个布局只需要新增一个对应的策略即可。
最后附上源码地址: