Android如何优雅地实现ListView加载更多功能(2)

来源:互联网 发布:手机直播字幕软件 编辑:程序博客网 时间:2024/06/05 06:25

利用ListView的分类型Item功能实现加载更多功能

上一篇博客Android如何优雅地实现ListView加载更多功能(1),我们介绍完了如何对ListView的Adapter进行抽取的过程,根据MVC编程规范,对getView方法的抽取,其中BaseHolder作为一个抽象类,主要提供Item的界面构造与数据填充功能,有效地将UI与数据解耦。这次我们将基于上一次的内容,利用ListView分类型Item来实现加载更多的功能。

3、加载更多Item的添加

分类型Item即一个ListView列表可以显示多种不同类型的Item布局结构,要显示多种不同类型的Item,主要是要重写BaseAdapter的getItemViewType和getViewTypeCount方法,其中getItemViewType方法是根据position索引位置返回当前Item的类型,getViewTypeCount方法是返回Item的类型的总数。

@Override    public int getItemViewType(int position) {        return super.getItemViewType(position);    }    @Override    public int getViewTypeCount() {        return super.getViewTypeCount();    }

我们重写这两个方法之前,首先定义好Item的类型:

private static final int TYPE_MOREITEM = 0;
private static final int TYPE_CONTENTITEM = TYPE_MOREITEM + 1;

如上,我定义了两种不同的Item类型,TYPE_MOREITEM 既是加载更多对应的Item,TYPE_CONTENTITEM 是普通的item类型。

接下来,因为我们添加了一种Item,我们就需要修改getCount方法,将返回的总数+1;

@Override    public int getCount() {        return  mList.size() + 1;    }

加的这个1,也就是加载更多这个Item条目。

紧接着,我们需要将刚刚我们重写的getItemViewType和getViewTypeCount方法返回正确的数据:

@Override    public int getItemViewType(int position) {        if (getCount() - 1 == position) {            return TYPE_MOREITEM;        } else {            return TYPE_CONTENTITEM ;        }    }@Override    public int getViewTypeCount() {        return TYPE_CONTENTITEM + 1;    }

因为,加载更多类型的Item条目,总是在ListView的最后一个位置,所以在getItemViewType中,当我们判断到position与数据集合元素的size一致时,返回加载更多类型Item对应的条目,其它情况返回普通的Item类型。
getViewTypeCount方法名的字面意思就是返回Item类型的总数,这个很简单,直接return目前最大的Item类型的下标加1,这是因为下标都是从0开始计数的。

好了,刚才我们定义了一种Item的类型是加载更多,接下来,我们需要在合适的时候,返回加载更多的View对象用于显示,当然就需要继续修改getView方法啦。

接着看,getView方法里面,上次我们当convertView为空时,我们返回的是BaseHolder的对象,这个对象,因为在getView方法里面无法确定下来,所以构建了一个抽象方法,子类需要实现。那么我们想想,加载更多的Holder在getView里面能确定下来么?答案是肯定的,因为,加载更多类型的item的界面布局是通用的,UI设计师,肯定是做成一样的,所以我们这时候,需要创建一个加载更多的Holder类,我把这个类叫做MoreHolder,继承自BaseHolder。

那么,我们就需要考虑下,加载更多这个Item都会显示哪些内容呢?根据市面上的一些应用,我们发现,一般这个Item在不同的加载阶段会显示不一样的状态,我总结了一下,一般可以概括为一下几种:

  • 加载更多(上拉过程中,listView显示完成最后一条有效数据时,紧接着显示加载更多的View)
  • 加载中(数据请求的过程显示此View)
  • 加载失败(数据请求失败,包括服务端返回404或者网络出现错误导致请求失败的情况)
  • 加载成功(数据请求成功)
  • 没有更多内容(服务器没有更多内容时显示)

那么首先,我们创建出来这个MoreHolder类,接着定义出这5中状态:

    public static final int STATE_LOADMORE = 0x00; //加载更多    public static final int STATE_LOADING = 0x01; //加载中    public static final int STATE_LOADED = 0x02; //加载完成    public static final int STATE_NOMORE = 0x03; //没有更多数据     public static final int STATE_ERROR = 0x04; //加载失败

并创建出相对用的View,我在initView方法中,实现了界面的布局:

