課程 2: 在 Android 應(yīng)用中使用數(shù)據(jù)庫(kù)

這節(jié)課是 Android 開(kāi)發(fā)(入門(mén))課程 的第四部分《數(shù)據(jù)與數(shù)據(jù)庫(kù)》的第二節(jié)課,導(dǎo)師依然是 Jessica Lin 和 Katherine Kuan。這節(jié)課在掌握 SQLite 數(shù)據(jù)庫(kù)基本操作的前提下,在 Pets App 的最小可行性應(yīng)用 (MVP, Minimum Viable Product) 中使用 SQLite 數(shù)據(jù)庫(kù),主要參考 這篇 Android Developers 文檔,內(nèi)容包括

  • 使用 Contract 類(lèi)確定數(shù)據(jù)庫(kù)架構(gòu)。
  • 使用 SQLiteOpenHelper 類(lèi)創(chuàng)建、連接、管理數(shù)據(jù)庫(kù)。
  • 插入數(shù)據(jù)時(shí)使用的 ContentValues 類(lèi)。
  • 讀取數(shù)據(jù)時(shí)使用的 Cursor 對(duì)象。

關(guān)鍵詞:Contract Class、SQLiteOpenHelper、SQLiteDatabase 、ContentValues、Cursor、Floating Action Button、Spinner

Contract Class

在 Android 應(yīng)用中使用 SQLite 數(shù)據(jù)庫(kù),首先需要使用 Contract 類(lèi)確定數(shù)據(jù)庫(kù)架構(gòu) (schema)。所謂數(shù)據(jù)庫(kù)架構(gòu),可以理解為第一節(jié)課中提到的表格結(jié)構(gòu),主要是每一列的屬性及其對(duì)應(yīng)的存儲(chǔ)類(lèi)和限制信息等。Android 提供了 Contract 類(lèi)來(lái)保存數(shù)據(jù)庫(kù)架構(gòu),即保存表格的名稱(chēng)、每一列的屬性名等。這樣做的好處是,將數(shù)據(jù)庫(kù)架構(gòu)及其相關(guān)的常量統(tǒng)一放在一個(gè)類(lèi)內(nèi),容易訪問(wèn)和管理;同時(shí)在利用數(shù)據(jù)庫(kù)架構(gòu)的常量生成 SQL 指令時(shí),杜絕了錯(cuò)別字的可能性。

例如在 Pets App 中,通過(guò)內(nèi)部類(lèi) PetEntry 定義了表格的名稱(chēng)為字符串常量 pets,列 _id 作為數(shù)據(jù)庫(kù)表格的 ID,寵物名字的列為字符串常量 name 等。

In java/com.example.android.pets/data/PetContract.java

/**
 * Inner class that defines constant values for the pets database table.
 * Each entry in the table represents a single pet.
 */
public static final class PetEntry implements BaseColumns {

    /** Name of database table for pets */
    public final static String TABLE_NAME = "pets";

    /**
     * Unique ID number for the pet (only for use in the database table).
     *
     * Type: INTEGER
     */
    public final static String _ID = BaseColumns._ID;

    /**
     * Name of the pet.
     *
     * Type: TEXT
     */
    public final static String COLUMN_PET_NAME ="name";

