当ListView加载的数据项数量非常庞大时,数据加载速度会非常慢,这样将严重影响应用的用户体验,甚至会提示程序无响应的错误。因此,对ListView采取分页异步的方式加载数据,对于改善用户体验和提高性能将有很大的帮助。
所谓分页,是指在加载数据时,先加载其中一部分数据,剩余的数据当用户下拉到ListView底部时再进行加载。
我们还需要获取手机中通讯录中项目的数量,还要设定每页加载多少条项目,以便获取需要分几页来加载:
在主线程中,new一个Handler对象,当我们刚刚开的线程获取了联系人数据后,就开始更新ListView的视图,把新增的数据追加到ListView底部:
另外,在getContacts()方法,中,我们使用:
所谓分页,是指在加载数据时,先加载其中一部分数据,剩余的数据当用户下拉到ListView底部时再进行加载。
假设ListView显示项目条目界面如下:
当拉到最后一项时,将显示一个进度条,并提示正在加载:
为了获取手机中的联系人,添加一个服务类:DataService,该类方法getContacts()返回获取的联系人集合。getCount()方法用于获取手机中所有联系人的数量。
要读取通讯录,应在程序清单文件中声明该权限:
<uses-permission android:name="android.permission.READ_CONTACTS" />
获取联系人的代码如下:
<span style="white-space:pre"> </span>/* Get the contacts.
*
* @return a List content the contacts.
*/
public List<HashMap<String, String>> getContacts(int offset, int maxValue)
{
List<HashMap<String, String>> contacts = new ArrayList<HashMap<String, String>>();
HashMap<String, String> map = null;
// raw_contacts table
Uri uri = Uri.parse("content://com.android.contacts/contacts");
ContentResolver resolver = this.context.getContentResolver();
String limit = "_id desc limit " + offset + " ," + maxValue;
Cursor cursor = resolver.query(uri, new String[] { "_id" }, null, null, limit);
int rowId = 0;
Cursor dataCursor = null;
while (cursor.moveToNext())
{
// data table
rowId = cursor.getInt(cursor.getColumnIndex("_id"));
uri = Uri.parse("content://com.android.contacts/contacts/" + rowId
+ "/data");
dataCursor = resolver.query(uri, new String[]
{ "mimetype", "data1" }, null, null, null);
map = new HashMap<String, String>();
while (dataCursor.moveToNext())
{
String data = dataCursor.getString(dataCursor
.getColumnIndex("data1"));
String mineType = dataCursor.getString(dataCursor
.getColumnIndex("mimetype"));
if (mineType.equals("vnd.android.cursor.item/name"))
map.put("name", data);
else if (mineType.equals("vnd.android.cursor.item/phone_v2"))
map.put("phone", data);
}
contacts.add(map);
}
return contacts;
}
/**
* Get the number of contacts.
*
* @return the number of contacts.
*/
public long getCount()
{
Uri uri = Uri.parse("content://com.android.contacts/contacts");
ContentResolver resolver = this.context.getContentResolver();
Cursor cursor = resolver.query(uri, new String[] { "_id" }, null, null, null);
return cursor.getCount();
}
要想知道ListView什么拉到底部,需要为ListView添加一个监听对象,该对象应是实现的OnScrollListener接口的实例。添加一个实现OnScrollListener接口的内部类。
该接口有两个方法:
public void onScrollStateChanged(AbsListView view, int scrollState):
这个方法用于获取ListView在滚动时的状态,要想知道详细的状态,可以在日志控件台中打印scrollState参数。在这里我们不必关心这个方法。
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
这个方法在ListView滚动时调用,我们关心的是totalItemCount这个参数。从名字中也可以推测到方法参数代表ListView中已加载的项目总数。
我们还要获取ListView当前页中的最后一个可见项目的索引:
int lastVisiblePosition = listView.getLastVisiblePosition();
最后一个可见项目始终在ListView的最下方,并且当列表后面还有可显示的项目时,最后一个可见项目始终是在变化的,直到已经到达底部。
到达底部后,我们就要开始加载下一页。那么怎么知道已经到达了底部呢?结合totalItemCount这个参数可以计算出来:当最后一个可见项目的索引+1 等于totalItemCount时,就已经到达了该页的底:
// reach the end of one page
if (lastVisiblePosition + 1 == totalItemCount && totalItemCount > 0)
为什么还要判断 totalItemCount > 0 呢,因为当ListView开始显示时,totalItemCount的值为0,而lastVisiblePositioin的值为-1,这样一来if中的条件成立,就会执行if中的代码,开始加载下一页,而这不是我们想要的结果。
我们还需要获取手机中通讯录中项目的数量,还要设定每页加载多少条项目,以便获取需要分几页来加载:
// Get the number of contacts.
long contactCount = dataService.getCount();
// How many items are visible in one page.
int pageItemCount = 30;
// Total page count.
int maxPageCount = (int ) (contactCount%pageItemCount == 0 ?
contactCount/pageItemCount : contactCount/pageItemCount+1); // Total page count.
int currentPageIndex;
判断了当前加载的页面不超过最大页面数后,就可以进行加载了,我们开一条线程来获取通讯录,以实现异步加载。
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
int lastVisiblePosition = listView.getLastVisiblePosition();
// reach the end of one page
if (lastVisiblePosition + 1 == totalItemCount && totalItemCount > 0)
{
currentPageIndex = totalItemCount % pageItemCount == 0 ?
totalItemCount / pageItemCount : totalItemCount / pageItemCount + 1;
int nextPageIndex = currentPageIndex + 1;
if (nextPageIndex <= maxPageCount && loadFinished)
{
// Add a footer to the listView
listView.addFooterView(footer);
// Start a thread to load the data.
loadFinished = false;
new Thread(new Runnable()
{
@Override
public void run()
{
// Offset data that had been loaded.
List<HashMap<String, String>> data = dataService.getContacts(currentPageIndex * 30, 30);
handler.sendMessage(handler.obtainMessage(100, data));
}
}).start();
}
}
}
}
在加载时,可以使用
listView.addFooterView(footer);
为ListView添加一个页脚,就是我们一开始显示的那个进度条和“正在加载”的文本。
在主线程中,new一个Handler对象,当我们刚刚开的线程获取了联系人数据后,就开始更新ListView的视图,把新增的数据追加到ListView底部:
Handler handler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
@SuppressWarnings("unchecked")
List<HashMap<String, String>> newData = (List<HashMap<String, String>>) msg.obj;
MainActivity.this.data.addAll(newData);
// Notifies the listView that the data set has been changed.
adapter.notifyDataSetChanged();
loadFinished = true;
// Remove the footer from the listView.
listView.removeFooterView(footer);
}
};
需要注意的是,在线程中获取联系人数据时,我们是通过:
List<HashMap<String, String>> data = dataService.getContacts(currentPageIndex * 30, 30);
来实现,currentPageIndex * 30 是偏移量,因为我们要跳过已加载的数据项目。第二个参数30是指一次获取多少条项目。
另外,在getContacts()方法,中,我们使用:
String limit = "_id desc limit " + offset + " ," + maxValue;
Cursor cursor = resolver.query(uri, new String[] { "_id" }, null, null, limit);
来实现数据偏移,因为ContentResolver的query()方法内部也可通过组拼sql语句来实现的。