@Override    public View initView() {        layout = new LinearLayout(mContext);        layout.setOrientation(LinearLayout.VERTICAL);        layout.setGravity(Gravity.CENTER);        layout.setClickable(false);        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);        params.gravity = Gravity.CENTER;        loadMoreView = new TextView(mContext);        loadMoreView.setText("加载更多");        loadMoreView.setPadding(40,40,40,40);        loadMoreView.setGravity(Gravity.CENTER);        loadMoreView.setVisibility(View.GONE);        loadingView = new TextView(mContext);        loadingView.setText("正在加载中");        loadingView.setGravity(Gravity.CENTER);        loadingView.setVisibility(View.GONE);        loadingView.setPadding(40,40,40,40);        loadedView = new TextView(mContext);        loadedView.setText("加载成功");        loadedView.setPadding(40,40,40,40);        loadedView.setGravity(Gravity.CENTER);        loadedView.setVisibility(View.GONE);        noMoreView = new TextView(mContext);        noMoreView.setText("------我是有底线的------");        noMoreView.setGravity(Gravity.CENTER);        noMoreView.setPadding(40,40,40,40);        noMoreView.setVisibility(View.GONE);        loadingErrorView = new TextView(mContext);        loadingErrorView.setText("加载失败,点击重试");        loadingErrorView.setPadding(40,40,40,40);        loadingErrorView.setGravity(Gravity.CENTER);        loadingErrorView.setVisibility(View.GONE);        loadingErrorView.setOnClickListener(this);        layout.addView(loadMoreView, params);        layout.addView(loadingView, params);        layout.addView(loadedView, params);        layout.addView(noMoreView, params);        layout.addView(loadingErrorView, params);        return layout;    }

因为,我没有创建layout布局文件,在这里,我用纯Java代码写的布局,这样反而方便,因为大部分情况,这个布局是非常简单的,特意写一个布局,我认为是没有必要的。上面的代码,我用最简单的TextView创建出了这5中不同状态下的View界面。并把它们统一加在一个线性布局中,然后返回这个LinearLayout。

现在回头想想,前面说了,这个Item可能存在5种不同的状态,所以我们还需要定义一个成员变量,记录当前加载更多这个Item的状态,是否需要提供set方法呢?不需要,为什么呢?不要忘了,我们的BaseHolder中有一个setData的方法,可以借这个方法来为当前状态赋值。那么我们先定义出来,这时候,我们MoreHolder的泛型是不是可以确定下来了呢,对,我们要给Integer类型做为限制:

    private int currentState = STATE_LOADMORE;

下面,我们实现initData方法:

@Override    public void initData() {        currentState = getData();    }

这时候,我们就已经给currentState赋值了,我们目前其实还差一个根据状态来控制5种状态的View对象显示隐藏的方法,我们也定义出来:

    private void refleshView() {        loadMoreView.setVisibility(currentState == STATE_LOADMORE ? View.VISIBLE : View.GONE);        loadingView.setVisibility(currentState == STATE_LOADING ? View.VISIBLE : View.GONE);        loadedView.setVisibility(currentState == STATE_LOADED ? View.VISIBLE : View.GONE);        noMoreView.setVisibility(currentState == STATE_NOMORE ? View.VISIBLE : View.GONE);        loadingErrorView.setVisibility(currentState == STATE_ERROR ? View.VISIBLE : View.GONE);    }

这个方法不对外暴露,给private就可以了。这个方法在initView就需要调用一次,初始化加载更多的Item为STATE_LOADMORE 状态对应的View对象。initData时,还需要调用,状态改变了,用于刷新当前的Item的View。

创建完了MoreHolder类之后,我们再回来AnzhiBaseAdapter中的getView方法,当convertView为空时,我们要根据position的位置,调用getItemViewType方法来判断Item的类型,当确定是加载更多这一类型的Item时,我们就创建一个MoreHolder对象,但是,这个对象,我们只需要创建一次,所以,我们可以创建一个方法getMoreHolder,用于获取MoreHolder的对象,代码如下:

private MoreHolder getMoreHolder() {        if (moreHolder == null) {            moreHolder = new MoreHolder(mContext, isHasMore(), this);        }        return moreHolder;    }