    /**
     * Possible values for the gender of the pet.
     */
    public static final int GENDER_UNKNOWN = 0;
    public static final int GENDER_MALE = 1;
    public static final int GENDER_FEMALE = 2;   
}
  1. 注意 PetContract 文件放在應(yīng)用包名 (com.example.android.pets) 下單獨(dú)的 data 包內(nèi),與 Activity 文件區(qū)分開(kāi)。這是因?yàn)?Android 是通過(guò)包 (package) 來(lái)組織代碼的,一類(lèi)包存放一類(lèi)特定功能的代碼,例如 data 包下就存放數(shù)據(jù)相關(guān)的代碼。

  2. 在調(diào)用 PetContract 中 PetEntry 的常量時(shí),需要通過(guò) PetContract.PetEntry 指定,例如

     PetContract.PetEntry.GENDER_MALE
    

    這是因?yàn)檎{(diào)用處導(dǎo)入的包名默認(rèn)為外部類(lèi) PetContract

     import com.example.android.pets.data.PetContract;
    

    為了精簡(jiǎn)代碼,可以將包名指向 PetContract 的內(nèi)部類(lèi)

     import com.example.android.pets.data.PetContract.PetEntry;
    

    這樣在調(diào)用其常量時(shí),就可以省略 PetContract

     PetEntry.GENDER_MALE
    

    這種方法適合在 Contract 類(lèi)只有一個(gè)表格的情況下使用。

  3. PetContract 類(lèi)要定義為 final 使之不能被擴(kuò)展 (extends),因?yàn)樗粚?duì)外提供常量,不實(shí)現(xiàn)任何功能。

  4. 對(duì)于用 INETGER 來(lái)存儲(chǔ)固定選項(xiàng)的數(shù)據(jù),其對(duì)應(yīng)的規(guī)則常量也在此定義。例如上面的寵物性別信息,0 表示未知,1 表示雄性,2 表示雌性。

SQLiteOpenHelper

在 Contract 類(lèi)確定數(shù)據(jù)庫(kù)架構(gòu)后,使用 Android 提供的 SQLiteOpenHelper 類(lèi)來(lái)創(chuàng)建、連接、管理數(shù)據(jù)庫(kù)。SQLiteOpenHelper 可以看成是應(yīng)用與數(shù)據(jù)庫(kù)之間的橋梁。

SQLiteOpenHelper 是一個(gè)抽象類(lèi),實(shí)現(xiàn)時(shí)需要 override onCreateonUpgrade method。例如在 Pets App 中,創(chuàng)建一個(gè) PetDbHelper 類(lèi),擴(kuò)展自 SQLiteOpenHelper。注意文件路徑與 PetContract 的相同,表示 PetDbHelper 類(lèi)屬于數(shù)據(jù)功能的代碼。

In java/com.example.android.pets/data/PetDbHelper.java

public class PetDbHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "shelter.db";
    private static final int DATABASE_VERSION = 1;

    public PetDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        // Create a String that contains the SQL statement to create the pets table
        String SQL_CREATE_PETS_TABLE =  "CREATE TABLE " + PetEntry.TABLE_NAME + " ("
                + PetEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
                + PetEntry.COLUMN_PET_NAME + " TEXT NOT NULL, "
                + PetEntry.COLUMN_PET_BREED + " TEXT, "
                + PetEntry.COLUMN_PET_GENDER + " INTEGER NOT NULL, "
                + PetEntry.COLUMN_PET_WEIGHT + " INTEGER NOT NULL DEFAULT 0);";

        // Execute the SQL statement
        db.execSQL(SQL_CREATE_PETS_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // The database is still at version 1, so there's nothing to do be done here.
    }
}
  1. 首先定義兩個(gè)常量,分別表示數(shù)據(jù)庫(kù)的名稱(chēng),不要忘記后綴 .db;以及數(shù)據(jù)庫(kù)版本,初始值默認(rèn)為 1。
  2. 構(gòu)造函數(shù)調(diào)用超級(jí)類(lèi),輸入?yún)?shù)分別為
    (1)Context,應(yīng)用環(huán)境,執(zhí)行構(gòu)造函數(shù)時(shí)傳入;
    (2)數(shù)據(jù)庫(kù)名稱(chēng),傳入在外部類(lèi)定義的常量;
    (3)CursorFactory,設(shè)為 null 以使用默認(rèn)值;
    (4)數(shù)據(jù)庫(kù)版本,傳入在外部類(lèi)定義的常量。
  3. override onCreate method 添加創(chuàng)建數(shù)據(jù)庫(kù)時(shí)的指令,其中調(diào)用了 SQLiteDatabase 的 execSQL method 執(zhí)行上面生成的 CREATE TABLE SQL 指令(存為字符串),實(shí)現(xiàn)創(chuàng)建數(shù)據(jù)庫(kù)的操作。值得注意的是,由于 execSQL 無(wú)返回值,所以它不能用來(lái)執(zhí)行如 SELECT 等帶有返回值的 SQL 指令。
  4. override onUpgrade method 添加數(shù)據(jù)庫(kù)版本發(fā)生變化時(shí)的指令,例如 SQLite 數(shù)據(jù)庫(kù)表格增加了一列,數(shù)據(jù)庫(kù)版本變更,可以在 onUpgrade 使用 execSQL 執(zhí)行 DROP TABLE SQL 指令,刪除舊表格后再調(diào)用 onCreate 創(chuàng)建新數(shù)據(jù)庫(kù)。不過(guò)在這里,目前數(shù)據(jù)庫(kù)版本保持不變,所以 onUpgrade 暫時(shí)留空,這部分內(nèi)容將在后續(xù)課程修改。

