谈论起瀑布流,相信大家不怎么陌生,实际上现在好多的网站都已经按照这种风格设计了,简言之就是按需加载,先加载一屏的内容,当用户向下滚动到页面底部的时候,继续加载一屏的内容,就这样,一直循环往复,用户浏览多少,加载多少!
在Android ListView的设计中,就可以很好的融合这种设计方法,本文就简单介绍一种方法来实现瀑布流的功能,进入程序主界面的时候先加载一部分内容,然后当用户向下滚到到底部的时候继续加载,用户向下继续滚到,则继续加载,一直到内容加载完毕。
本文的的程序设计思路来自于cwac-endless,其功能介绍如下:
Provides the EndlessAdapter, a wrapper for an existing ListAdapter that adds "endless list" capability. When the user scrolls to the bottom of the list, if there is more data for this list to be retrieved, your code gets invoked in a background thread to fetch the new rows, which then get seamlessly attached to the bottom of the list.
代码的实现还依赖于另一个开发包cwac-adapter,其功能介绍如下:
Provides an AdapterWrapper, a simple wrapper class that, by default, delegates all ListAdapter methods to a wrapped ListAdapter. The idea is that you can extend AdapterWrapper and only override certain ListAdapter methods, with the rest handled via the wrapped adapter.
上面两个jar包的代码都可以在参考链接中在线浏览,demo程序也可以在参照链接中下载,本文主要分析一下其主要的源码部分,了解一下背后的实现机制。
源码分析
cwac-adapter
它的实现比较简单,只是简单封装了一下BaseAdapter,通过委托机制调用用户自己实现的ListAdapter类,示例代码:
123456789101112131415161718192021222324252627282930313233343536373839 | /**
* Adapter that delegates to a wrapped LisAdapter, much as
* CursorWrapper delegates to a wrapped Cursor.
*/
public class AdapterWrapper extends BaseAdapter {
// ...
/**
* Constructor wrapping a supplied ListAdapter
*/
public AdapterWrapper(ListAdapter wrapped) {
super();
this.wrapped=wrapped;
wrapped.registerDataSetObserver(new DataSetObserver() {
public void onChanged() {
notifyDataSetChanged();
}
public void onInvalidated() {
notifyDataSetInvalidated();
}
});
}
/**
* Get a View that displays the data at the specified
* position in the data set.
* @param position Position of the item whose data we want
* @param convertView View to recycle, if not null
* @param parent ViewGroup containing the returned View
*/
@Override
public View getView(int position, View convertView,
ViewGroup parent) {
return(wrapped.getView(position, convertView, parent));
}
}
|
它的功能就是要实现数据的按需加载,它的构造函数如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950 | /**
* Adapter that assists another adapter in appearing
* endless. For example, this could be used for an adapter
* being filled by a set of Web service calls, where each
* call returns a "page" of data.
*
* Subclasses need to be able to return, via
* getPendingView() a row that can serve as both a
* placeholder while more data is being appended.
*
* The actual logic for loading new data should be done in
* appendInBackground(). This method, as the name suggests,
* is run in a background thread. It should return true if
* there might be more data, false otherwise.
*
* If your situation is such that you will not know if there
* is more data until you do some work (e.g., make another
* Web service call), it is up to you to do something useful
* with that row returned by getPendingView() to let the
* user know you are out of data, plus return false from
* that final call to appendInBackground().
*/
abstract public class EndlessAdapter extends AdapterWrapper {
abstract protected boolean cacheInBackground() throws Exception;
abstract protected void appendCachedData();
private View pendingView=null;
private AtomicBoolean keepOnAppending=new AtomicBoolean(true);
private Context context;
private int pendingResource=-1;
private boolean isSerialized=false;
// ...
/**
* Constructor wrapping a supplied ListAdapter and
* providing a id for a pending view.
*
* @param context
* @param wrapped
* @param pendingResource
*/
public EndlessAdapter(Context context, ListAdapter wrapped,
int pendingResource) {
super(wrapped);
this.context=context;
this.pendingResource=pendingResource;
}
}
|
大家可以发现,其实现也是利用一种委托机制,调用用户自己实现的ListAdapter,上面的pendingResource参数是占位符资源Layout ID,类似于“更多”,如果目前只加载了部分的数据,那么会在listview的最底端加入一个虚拟节点,当用户拖动到该虚拟节点的时候,继续加载后面的数据,如果数据已经全部加载完毕,那么该节点就自动消失了。
接下来一个重要的方法是getCount,它用来指示当前listview中数据的条目数,源码如下:
123456789101112 | /**
* How many items are in the data set represented by this
* Adapter.
*/
@Override
public int getCount() {
if (keepOnAppending.get()) {
return(super.getCount() + 1); // one more for "pending"
}
return(super.getCount());
}
|
大家发现其特殊的地方了吧,在正常数据的末尾多加了一项,虚拟节点的占位符。那个keepOnAppending.get()的判断语句是用来标示数据是否已经全部加载完毕,如果没有的话就加入虚拟节点,如果已经全部加载完毕,则不会添加虚拟节点,正常显示就ok了。
另一个最重要的函数就是getview,其源码如下:
1234567891011121314151617181920212223242526272829 | /**
* Get a View that displays the data at the specified
* position in the data set. In this case, if we are at
* the end of the list and we are still in append mode, we
* ask for a pending view and return it, plus kick off the
* background task to append more data to the wrapped
* adapter.
*
* @param position
* Position of the item whose data we want
* @param convertView
* View to recycle, if not null
* @param parent
* ViewGroup containing the returned View
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (position == super.getCount() && keepOnAppending.get()) {
if (pendingView == null) {
pendingView=getPendingView(parent);
executeAsyncTask(new AppendTask());
}
return(pendingView);
}
return(super.getView(position, convertView, parent));
}
|
这个就是按需加载的实现点了,判断一下如果数据还未完全加载完毕,并且用户已经拖动屏幕到虚拟节点的位置,则启动一个异步线程executeAsyncTask来实现后台数据的加载,这样就实现了按需加载的事件促发,并启动了业务处理线程。
后台业务处理线程的代码如下:
1234567891011121314151617181920212223242526272829303132333435363738 | abstract protected boolean cacheInBackground() throws Exception;
abstract protected void appendCachedData();
/**
* A background task that will be run when there is a need
* to append more data. Mostly, this code delegates to the
* subclass, to append the data in the background thread
* and rebind the pending view once that is done.
*/
class AppendTask extends AsyncTask {
@Override
protected Exception doInBackground(Void... params) {
Exception result=null;
try {
keepOnAppending.set(cacheInBackground());
}
catch (Exception e) {
result=e;
}
return(result);
}
@Override
protected void onPostExecute(Exception e) {
if (e == null) {
appendCachedData();
}
else {
keepOnAppending.set(onException(pendingView, e));
}
pendingView=null;
notifyDataSetChanged();
}
}
|
所以,用户只需要实现其中的两个回调方法即可,一个是后台获取数据的方法,他在非UI线程中运行,实现数据的获取过程,即cacheInBackground,另一个是数据数据获取以后的回调方法,用来将数据和listview的界面绑定在一起,他运行在UI线程中,即appendCachedData。
最后特别说明一下即cacheInBackground的返回值,原文描述如下:
This method returns a boolean, which needs to be true if there is more data yet to be fetched, false otherwise.
也就是,每次后台加载完数据的时候要返回一个布尔类型的值,如果当前加载的数据不是最后一批数据,则返回true,代表后面还可以继续加载,如果当前加载的数据已经是最后一批数据,那么返回false,代表数据已经全部加载完毕,就不会再出现虚拟节点了,实际上回调机制中还处理了另一种情况,那就是后台加载过程中出现异常,此时的回调在onException中,源码如下:
123456789101112131415161718 | /**
* Called if cacheInBackground() raises a runtime
* exception, to allow the UI to deal with the exception
* on the main application thread.
*
* @param pendingView
* View representing the pending row
* @param e
* Exception that was raised by
* cacheInBackground()
* @return true if should allow retrying appending new
* data, false otherwise
*/
protected boolean onException(View pendingView, Exception e) {
Log.e("EndlessAdapter", "Exception in cacheInBackground()", e);
return(false);
}
|
总结
通过上面的源码介绍,相信大家知道背后的原理了吧,实际上非常简单,就是判断一下当前是否已经加载完了全部的数据,如果没有的话,就在界面的最底端加入一个虚拟节点,每当用户滑动界面到虚拟节点,则启动一个后台线程来实现数据的请求,当获取到数据后再绑定到UI界面中,如此循环往复,直至数据全部加载完。