电子市场项目总结(三)
ListView的抽取和封装
1.BaseFragment中我们留下了三个抽象方法,留给子类去实现,先回顾一下是哪三个方法
- protected abstract View initSuccessLayout(); // 加载成功后是什么样的布局
- protected abstract State loadData(); // 加载数据的逻辑
- protected abstract void initListener(); // 监听的初始化
还记得我们使用了简单工厂的方式去生产对应序号的Fragment,那么接下来我们就为工厂中添加产品模型。
第一个继承自BaseFragment的Fragment:
public class HomeFragment extends BaseFragment{
// 监听器的添加都可以安排在这里,但是该方法应当由调用者安排放在自己的调用位置
protected void initListener() {
}
// 加载数据,调用者根据自己的数据加载程度返回对应的加载状态
protected State loadData() {
return null;
}
// 加载成功的布局,调用者可以返回自己的布局
protected View initSuccessLayout() {
return null;
}
}
2.分析布局,完成initSuccessLayout()
项目演示:
我们先从布局的实现开始,通过观察完整的项目我们可以看到大多Fragment都使用了listview(因为是学习还是先从listview开始入手)来进行展示,涉及到listview数据展示那么肯定也会使用到对应BaseAdapter,同时可以看到每一个listview都具备上拉加载更多的功能。有这么多的相似点我们首先想到的就是将listview进行封装。
3.抽取MyBaseAdapter
首先可以想到的抽取就是数据获取的抽取,代码如下:
public abstract class MyBaseAdapter<T> extends BaseAdapter {
public MyBaseAdapter(List<T> mDataList) {
this.mDataList = mDataList;
}
@Override
public int getCount() {
return mDataList.size();
}
@Override
public T getItem(int position) {
return mDataList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
}
为Adapter添加泛型T,这样Adapter中就可以使用泛型来规范整个类中的数据。
其次getView()方法同样也可以进行封装,但是getView中都是不同的item布局又该如何进行封装呢?先看一看我们以前是如何使用getView的,代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
// 1.判断是否存在复用的view
if(convertView == null){
// 2.加载对应的item的布局文件
convertView = UIUtils.initLayout(R.layout.xxx);
// 3.将复用的view放到viewholder中去,让viewholder进行FindViewById
// 同时将viewholder存储在convertView的tag中
convertView.setTag(new ViewHolder(convertView));
}
// 4.从tag中获取viewholder
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
// 5.再对viewholder中已经findViewById完成的view进行数据填充
viewHolder.textView.setText("XXX");
// 6.返回这个复用view
return convertView;
}
/*
这里很多人会问到为什么要使用static的内部类,实际上static的内部类在使用方式上相当于一个外部类,可以直接new Adapter.ViewHolder()这样来创建,相反创建的方式为new Adapter().new ViewHolder()。
当然这里是可以直接new ViewHolder()的,那么为什么要使用静态的呢?1.Java大牛们都是这么写的有什么问题你问他们啊!
2.这样写可以看出类的加载顺序,非static情况时必须要有一个Adapter对象才会加载这个ViewHolder,word天,这不很耗内存吗,在android这种实时要考虑内存分配问题的编程环境里,一个非static内部类,一直持有上一个item对象的指引这将导致内存泄漏啊!
所以,推荐使用static的内部类来让viewholder不再持有Adapter的引用这样会起到一定的优化。
*/
public static class ViewHolder{
TextView textView;
public ViewHolder(View view){
// 在构造方法中去findViewById
textView = (TextView) view.findViewById(R.id.xxx);
}
}
关于为什么使用static的viewholder可以看http://blog.csdn.net/caoyang521/article/details/49847881
可以看到在getView中我们一定会做的事情就是以上6步,那么这些步骤如果每次都让调用者去写的话,这么多listview也未免太重复了。那么该怎么办呢?
4.封装viewholder
实际上在最新的开发中已经不推荐使用listview了,取而代之的是以viewholder为中心的Recycleview,我们把主要的目光放在viewholder上处理对应的布局,而不是如何获取getview。
首先可以看到viewholder中我们已经可以findViewById了
那么同理我们也可以在这里进行 setTag和设置数据,代码如下:
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// 生成一个新的BaseViewHolder并拿到它已经实现好的view
convertView = getBaseViewHolder().getRootView();
}
// 获取对应的viewHolder
BaseViewHolder holder = (BaseViewHolder) convertView.getTag();
// 使用viewholder去填充数据
holder.setData(getItem(position));//如果时普通的布局时加载数据
// 返回复用的view
return convertView;
}
// 返回一个BaseViewHolder 以供显示item使用
protected abstract BaseViewHolder<T> getBaseViewHolder();
// 抽象出来的ViewHolder T为item对应的数据类型
public abstract class BaseViewHolder<T> {
protected View mRootView;
protected T data;
public BaseViewHolder() {
// 加载布局,同时将自身holder打上tag
mRootView = initLayout();
mRootView.setTag(this);
}
// 加载布局由对应的调用者加载,并进行findViewById
protected abstract View initLayout();
// 刷新界面的过程由调用者实现
protected abstract void refreshView(T item);
// 返回加载的布局
public View getRootView() {
return mRootView;
}
// 设置数据(虽然这个方法只是比refreshView多了一句设置data,但这一句很重要。
因为refreshView是交给子类去实现的,那么子类应当只关心如何填充数据,同时在父类中进行这一步,在MyBaseAdapter中就可以使用对应的data)
public void setData(T data){
this.data=data;
refreshView(data);
}
// 获取当前的数据
public T getData(){
return data;
}
}
可以看到viewholder进行了两个主要事情,让子类去加载布局和让子类去填充数据,同时还有将自己标记在加载出来的布局上。
那么这样的viewHolder应用面就变得更加广泛了,它不仅可以使用在listview中,所有的需要findViewById和setData的场合都可以使用它。
5.使用ViewHolder在MyBaseAdapter中添加上拉加载更多
上拉加载更多的实现方法有很多种,在之前我们是通过将一个layout添加在listview的脚步布局中,然后监听listview 的scroll事件,当其展示lastViewPosition为最后一个对象且scroll处于IDLE空闲状态时就让layout的padding值变为0,当不需要显示时将padding值设置为负值。通过设置监听的方式,将加载事件写在layout展现之后。通过flag的形式控制layout的显示和关闭。
那么这里我们已经封装了viewholder能否换一种做法呢?代码如下
public class LoadingMoreViewHolder extends BaseViewHolder<Integer> {
public static int state_has_more = 1;
public static int state_error = -1;
public static int state_no_more = 0;
private ProgressBar mLoadingPb;
private TextView mMLoadingTv;
//是否是可见的,在有的listview里可能并不需要下拉加载
private boolean isShow;
//初始化时声明是否显示加载
public LoadingMoreViewHolder(boolean isShow) {
super();
this.isShow = isShow;
// 声明当前的初始状态,并重新设置
setData(isShow ? state_has_more : state_no_more);
}
// 重写父类的方法,并进行findViewById
protected View initLayout() {
mRootView = UIUtils.initLayout(R.layout.list_item_loading_more);
mLoadingPb = (ProgressBar) mRootView.findViewById(R.id.pb_loading);
mMLoadingTv = (TextView) mRootView.findViewById(R.id.tv_loading_desc);
return mRootView;
}
// 根据数据进行刷新,这里是loadingMore的逻辑判断
public void refreshView(Integer item) {
if (!isShow)
item = state_no_more;
//设置 进度条的可见性
mLoadingPb.setVisibility(item == state_has_more ? View.VISIBLE : View.GONE);
//设置描述文字的内容
if (item == state_has_more)
mMLoadingTv.setText("正在加载中");
else if (item == state_error)
mMLoadingTv.setText("加载失败");
//设置描述文字的可见性,加载数据为空时不可见
mMLoadingTv.setVisibility(item == state_no_more ? View.GONE : View.VISIBLE);
}
}
如上代码是封装了一个LoadingMore的ViewHolder,可以通过设置setData的方式来控制其状态。那么我们怎么将其添加到Adapter中呢?实际上绝大多数步骤和为listview添加多种类型是一样的方法,代码如下:
// 定义了几种不同的状态,这写状态是用来判断当前展示的是loadingMore还是普通的item
public static final int TYPE_NORMAL = 1;
public static final int TYPE_LOAD_MORE = 0;
private boolean isLoading = false;
// 对应的count需要加1
public int getCount() {
return mDataList.size() + 1;
}
// 对应getItem应当改变
public T getItem(int position) {
return position < getCount() - 1 ? mDataList.get(position) : null;
}
// 当前所显示的种类
public int getViewTypeCount() {
return 2;
}
//返回对应position对应的种类
public int getItemViewType(int position) {
if (position == getCount() - 1) {
// 这样判断,有利于更多类型的拓展,因为只有唯一不变的那个类型被限制了
return TYPE_LOAD_MORE;
} else {
return getMoreViewType(position);
}
}
//提供给子类进行重写,拓展子类中类型
public int getMoreViewType(int position) {
return TYPE_NORMAL;
}
//封装viewholder,将settag和findviewbyid以及view的刷新封装出来
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
//获取一个BaseViewHolder 并获取其中的方法
if (getItemViewType(position) == TYPE_LOAD_MORE)
// canload方法提供给子类来控制是否存在上拉加载
convertView = new LoadingMoreViewHolder(canLoad()).getRootView();
else {
// 这样写判断有利于拓展 baseviewholder的返回类型
convertView = getBaseViewHolder(position).getRootView();
}
}
BaseViewHolder holder = (BaseViewHolder) convertView.getTag();
// 当上拉加载的视图可见时,应当刷新数据,同时改变加载状态的view
if (getItemViewType(position) == TYPE_LOAD_MORE) {
// 刷新过程由子类来实现
loadingMore(holder);
} else
holder.setData(getItem(position));//如果时普通的布局时加载数据
return convertView;
}
//设置是否可以加载,默认返回true可加载
public boolean canLoad() {
return true;
}
protected void loadingMore(final BaseViewHolder holder) {
// 当item可视时会不停的加载,所以应当对加载进行判断
if (isLoading || holder.getData() != Integer.valueOf(LoadingMoreViewHolder.state_has_more)) {
return;
}
// 同样与BaseFragment中封装的load方法相同,父类将开辟子线程和对布局状态控制进行了封装,子类只需要关心如何去加载更多
ThreadManager.getInstance().execute(new Runnable() {
@Override
public void run() {
isLoading = true;
// 这里仍然将加载更多的方法交给子类去实现
final ArrayList<T> data = (ArrayList<T>) loadingMore();
UIUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
isLoading = false;
// 设置的是下一次的加载情况
// 判断是否加载到数据
if (data != null) {
mDataList.addAll(data);
// 该处小与20仅仅判断的一种形式,可以是其他任何逻辑
if (data.size() < 20) {
ToastUtil.show(MyApplication.getContext(), "没有更多更新了~");
holder.setData(LoadingMoreViewHolder.state_no_more);
} else
holder.setData(LoadingMoreViewHolder.state_has_more);
} else
holder.setData(LoadingMoreViewHolder.state_error);
notifyDataSetChanged();
}
});
}
});
}
//上拉加载的过程,返回加载的结果
protected abstract List<T> loadingMore();
5.总结,封装完成后子类应该做的事情
- protected abstract BaseViewHolder getBaseViewHolder(int position); // 子类提供一个用于处理item数据的holder
- protected abstract List loadingMore(); // 子类完成加载更多的逻辑,返回对应的数据给Adapter来进行判断处理
- public int getMoreViewType(int position); // 子类可以重写这个方法,因为这里父类只有两种类型,但如果子类中有更多种类就可通过重写这个方法来实现更多种类型的判断。
- public boolean canLoad(); // 通过重写该方法,子类可以很控制是否需要加载更多这一功能
- protected abstract View initLayout(); // viewholder中要实现布局的加载和findViewById
- protected abstract void refreshView(T item); // viewholder中实现数据填充的逻辑
ViewHolder的封装看似复杂,实则简单,其是主要逻辑是将initView的部分在构造器中直接执行,refreshView的部分交给调用者在有需要时调用,很好的将initView和setData封装在了一起,使得在开发时,每一个模块只需要关心自己的事情不会与大量的不相干的代码混杂在一起。