實(shí)現(xiàn) SQLiteOpenHelper 這個(gè)抽象類(lèi)后,就可以通過(guò)它來(lái)創(chuàng)建、連接、管理 SQLiteDatabase 數(shù)據(jù)庫(kù)了。例如在 Pets App 中,首先創(chuàng)建 SQLiteOpenHelper 實(shí)例。

In CatalogActivity.java

PetDbHelper mDbHelper = new PetDbHelper(this);

調(diào)用 PetDbHelper 的構(gòu)造函數(shù),傳入 this 即當(dāng)前 Activity 的環(huán)境。獲得 PetDbHelper 實(shí)例后,調(diào)用 getReadableDatabase() 創(chuàng)建或連接 SQLiteDatabase 數(shù)據(jù)庫(kù)。

SQLiteDatabase db = mDbHelper.getReadableDatabase();

這條指令相當(dāng)于 sqlite3 的 .open 指令,用于打開(kāi)或創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)文件。具體來(lái)說(shuō),應(yīng)用在調(diào)用 getReadableDatabase() 時(shí),PetDbHelper 會(huì)查詢當(dāng)前是否已存在數(shù)據(jù)庫(kù),若無(wú),PetDbHelper 會(huì)執(zhí)行其 onCreate method 創(chuàng)建數(shù)據(jù)庫(kù),返回一個(gè) SQLiteDatabase 對(duì)象;若有則不執(zhí)行 onCreate method,直接創(chuàng)建一個(gè) SQLiteDatabase 對(duì)象,與已有的數(shù)據(jù)庫(kù)連接。因此,無(wú)論應(yīng)用是否已存在數(shù)據(jù)庫(kù),調(diào)用 getReadableDatabase() 的結(jié)果都是獲得一個(gè)連接數(shù)據(jù)庫(kù)的 SQLiteDatabase 對(duì)象,通過(guò)它來(lái)操作數(shù)據(jù)庫(kù)。

不過(guò)事實(shí)上,通過(guò) getReadableDatabase() 獲得的 SQLiteDatabase 對(duì)象,只能對(duì)數(shù)據(jù)庫(kù)進(jìn)行讀取 (Read) 操作,如果想要 CRUD 的其余操作,需要調(diào)用 getWritableDatabase() 獲取 SQLiteDatabase 對(duì)象。

ContentValues

在獲得一個(gè)連接數(shù)據(jù)庫(kù)的 SQLiteDatabase 對(duì)象后,就可以通過(guò)它對(duì)數(shù)據(jù)庫(kù)進(jìn)行 CRUD 操作,其中經(jīng)常用到 ContentValues 構(gòu)造數(shù)據(jù)。ContentValues 是一個(gè)具象類(lèi),用于存儲(chǔ)大量的鍵/值對(duì),對(duì)于數(shù)據(jù)庫(kù)而言,鍵是對(duì)象的屬性,值是對(duì)應(yīng)的屬性值。例如在 Pets App 中,創(chuàng)建一個(gè) ContentValues 對(duì)象,并通過(guò) put method 添加鍵/值對(duì)。

String nameString = mNameEditText.getText().toString().trim();
String breedString = mBreedEditText.getText().toString().trim();
String weightString = mWeightEditText.getText().toString().trim();
int weight = Integer.parseInt(weightString);

ContentValues values = new ContentValues();
values.put(PetEntry.COLUMN_PET_NAME, nameString);
values.put(PetEntry.COLUMN_PET_BREED, breedString);
values.put(PetEntry.COLUMN_PET_GENDER, mGender);
values.put(PetEntry.COLUMN_PET_WEIGHT, weight);

long newRowId = db.insert(PetEntry.TABLE_NAME, null, values);
  1. 添加到 ContentValues 對(duì)象的鍵為 Contract 類(lèi)中定義的架構(gòu)列常量,對(duì)應(yīng)的值為從 EditText 獲取的用戶輸入值。
  2. toString 后調(diào)用 trim 去除字符串開(kāi)頭和結(jié)尾的多余空格。
  3. 調(diào)用 Integer.parseInt() 使字符串轉(zhuǎn)換為整數(shù)。

構(gòu)造好往數(shù)據(jù)庫(kù)添加的數(shù)據(jù)后,調(diào)用 SQLiteDatabase 的 insert method 向數(shù)據(jù)庫(kù)添加數(shù)據(jù)。輸入?yún)?shù)分別為

  1. table: 要添加數(shù)據(jù)的表格名稱(chēng)。
  2. nullColumnHack: 可選參數(shù),可設(shè)為 null,僅在往數(shù)據(jù)庫(kù)添加空行時(shí)用到。
  3. values: 要往數(shù)據(jù)庫(kù)添加的數(shù)據(jù),數(shù)據(jù)類(lèi)型為 ContentValues 對(duì)象。

SQLiteDatabase 的 insert method 返回值為新添加的行 ID,發(fā)生錯(cuò)誤時(shí)返回 -1。

Tips:
1. 調(diào)用 finish() method 可以關(guān)閉當(dāng)前 Activity,使屏幕界面回到原先 Activity。
2. 根據(jù) Activity 的生命周期,在 onStart 中添加的代碼,可以在用戶通過(guò) Activity 切換回來(lái)時(shí)執(zhí)行。

Cursor

與往數(shù)據(jù)庫(kù)添加數(shù)據(jù)類(lèi)似,SQLiteDatabase 也提供了讀取數(shù)據(jù)的方法,例如通過(guò) rawQuery 傳入 SQL 指令讀取數(shù)據(jù),不過(guò)還有更規(guī)范的 query method,可以避免直接操作 SQL 指令導(dǎo)致的語(yǔ)法錯(cuò)誤。SQLiteDatabase 提供了很多不同的 query method,其中較簡(jiǎn)單的為

Cursor query (String table, 
              String[] columns, 
              String selection, 
              String[] selectionArgs, 
              String groupBy, 
              String having, 
              String orderBy)

有七個(gè)輸入?yún)?shù),分別為

  1. table
    想要讀取的表格名稱(chēng)。
  2. columns
    想要讀取的列數(shù)據(jù),傳入 null 表示讀取所有列,但是不建議這么做,尤其是數(shù)據(jù)庫(kù)很龐大時(shí)會(huì)消耗大量系統(tǒng)資源。
  3. selection
    讀取數(shù)據(jù)的篩選條件,相當(dāng)于 SQL 指令的 WHERE 條件,傳入 null 表示無(wú)篩選條件,返回所有數(shù)據(jù)行。
  4. selectionArgs
    與 selection 配合使用,當(dāng) selection 包含 "?" 時(shí),selectionArgs 數(shù)組的字符串會(huì)填入相應(yīng)的位置,作為讀取數(shù)據(jù)的篩選條件。這種模式屬于防止 SQL 注入的安全措施。
  5. groupBy
    讀取的數(shù)據(jù)行的分組條件,相當(dāng)于 SQL 指令的 GROUP BY 條件,傳入 null 表示數(shù)據(jù)行不分組。
  6. having
    在讀取的數(shù)據(jù)行通過(guò) groupBy 分組的情況下,指定哪一組包含 Cursor 內(nèi),相當(dāng)于 SQL 指令的 HAVING 條件,傳入 null 表示所有組包含 Cursor 內(nèi)。另外,當(dāng) groupBy 為 null,having 也要傳入 null。
  7. orderBy
    讀取的數(shù)據(jù)行的排序方法,相當(dāng)于 SQL 指令的 ORDER BY 條件,傳入 null 表示數(shù)據(jù)行保持默認(rèn)排序。

例如 這篇 Android Developers 文檔 中的例子,它讀取的數(shù)據(jù)是 ID、標(biāo)題、副標(biāo)題三列,標(biāo)題為 "My Title",按照副標(biāo)題降序排列的,不分組的。其中 selection 中的 "= ?" 可以是一個(gè)等號(hào),也可以是兩個(gè)等號(hào)。

SQLiteDatabase db = mDbHelper.getReadableDatabase();

// Define a projection that specifies which columns from the database
// you will actually use after this query.
String[] projection = {
    FeedEntry._ID,
    FeedEntry.COLUMN_NAME_TITLE,
    FeedEntry.COLUMN_NAME_SUBTITLE
    };

// Filter results WHERE "title" = 'My Title'
String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?";
String[] selectionArgs = { "My Title" };

// How you want the results sorted in the resulting Cursor
String sortOrder = FeedEntry.COLUMN_NAME_SUBTITLE + " DESC";

Cursor cursor = db.query(
    FeedEntry.TABLE_NAME,      // The table to query
    projection,                // The columns to return
    selection,                 // The columns for the WHERE clause
    selectionArgs,             // The values for the WHERE clause
    null,                      // don't group the rows
    null,                      // don't filter by row groups
    sortOrder                  // The sort order
    );

SQLiteDatabase 的所有 query method 的返回值都是一個(gè) Cursor 對(duì)象,它保存了讀取到的數(shù)據(jù)庫(kù)的多行內(nèi)容。在讀取數(shù)據(jù)時(shí),Cursor 可以看成是應(yīng)用與 SQLiteDatabase 之間的橋梁。

Cursor 的一個(gè)重要概念是當(dāng)前行的位置。在 Cursor 沒(méi)有數(shù)據(jù)時(shí),它的默認(rèn)位置為無(wú)效的 -1,所以與 Array 類(lèi)似,Cursor 的首個(gè)可用位置為 0,隨后逐步遞增。Cursor 提供了 move method 用于移動(dòng)當(dāng)前行的位置,以獲取指定行的數(shù)據(jù),常用的有

Method Description
moveToFirst () 使 Cursor 當(dāng)前行的位置移至首行
moveToLast () 使 Cursor 當(dāng)前行的位置移至末行
moveToNext () 使 Cursor 當(dāng)前行的位置下移一行
moveToPosition (int position) 使 Cursor 當(dāng)前行的位置移至指定行,-1 <= position <= count
moveToPrevious () 使 Cursor 當(dāng)前行的位置返回到原來(lái)那一行

Cursor 的 move method 的返回值類(lèi)型都是布爾類(lèi)型,移動(dòng)成功為 true,失敗則 false。例如 Cursor 當(dāng)前行為最后一行時(shí)調(diào)用 moveToNext 的返回值即 false。

Cursor 還提供很多 getter method 用于獲取不同類(lèi)型的數(shù)據(jù),例如 getInt()、getString、getType 等,它們的輸入?yún)?shù)都是 columnIndex 列索引,這是由 projection 決定的,通過(guò) getColumnIndex 獲得。

使用完 Cursor 后,一定要調(diào)用 close method 關(guān)閉 Cursor,防止內(nèi)存泄漏。

以 Pets App 為例

try {
    displayView.setText("The pets table contains " + cursor.getCount() + " pets.\n\n");
    displayView.append(PetEntry._ID + " - " +
            PetEntry.COLUMN_PET_NAME + " - " +
            PetEntry.COLUMN_PET_BREED + " - " +
            PetEntry.COLUMN_PET_GENDER + " - " +
            PetEntry.COLUMN_PET_WEIGHT + "\n");

    int idColumnIndex = cursor.getColumnIndex(PetEntry._ID);
    int nameColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_NAME);
    int breedColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_BREED);
    int genderColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_GENDER);
    int weightColumnIndex = cursor.getColumnIndex(PetEntry.COLUMN_PET_WEIGHT);

    while (cursor.moveToNext()) {
        int currentID = cursor.getInt(idColumnIndex);
        String currentName = cursor.getString(nameColumnIndex);
        String currentBreed = cursor.getString(breedColumnIndex);
        int currentGender = cursor.getInt(genderColumnIndex);
        int currentWeight = cursor.getInt(weightColumnIndex);

        displayView.append(("\n" + currentID + " - " +
                currentName + " - " +
                currentBreed + " - " +
                currentGender + " - " +
                currentWeight));
    }
} finally {
    cursor.close();
}
  1. 使用 append 為 TextView 添加內(nèi)容。
  2. 使用 getColumnIndex 獲取每一列的索引。
  3. moveToNext 放入 while 循環(huán)語(yǔ)句中達(dá)到遍歷所有數(shù)據(jù)行的效果。這是因?yàn)檎G闆r下,moveToNext 的返回值為 true 即進(jìn)入循環(huán);直到 Cursor 在最后一行時(shí)調(diào)用 moveToNext 的返回值為 false 即跳出循環(huán)。
  4. 通過(guò)不同的 getter method 獲取不同類(lèi)型的數(shù)據(jù)。
  5. 將上述代碼放入 try/finally 區(qū)塊內(nèi),即使應(yīng)用崩潰,也能保證執(zhí)行 close method 關(guān)閉 Cursor。

如何提取應(yīng)用的數(shù)據(jù)庫(kù)文件

在 Android Studio 界面右上角搜索 Device File Explorer 打開(kāi)模擬器的文件瀏覽器,打開(kāi)目錄 data > data > com.example.android.pets > databases 即可查看應(yīng)用內(nèi)的數(shù)據(jù)庫(kù)文件。右鍵選擇 "Save As..." 將數(shù)據(jù)庫(kù)文件提取到電腦中,即可通過(guò)終端訪問(wèn)。

Note:
1. 此方法僅適用于模擬器或具有最高權(quán)限 (rooted) 的物理設(shè)備。
2. 清除應(yīng)用的緩存 (cache) 和數(shù)據(jù) (data) 會(huì)刪除應(yīng)用的數(shù)據(jù)庫(kù)文件。

Floating Action Button

Pets App 使用了 FloatingActionButton 連接兩個(gè) Activity,它是一個(gè)懸浮在 UI 之上的圓形按鈕,有獨(dú)特的顯示和交互效果。

In activity_catalog.xml

<android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentBottom="true"
    android:layout_alignParentRight="true"
    android:layout_margin="@dimen/fab_margin"
    android:src="@drawable/ic_add_pet"/>

將 FloatingActionButton 的 ID 設(shè)置為 fab,圓形按鈕的位置放在屏幕的右下角,距離邊緣 16dp。由于 FloatingActionButton 是 ImageView 的子類(lèi),所以其顯示圖標(biāo)可以通過(guò) android:src 屬性設(shè)置;其余特性與 Button 類(lèi)似,例如在 Pets App 中,設(shè)置其 OnClickListener 動(dòng)作為打開(kāi) EditorActivity。

In CatalogActivity.java

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

    // Setup FAB to open EditorActivity
    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Intent intent = new Intent(CatalogActivity.this, EditorActivity.class);
            startActivity(intent);
        }
    });

    ...
}
Spinner

Pets App 使用了 Spinner 作為輸入寵物性別的選項(xiàng),它是一個(gè)單選菜單,默認(rèn)狀態(tài)下會(huì)顯示默認(rèn)值和一個(gè)向下箭頭的圖標(biāo)。

In activity_editor.xml

<Spinner
    android:id="@+id/spinner_gender"
    android:layout_height="48dp"
    android:layout_width="wrap_content"
    android:paddingRight="16dp"
    android:spinnerMode="dropdown"/>

將 Spinner 的 ID 設(shè)置為 spinner_gender,通過(guò) android:spinnerMode 屬性設(shè)置用戶在點(diǎn)擊 Spinner 時(shí)的展開(kāi)方式是默認(rèn)的下拉菜單 (dropdown) 還是彈出對(duì)話框 (dialog)。

In EditorActivity.java

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

    ...

    mGenderSpinner = (Spinner) findViewById(R.id.spinner_gender);
    setupSpinner();
}

/**
 * Setup the dropdown spinner that allows the user to select the gender of the pet.
 */
private void setupSpinner() {
    // Create adapter for spinner. The list options are from the String array it will use
    // the spinner will use the default layout
    ArrayAdapter genderSpinnerAdapter = ArrayAdapter.createFromResource(this,
            R.array.array_gender_options, android.R.layout.simple_spinner_item);

    // Specify dropdown layout style - simple list view with 1 item per line
    genderSpinnerAdapter.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line);

    // Apply the adapter to the spinner
    mGenderSpinner.setAdapter(genderSpinnerAdapter);

    // Set the integer mSelected to the constant values
    mGenderSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
            String selection = (String) parent.getItemAtPosition(position);
            if (!TextUtils.isEmpty(selection)) {
                if (selection.equals(getString(R.string.gender_male))) {
                    mGender = PetEntry.GENDER_MALE;
                } else if (selection.equals(getString(R.string.gender_female))) {
                    mGender = PetEntry.GENDER_FEMALE;
                } else {
                    mGender = PetEntry.GENDER_UNKNOWN;
                }
            }
        }

        // Because AdapterView is an abstract class, onNothingSelected must be defined
        @Override
        public void onNothingSelected(AdapterView<?> parent) {
            mGender = PetEntry.GENDER_UNKNOWN;
        }
    });
}
  1. onCreate 通過(guò) findViewById 找到 Spinner 對(duì)象,并通過(guò)輔助方法 setupSpinner() 設(shè)置 Spinner 適配器和監(jiān)聽(tīng)器。
  2. Spinner 適配器設(shè)置為一個(gè) ArrayAdapter,通過(guò)其靜態(tài)方法 createFromResource 創(chuàng)建,輸入?yún)?shù)分別為應(yīng)用環(huán)境 (Context)、Array 資源 ID、布局資源 ID。其中,布局資源 ID 使用 Android 提供的默認(rèn)布局 simple_spinner_item;Array 資源 ID 則是在應(yīng)用內(nèi)定義的資源。

In res/values/arrays.xml

<resources>
    <!-- These are the options displayed in the gender drop-down Spinner -->
    <string-array name="array_gender_options">
        <item>@string/gender_unknown</item>
        <item>@string/gender_male</item>
        <item>@string/gender_female</item>
    </string-array>
</resources>
  1. 創(chuàng)建 Spinner 的 ArrayAdapter 適配器后,調(diào)用 setDropDownViewResource 設(shè)置菜單的布局,其中 Android 提供的布局 simple_dropdown_item_1line 為每行顯示一個(gè)項(xiàng)目的 ListView。

  2. Spinner 的 AdapterView.OnItemSelectedListener 需要 override 兩個(gè)方法,通過(guò) onItemSelected 設(shè)置用戶選擇不同項(xiàng)目時(shí)的對(duì)應(yīng)指令,其中應(yīng)用了 TextUtils.isEmpty 來(lái)判斷當(dāng)前選項(xiàng)是否為空,增強(qiáng)代碼的魯棒性;另外,通過(guò) onNothingSelected 設(shè)置沒(méi)有項(xiàng)目選中時(shí)的指令。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容