您的位置:首页 > 移动开发 > Android开发

Android ListView 实现分批加载

2016-10-23 14:09 260 查看
转载请注明出处: http://blog.csdn.net/like_program/article/details/52901492

ListView 想必大家都很熟悉了,当有大量数据需要显示时,通常不会一次性把数据全部加载显示出来,而是会先加载一部分,当用户滑动屏幕滑到最后一条数据时,再加载下一部分数据。也就是分批加载。

这篇博客将讲解如何实现 ListView 的分批加载数据。

首先来说下它的原理,其实原理很简单:

在 ListView 下滑的过程中,不停检查 ListView 有没有滑到底(数据源的最后一个数据是否可见),如果滑到底了,就加载下一批数据,并把加载的数据追加到 ListView 适配器的数据源中,然后调用 adapter.notifyDataSetChanged() ,就可以刷新界面,将新增的数据显示出来,从而达到分批加载数据的效果。

好了,原理我们已经知道了,那就来写个 Demo 体验一下。

打开 Android Studio,新建 ListViewLoadTest 项目。

新建 MySQLiteOpenHelper.java,继承自 SQLiteOpenHelper,在这个类中,我们将实现创建数据库和表的逻辑。

MySQLiteOpenHelper.java 代码如下所示:

package com.example.listviewloadtest;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class MySQLiteOpenHelper extends SQLiteOpenHelper {

/**
* 数据库名字
*/
private static final String DB_NAME = "database.db";

/**
* 数据库版本
*/
private static final int DB_VERSION = 1;

/**
* 建表语句
*/
private static final String CREATE_TABLE =
"create table dataList (" +
"_id integer primary key autoincrement, " +
"number varchar(20))";

public MySQLiteOpenHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}
}


代码很简单,就是创建了一个名字叫 database.db 的数据库,和名字叫 dataList 的表,_id 是表的主键,自增。number 就是我们等会要加载的数据。

我们再新建 Dao.java ,在这个类中,实现对数据库的操作。

Dao.java 代码如下所示:

package com.example.listviewloadtest;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import java.util.ArrayList;
import java.util.List;

public class Dao {

private static final String KEY_NUMBER = "number";

private static final String TABLE_NAME = "dataList";

public MySQLiteOpenHelper helper;

public Dao(Context context) {
helper = new MySQLiteOpenHelper(context);
}

/**
* 插入数据
*
* @param number 数字
* @return 执行结果
*          true 表示 执行成功
*          false 表示 执行失败
*/
public boolean add(String number) {
SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(KEY_NUMBER, number);
// 如果执行失败,会返回 -1
long rowId = db.insert(TABLE_NAME, null, values);
db.close();
if (rowId == -1) {
return false;
} else {
return true;
}
}

/**
* 分批加载
*
* @param startIndex 开始的位置
* @param maxCount   要加载的数据数量
* @return 新增的数据
*/
public List<String> loadMore(int startIndex, int maxCount) {
SQLiteDatabase db = helper.getReadableDatabase();
// limit 表示 限制当前有多少数据
// offset 表示 跳过,从第几条开始
// sql 语句含义:
// 假设 startIndex 为 19, maxCount 为 20:
// 查询从第 (19 + 1) = 20 条数据开始,往后的 20 条数据
Cursor cursor = db.rawQuery("select number from " + TABLE_NAME + " limit ? offset ?", new
String[]{String.valueOf(maxCount),
String.valueOf(startIndex)});
List<String> moreDataList = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
// 获取每行 "number" 那一列 的数据
String number = cursor.getString(cursor.getColumnIndex(KEY_NUMBER));
} while (cursor.moveToNext());
}
cursor.close();
db.close();
return moreDataList;
}

/**
* 获取数据总数
*
* @return 数据总数
*/
public int getTotalCount() {
int totalCount = -1;
SQLiteDatabase db = helper.getReadableDatabase();
// sql 语句含义:
// 获取 "number" 那一列数据的总数
Cursor cursor = db.rawQuery("select count(number) from " + TABLE_NAME, null);
if (cursor.moveToFirst()) {
totalCount = cursor.getInt(0);
}
cursor.close();
db.close();
return totalCount;
}
}


add() 方法是用来向表中插入数据,表中有了数据,我们才能从数据库取出数据,并显示在 ListView 上。

loadMore() 方法就是分批查询的重点了,每当 ListView 滑到底时,就调用这个方法,查询下一批数据,并把查询到的数据追加到 ListView 适配器的数据源中。

getTotalCount() 方法可以获取数据总数,也就是一共要加载多少数据,每当 ListView 滑到底时,先检查 ListView 有没有把数据加载完,如果当前 ListView 适配器的数据源中的数据少于数据总数,就调用 loadMore() 方法,否则不调用。

我们先添加下 ListView 等会要用到的数据。

在 ListViewLoadTest / app / src / androidTest / java / com.example.listviewloadtest 目录下新建 Test.java ,继承自 AndroidTestCase ,在这个类中,我们来添加数据,顺便测试下刚才写的 add() 方法是否可用。

Test.java 代码如下所示:

package com.example.listviewloadtest;

import android.test.AndroidTestCase;

public class Test extends AndroidTestCase {

public void testAdd() {
Dao dao = new Dao(getContext());
for (int i = 0; i < 100; i++) {
String number = String.valueOf(i + 1);
boolean add = dao.add(number);
// 断言,如果 add 为 true,就继续执行,
// 否则终止程序执行
assertEquals(true, add);
}
}
}




点击鼠标右键,选择
Run 'testAdd'
,可以看到 testAdd() 方法已经执行成功了。



那么我们再进入数据库看下,数据有没有被添加进去。

