1. 程式人生 > >Android ListView 實現分批載入

Android ListView 實現分批載入

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() 方法。