Holder创建好了 ,接下来就应该设置数据了,但是,我们不应该给MoreHolder设置数据,这是为什么呢?我们想想,上拉加载更多的时候,是不是有一个请求网络的过程,请求网络又是一个耗时并且不确定结果的操作。那么,我们不可能在这是为MoreHolder设置数据,所以,需要做一个判断,当getItemViewType的类型是一般类型条目,即TYPE_CONTENTITEM类型时,我们才设置数据。所以getView里面的方法体我们也分析完了。下面给出代码:

@Override    public View getView(int position, View convertView, ViewGroup parent) {        if (convertView == null) {            if (getItemViewType(position) == TYPE_MOREITEM) {                itemHolder = getMoreHolder();            } else {                itemHolder = getHolder();            }        } else {            itemHolder = (BaseHolder) convertView.getTag();        }        if (getItemViewType(position) == TYPE_CONTENTITEM) {            itemHolder.setData(getItem(position));        }        return itemHolder.getItemView();    }

现在我们其实运行程序的话,到了最后一个item时,显示的就是加载更多的item,但是,我们还没有写完,显示出加载更多时,我们需要立即做的事是请求网络,然后获得数据,添加到集合的list中,然后再notify通知ListView刷新列表。我们下面看下,这个过程,该如何实现?

4、加载更多功能实现

首先考虑一个问题,当ListView滑动到最后一个Item的时候,在MoreHolder里面我们知不知道?答案是肯定的,因为滑动到最后一个Item的时候,需要调用BaseHolder里面的getItemView这个方法返回一个View给ListView显示。那么,我们就可以考虑在这个方法里面来加载数据。但是还有一个问题,我们的这个MoreHolder针对的是整个APP的ListView的一个统一的通用的Holder,考虑到,应用中,需要用列表分页展示的ListView有很多,而且,他们请求的URL也不一致,那么,我们就没法在MoreHolder里面确定下来请求的URL,如何解决这个问题?

我们可以这样,在MoreHolder的构造方法里面传递一个AnzhiBaseAdapter的对象,如何把加载更多的数据的方法,定义在AnzhiBaseAdapter中,姑且叫它为loadMore吧。在这个方法里面,我们可以去请求网络加载更多数据,然后根据请求结果,给MoreHolder设置currentState的状态,在请求的过程中,动态的去刷新这个item显示的内容。但是依然存在一个问题,AnzhiBaseAdapter还是一个抽象的父类,无法确定具体的请求URL,所以,我们可以这样做:

public void loadMore() {        new Thread(new Runnable() {            @Override            public void run() {                ((Activity) mContext).runOnUiThread(new Runnable() {                    @Override                    public void run() {                        getMoreHolder().setData(MoreHolder.STATE_LOADING);                    }                });                SystemClock.sleep(3000);                final List<T> list = onLoadMore();                ((Activity) mContext).runOnUiThread(new Runnable() {                    @Override                    public void run() {                        if (list != null) {                            if (list.size() == LISTSIZE) {                                getMoreHolder().setData(MoreHolder.STATE_LOADED);                                SystemClock.sleep(300);                                getMoreHolder().setData(MoreHolder.STATE_LOADMORE);                            } else if (list.size() < LISTSIZE) {                                getMoreHolder().setData(MoreHolder.STATE_NOMORE);                            }                        } else {                            getMoreHolder().setData(MoreHolder.STATE_ERROR);                        }                        if (list != null) {                            mList.addAll(list);                            notifyDataSetChanged();                        }                    }                });            }        }).start();    }

这里,我们定义了一个常量LISTSIZE用来维护ListView分页的条数,这个值可以依照不同公司的具体需求更改。
因为请求网络是一个耗时操作,不能放在主线程中执行,我们开了一个线程,线程里面,我们又调用了一个方法onLoadMore,这是一个抽象方法,由子类去实现具体的加载更多的请求操作,我们在这里,只需要关注这个方法返回的list集合的结果,这个请求结果跟我们在MoreHolder中定义的状态值应该是基本吻合的。我们知道,MoreHolder的状态,我已经设置为STATE_LOADMORE了,在开始加载之前,我们为MoreHolder赋了一个STATE_LOADING状态,变为加载中,然后我们调用onLoadMore方法,接着我们判断了返回的list是否为空,当为空时,这时候可能是网络出现错误导致的异常,我们为MoreHolder赋了一个STATE_ERROR状态,当list的size等于LISTSIZE时,默认服务端依然有更多数据,所以,先将MoreHolder的状态赋值为STATE_LOADED,然后给了一个300毫秒的停顿,我们再次将MoreHolder赋了一个STATE_LOADMORE状态,表示下一次还能去服务器请求更多数据。当list的size小于20时,我们判定服务端没有更多数据了,不需要继续去请求更多,这时候,我们将MoreHolder赋了一个STATE_NOMORE状态。
最后,当list不为空的时候,我们还需要将list添加到listView的数据集合中,然后再通知ListView数据集有变化刷新界面。
其实我们返回list的话,不是一个好的选择,最好是根据请求返回的code码来判断,这里我们没有服务器,无法模拟,朋友们自己去实现。

以上就讲解完了,接下来,我们需要把MoreHolder与AnzhiBaseHolder串起来,我们刚刚谈到,在getView的时候,生成MoreHolder对象,会调用getItemView方法,我们传递了一个AnzhiBaseAdapter的对象给MoreHolder,所以,我们在MoreHolder里面需要重写getItemView方法。下面也直接给出代码:

@Override    public View getItemView() {        if (getData() == STATE_LOADMORE && baseAdapter != null) {            baseAdapter.loadMore();        }        return super.getItemView();    }

接下来就是一些小细节的调整,我们这个是一个成熟的框架,必须要考虑到一些细节方面的东西,比如最后一个加载更多的item希望是不可点击的,但是当加载失败的时候,又需要点击重新加载,还有,当新建的ListView就不需要加载更多功能的时候,如何关闭加载更多Item的显示之类的,我们在这里就不做进一步的讲解,直接给出代码,以供参考。

public class MoreHolder extends BaseHolder<Integer> implements View.OnClickListener {    public static final int STATE_LOADMORE = 0x00;    public static final int STATE_LOADING = 0x01;    public static final int STATE_LOADED = 0x02;    public static final int STATE_NOMORE = 0x03;    public static final int STATE_ERROR = 0x04;    private int currentState = STATE_LOADMORE;    private TextView loadMoreView;    private TextView loadingView;    private TextView loadedView;    private TextView noMoreView;    private TextView loadingErrorView;    private AnzhiBaseAdapter baseAdapter;    private LinearLayout layout;    public MoreHolder(Context mContext, boolean isHasMore, AnzhiBaseAdapter baseAdapter) {        super(mContext);        setData(isHasMore ? STATE_LOADMORE : STATE_NOMORE);        this.baseAdapter = baseAdapter;    }    @Override    public View initView() {        layout = new LinearLayout(mContext);        layout.setOrientation(LinearLayout.VERTICAL);        layout.setGravity(Gravity.CENTER);        layout.setClickable(false);        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT);        params.gravity = Gravity.CENTER;        loadMoreView = new TextView(mContext);        loadMoreView.setText("加载更多");        loadMoreView.setPadding(40,40,40,40);        loadMoreView.setGravity(Gravity.CENTER);        loadMoreView.setVisibility(View.GONE);        loadingView = new TextView(mContext);        loadingView.setText("正在加载中");        loadingView.setGravity(Gravity.CENTER);        loadingView.setVisibility(View.GONE);        loadingView.setPadding(40,40,40,40);        loadedView = new TextView(mContext);        loadedView.setText("加载成功");        loadedView.setPadding(40,40,40,40);        loadedView.setGravity(Gravity.CENTER);        loadedView.setVisibility(View.GONE);        noMoreView = new TextView(mContext);        noMoreView.setText("------我是有底线的------");        noMoreView.setGravity(Gravity.CENTER);        noMoreView.setPadding(40,40,40,40);        noMoreView.setVisibility(View.GONE);        loadingErrorView = new TextView(mContext);        loadingErrorView.setText("加载失败,点击重试");        loadingErrorView.setPadding(40,40,40,40);        loadingErrorView.setGravity(Gravity.CENTER);        loadingErrorView.setVisibility(View.GONE);        loadingErrorView.setOnClickListener(this);        layout.addView(loadMoreView, params);        layout.addView(loadingView, params);        layout.addView(loadedView, params);        layout.addView(noMoreView, params);        layout.addView(loadingErrorView, params);        refleshView();        return layout;    }    private void refleshView() {        loadMoreView.setVisibility(currentState == STATE_LOADMORE ? View.VISIBLE : View.GONE);        loadingView.setVisibility(currentState == STATE_LOADING ? View.VISIBLE : View.GONE);        loadedView.setVisibility(currentState == STATE_LOADED ? View.VISIBLE : View.GONE);        noMoreView.setVisibility(currentState == STATE_NOMORE ? View.VISIBLE : View.GONE);        loadingErrorView.setVisibility(currentState == STATE_ERROR ? View.VISIBLE : View.GONE);    }    public int getCurrentState() {        return currentState;    }    @Override    public void initData() {        currentState = getData();        refleshView();    }    @Override    public View getItemView() {        if (getData() == STATE_LOADMORE && baseAdapter != null) {            baseAdapter.loadMore();        }        return super.getItemView();    }    @Override    public void onClick(View view) {        if (view == loadingErrorView && getData() == STATE_ERROR && baseAdapter != null) {            baseAdapter.loadMore();        }    }}
public abstract class AnzhiBaseAdapter<T> extends BaseAdapter {    private static final int TYPE_MOREITEM = 0;    private static final int TYPE_CONTENTITEM = TYPE_MOREITEM + 1;    private boolean hasMore = false;    protected static final int LISTSIZE = 20;    private List<T> mList;    public Context mContext;    private BaseHolder itemHolder;    private MoreHolder moreHolder;    public AnzhiBaseAdapter(Context context, List<T> list) {        this.mList = list;        this.mContext = context;        hasMore = list.size() < LISTSIZE ? false : true;    }    public boolean isHasMore() {        return hasMore;    }    @Override    public int getItemViewType(int position) {        if (hasMore && getCount() - 1 == position) {            return TYPE_MOREITEM;        } else {            return getOtherItemViewType();        }    }    /**     * 需要添加Item类型时 重写     *     * @return     */    public int getOtherItemViewType() {        return TYPE_CONTENTITEM;    }    @Override    public boolean isEnabled(int position) {        if (getItemViewType(position) == TYPE_MOREITEM) {            if (getMoreHolder().getCurrentState() == MoreHolder.STATE_ERROR) {                return true;            }            return false;        }        return super.isEnabled(position);    }    @Override    public int getViewTypeCount() {        return TYPE_CONTENTITEM + 1;    }    @Override    public int getCount() {        return hasMore ? mList.size() + 1 : mList.size();    }    @Override    public T getItem(int position) {        return mList.get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @Override    public View getView(int position, View convertView, ViewGroup parent) {        if (convertView == null) {            if (getItemViewType(position) == TYPE_MOREITEM) {                itemHolder = getMoreHolder();            } else {                itemHolder = getHolder();            }        } else {            itemHolder = (BaseHolder) convertView.getTag();        }        if (getItemViewType(position) == TYPE_CONTENTITEM) {            itemHolder.setData(getItem(position));        }        return itemHolder.getItemView();    }    private MoreHolder getMoreHolder() {        if (moreHolder == null) {            moreHolder = new MoreHolder(mContext, isHasMore(), this);        }        return moreHolder;    }    public abstract BaseHolder<T> getHolder();    public void loadMore() {        new Thread(new Runnable() {            @Override            public void run() {                ((Activity) mContext).runOnUiThread(new Runnable() {                    @Override                    public void run() {                        getMoreHolder().setData(MoreHolder.STATE_LOADING);                    }                });                SystemClock.sleep(3000);                final List<T> list = onLoadMore();                ((Activity) mContext).runOnUiThread(new Runnable() {                    @Override                    public void run() {                        if (list != null) {                            if (list.size() == LISTSIZE) {                                getMoreHolder().setData(MoreHolder.STATE_LOADED);                                SystemClock.sleep(300);                                getMoreHolder().setData(MoreHolder.STATE_LOADMORE);                            } else if (list.size() < LISTSIZE) {                                getMoreHolder().setData(MoreHolder.STATE_NOMORE);                                mList.addAll(list);                            notifyDataSetChanged();                            }                        } else {                            getMoreHolder().setData(MoreHolder.STATE_ERROR);                        }                    }                });            }        }).start();    }    protected abstract List<T> onLoadMore();}

到现在,我们就已经完成了ListView的加载更多的功能了,写一个demo测试一下,demo代码不贴了吧 篇幅太长,我们看下最终效果

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

阅读全文
0 0
原创粉丝点击