使用 adb 命令查询表,我们发现,数据已经被成功添加进去了。



好了,接下来我们就开始写界面了。

修改 activity_main.xml ,代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.listviewloadtest.MainActivity">

<LinearLayout
android:id="@+id/ll_progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">

<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="玩命加载中..."
android:textColor="@android:color/black"/>

</LinearLayout>

<ListView
android:id="@+id/lv_page_load"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</RelativeLayout>


布局很简单,就是在 ListView 的上方覆盖了一个旋转的进度条,默认是隐藏的。当我们在子线程中查询数据的时候,进度条就显示出来,查询完成后,进度条就隐藏。

再修改 MainActivity.java ,代码如下所示:

package com.example.listviewloadtest;

import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

/**
* 进度条
*/
private LinearLayout llProgress;

private ListView lvLoad;

/**
* 操作数据库
*/
private Dao dao;

/**
* 适配器的数据源
*/
private List<String> mDataList;

/**
* 下一批数据
*/
private List<String> mMoreData;

/**
* 下一批数据开始的位置
*/
private int mStartIndex = 0;

/**
* 下一批数据的数量
*/
private int mMaxCount = 20;

/**
* 数据总数
*/
private int mTotalCount = -1;

/**
* 适配器
*/
private ArrayAdapter<String> mAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initView();
initData();
}

/**
* 初始化视图
*/
private void initView() {
// 实例化控件
llProgress = (LinearLayout) findViewById(R.id.ll_progress);
lvLoad = (ListView) findViewById(R.id.lv_load);

// 设置滑动监听
lvLoad.setOnScrollListener(new AbsListView.OnScrollListener() {
// 滑动状态发生改变时回调
// SCROLL_STATE_IDLE 闲置状态,此时没有滑动
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case SCROLL_STATE_IDLE:
// 获取屏幕上可见的最后一项
int position = lvLoad.getLastVisiblePosition();
// 如果屏幕上可见的最后一项是当前适配器数据源的最后一项,
// 并且数据还没有加载完,就加载下一批数据。
if (position == mDataList.size() - 1 && position != mTotalCount - 1) {
mStartIndex += mMaxCount;
// 加载下一批数据
new LoadDataTask().execute();
} else if (position == mDataList.size() - 1 && position == mTotalCount -
1) {
Toast.makeText(MainActivity.this, "没有更多数据了", Toast.LENGTH_SHORT).show();
}
break;
}
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {

}
});
}

/**
* 初始化数据
*/
private void initData() {
dao = new Dao(MainActivity.this);
mDataList = new ArrayList<>();
mMoreData = new ArrayList<>();
// 加载第一批数据
new LoadDataTask().execute();
}

class LoadDataTask extends AsyncTask<Void, Void, List<String>> {

@Override
protected void onPreExecute() {
// 显示进度条
llProgress.setVisibility(View.VISIBLE);
}

@Override
protected List<String> doInBackground(Void... params) {
// 模拟耗时
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 查询一共有多少数据
if (mTotalCount == -1) {
mTotalCount = dao.getTotalCount();
}
// 分批加载
mMoreData = dao.loadMore(mStartIndex, mMaxCount);
return mMoreData;
}

@Override
protected void onPostExecute(List<String> strings) {
// 隐藏进度条
llProgress.setVisibility(View.GONE);
// 将新增数据追加到适配器的数据源中
mDataList.addAll(mMoreData);
if (mAdapter == null) {
mAdapter = new ArrayAdapter<>(MainActivity.this, android.R
.layout.simple_list_item_1, mDataList);
lvLoad.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
}
}
}


代码并不复杂,进入页面后,会调用 initView() 方法实例化控件,然后调用 initData() 方法初始化数据。

在 initData() 方法中,我们使用了 LoadDataTask 来加载数据。

在 LoadDataTask 的 onPreExecute() 方法中,先让进度条显示出来,为了模拟获取数据耗时,让进度条显示的时间更长一些,我们在 doInBackground() 方法 (子线程) 中让线程睡眠 2 秒种。

接着调用 dao.getTotalCount() 方法,获取一共要加载多少数据。然后我们去加载第一批数据,也就是前 20 个数据。

加载完成后,在 onPostExecute() 方法中,将 20 个数据追加到适配器的数据源中,并将数据源传入适配器,这样,第一批数据就加载出来了。

接着我们就要监听 ListView 的滑动了。在 initView() 方法中,我们给 ListView 设置了滑动监听,一旦 ListView 滑动到最后一项,ListView 就会停下(此时 scrollState 是 SCROLL_STATE_IDLE),这时会回调 onScrollStateChanged() 方法,进入 SCROLL_STATE_IDLE 的逻辑。

在 SCROLL_STATE_IDLE 的逻辑中,我们先获取当前屏幕可见的最后一项,然后判断当前屏幕可见的最后一项是不是当前适配器数据源的最后一项,如果是的话,说明 ListView 已经滑动到最底了,接着我们判断当前数据有没有加载完,如果还没加载完,就加载下一批,否则弹出一个 Toast,提示用户 “没有更多数据了”。

如果是加载下一批,mStartIndex 的值要修改为 下一批数据的第一项的索引。mMaxCount 的值不变,依然加载 20 条数据。接着使用 LoadDataTask 来加载数据,获取到下一批数据,把获取到的数据追加到适配器的数据源中,然后调用 mAdapter.notifyDataSetChanged() 来刷新页面,这样,我们就实现了分批加载。

运行一下项目:



源码下载

友情提示:

下载了源码的朋友,不要忘了运行 androidTest 目录下 Test.java 中的 testAdd() 方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: