情境
AlertDialog 是我蠻喜歡使用的一個元件,它強大的彈性操作以及可以自由佈局的方式,幾乎在所有的 App 都可以看得到它的蹤影,它呈現的方式就是在原有的 Activity 上面彈跳出一個對話視窗,這個視窗可以根據參數的設定來選擇是要部分遮蓋或者全部遮蓋,是一個非常實用的工具。
不過有個缺點就是它會跟使用者直接互動,如果太常使用這個元件,會讓使用者體驗變差,建議是在重要訊息的時候,才使用這個元件,如果是一般提示訊息且不需要跟使用者互動,則可以考慮 Toast 元件。
完整程式碼
你可以直接到 GitHub 上面觀看或下載程式碼
程式碼說明
這邊會介紹幾種 AlertDialog 常用的模式,一般來說, AlertDialog 除了基本的模式以外,它還可以讓你自訂畫面來增加彈性,所以會使用五種內建的模式來當作範例,分別是一般對話框、條列式對話框、多選跟單選對話框,以及自訂對話框,如果你想要做成全螢幕的 Dialog,可以參照之前所寫的如何自訂 Dialog 之一以及如何自訂 Dialog 之二的範例。
一開始我們的佈局畫面就會顯示五個按鈕, 這五個按鈕分別是 AlertDialog 的每一種範例呈現的結果,在下面會一一介紹這些效果以及使用方法。

一般對話框
一般式的對話框會有三個按鈕,分別是 PositiveButton (確定) 、NeutralButton (中立) 以及 NegativeButton (拒絕) 三種,那這三種分別是由右至左,其中 NeutralButton 按鈕是讓你處理是跟否之間的模糊地帶,畢竟在某些選項並不是非黑即白的條件,如果你不需要其中的按鈕,只要不設定事件,預設就不會出現。

如上圖所示,我們設定了三種事件,因此會出現三個按鈕可以讓你選擇,這也是很常見的使用方法,下面是我們所操作元件的程式碼。
AlertDialog.Builder(this@MainActivity)
.setTitle(R.string.lunch_time)
.setMessage(R.string.want_to_eat)
.setPositiveButton(R.string.ok) { _, _ -> Toast.makeText(applicationContext, R.string.gogo, Toast.LENGTH_SHORT).show() }
.setNegativeButton(R.string.wait_a_minute) { _, _ -> Toast.makeText(applicationContext, R.string.i_am_hungry, Toast.LENGTH_SHORT).show() }
.setNeutralButton(R.string.not_hungry) { _, _ -> Toast.makeText(applicationContext, R.string.diet, Toast.LENGTH_SHORT).show() }
.show()
AlertDialog 是採取 Builder Pattern 所建置而成的,在每一個按鈕當中,可以根據對應的 Callback 來設定對應的事件,因此我們針對每一個 Callback 來跳出對應的 Toast 訊息。

條列式對話框
條列式對話框也是很常用的一種對話框,當我們在一般對話框的選項不夠用的時候,那麼就需要條列式對話框來輔助使用,它可以讓你加入更多種選項, 讓使用者多更多選擇可以點選,在這個例子當中我們就可以看到透過條列式對話框來選擇宵夜文。

當然這個條列式對話框也是會有事件可以處理的,所以當我們選擇按下某一個選項的時候,就會彈跳出所選擇的選項的提示訊息。

在程式碼當中我們可以看到必須先傳入一個陣列,為了方便塞入值,我們先透過 ArrayList 來塞(單純覺得 ArrayList 比較方便XD,你可以直接使用 Array 的方式來塞值)。
private lateinit var lunch: List<String>
//..
lunch = listOf(getString(R.string.lunch1),
getString(R.string.lunch2),
getString(R.string.lunch3),
getString(R.string.lunch4),
getString(R.string.lunch5),
getString(R.string.lunch6))
那可以看到我們透過 ArrayList 的 toArray 這個方法轉換成 Array,程式碼如下所示。
AlertDialog.Builder(this@MainActivity)
.setItems(lunch.toTypedArray()) { _, which ->
val name = lunch[which]
Toast.makeText(applicationContext, getString(R.string.u_eat) + name, Toast.LENGTH_SHORT).show()
}
.show()
單選式對話框
單選式對話框也是一種很實用的對話框,你會說這樣跟上面條列式對話框有什麼不一樣?差別在於條列式對話框沒有前面的 Radio Button 的按鈕,而且這個對話框可以更明確讓使用者感受到單選的意義。

程式碼如下所示。
AlertDialog.Builder(this@MainActivity)
.setSingleChoiceItems(lunch.toTypedArray(), singleChoiceIndex
) { _, which -> singleChoiceIndex = which }
.setPositiveButton(R.string.ok) { dialog, _ ->
Toast.makeText(this@MainActivity, "你選擇的是" + lunch[singleChoiceIndex], Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
.show()
從上面的程式碼會看到唯一跟條列是不同的地方是設定事件的時候,一個是透過 setItems 這個方法, 而我們單選式對話框是透過 setSingleChoiceItems 這個方法來進行操作,因為方法不同,所以傳入的參數也不一樣,從上面可看到我們除了傳入陣列以及覆寫的事件以外,還多了一個 int 的傳入參數,這個值可以從原始碼內查看,發現到它長這樣子。
public Builder setSingleChoiceItems(CharSequence[] items, int checkedItem, final OnClickListener listener)
參數名稱為 checkedItem,那麼我們就可以理解為這是被按下的按鈕,來實驗一下,我們傳入一個參數為 0 試看看,可以看到下方圖片預設選項會變成第一個雞腿飯了。

再試著傳入 2 看看,可以看到下方圖片預設選項變成第三個排骨飯了。

接著讓我們來處理按下去的事件,按下去會顯示結果的 Toast 彈跳出來。
AlertDialog.Builder(this@MainActivity)
.setSingleChoiceItems(lunch.toTypedArray(), singleChoiceIndex
) { _, which -> singleChoiceIndex = which }
.show()
但是這樣不是很好的感覺,應該要讓使用者能夠選完自己按下確定按鈕,才能夠算是選取結束, 因此我們要增加一個選好的按鈕讓使用者選取,上面的一般對話框也可以用在單選對話框內,而這個 setPositiveButton 方法會回傳一個 which 的參數,就可以知道使用者是否已經選取好以及選取到哪一個選項,因此我們只需要處理使用者按下確定的時候關閉 Dialog 就可以了。
AlertDialog.Builder(this@MainActivity)
.setSingleChoiceItems(lunch.toTypedArray(), singleChoiceIndex
) { _, _ -> }
.setPositiveButton(R.string.ok) { dialog, _ ->
Toast.makeText(this@MainActivity, "你選擇的是" + lunch[singleChoiceIndex], Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
.show()
你會看到多一個確定的按鈕在單選式對話框上面。

選擇第四個排骨飯來當作我們確定選擇的項目。

當按下確定按鈕以後就彈出 Toast 提示視窗顯示你所選取的項目。

這樣一來就可以解決按下去對話框卻沒有消失的問題,可是這時候你又會很納悶,為什麼當我按下去的時候,應該要把這個狀態紀住,結果卻還是第一個選項,因此我們必須記錄這個變數。
AlertDialog.Builder(this@MainActivity)
.setSingleChoiceItems(lunch.toTypedArray(), singleChoiceIndex
) { _, which -> singleChoiceIndex = which }
.setPositiveButton(R.string.ok) { dialog, _ ->
Toast.makeText(this@MainActivity, "你選擇的是" + lunch[singleChoiceIndex], Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
.show()
在前面我們說到第二個參數是預設勾選的變數,現在我們用 checkedIndex 這個變數傳入,等到使用者勾選以後,再變更這個變數,就達到儲存上次點選的結果了,如果再開啟一次就會看到值被記錄下來了。

多選式對話框
多選式對話框比較好玩的地方就是它可以選擇多個選項,用法跟單選式對話框類似,一樣有處理勾選後的方法,比較不同的是他除了可以多選以外也可以進行取消的動作。
AlertDialog.Builder(this@MainActivity)
.setMultiChoiceItems(lunch.toTypedArray(), BooleanArray(lunch.size)
) { _, which, isChecked ->
if (isChecked) {
Toast.makeText(this@MainActivity, "你勾選了" + lunch[which], Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this@MainActivity, "你取消勾選了" + lunch[which], Toast.LENGTH_SHORT).show()
}
}.show()
勾選第一個雞腿飯的選項, 跳出提示訊息表示你選了雞腿飯。

取消勾選第一個雞腿飯的選項,根據 onClick 的方法回傳回來該選項是否被選取,如果取消勾選則顯示該選項被取消的提示訊息。

但是其實這樣不方便,因為使用者做一次動作就顯示一次訊息,對於使用者體驗會感覺到很煩躁,再來就是當使用者要勾選多個項目的時候,再一次送出會是比較正常的做法,因此,我們可以參照單選式對話框的模式來增加確定的按鈕,程式碼如下。
val checkedStatusList = ArrayList<Boolean>()
for (s in lunch) {
checkedStatusList.add(false)
}
AlertDialog.Builder(this@MainActivity)
.setMultiChoiceItems(lunch.toTypedArray(), BooleanArray(lunch.size)
) { _, which, isChecked -> checkedStatusList[which] = isChecked }
.setPositiveButton("確定") { _, _ ->
val sb = StringBuilder()
checkedStatusList.forEachIndexed { index, b ->
if (b) {
sb.append(lunch[index])
sb.append(" ")
}
}
Toast.makeText(this@MainActivity, "你選擇的是$sb", Toast.LENGTH_SHORT).show()
}
.show()
在這邊我們使用了 setMultiChoiceItems 這個方法的 Callback 回傳了三個參數,可以看到它的原型長得如下面程式碼。
public Builder setMultiChoiceItems(CharSequence[] items, boolean[] checkedItems,
final OnMultiChoiceClickListener listener)
其中,第二個參數是傳入一個 boolean 的陣列, 這個是用來看哪些值要預設勾選或者不勾選,所以一開始先宣告一個陣列,預設值全部都設定為 false,這當中我們宣告了一個 List checkedStatusList 來存放哪幾個 CheckBox 被勾選,所以如果當有 item 被勾選的時候,我們就可以把 which 以及 isChecked 這兩個參數的值傳入 checkedStatusList 進行修改,如此一來,就可以根據這個陣列的值拿到所有有被勾選的 item 了。



如果你想保留狀態, 則只需要將 checkedStatusList 再放回 setMultiChoiceItems 第二個參數就可以了, 這時候你會發現, 如果沒有勾選任何選項會出現奇怪的訊息。

所以我們必須修改一下程式碼, 加入一個判斷的機制。
val checkedStatusList = ArrayList<Boolean>()
for (s in lunch) {
checkedStatusList.add(false)
}
AlertDialog.Builder(this@MainActivity)
.setMultiChoiceItems(lunch.toTypedArray(), BooleanArray(lunch.size)
) { _, which, isChecked -> checkedStatusList[which] = isChecked }
.setPositiveButton(R.string.ok) { _, _ ->
val sb = StringBuilder()
var isEmpty = true
checkedStatusList.forEachIndexed { index, b ->
if (b) {
sb.append(lunch[index])
sb.append(" ")
isEmpty = false
}
}
if (!isEmpty) {
Toast.makeText(this@MainActivity, "你選擇的是$sb", Toast.LENGTH_SHORT).show()
for (s in lunch) {
checkedStatusList.add(false)
}
} else {
Toast.makeText(this@MainActivity, "請勾選項目", Toast.LENGTH_SHORT).show()
}
}
.show()
這樣一來即便沒有勾選任何項目,按下確定以後也會跳出對應的訊息。

自製對話框
假設以上的對話框對你來說都沒辦法滿足你所需要的,那也沒關係,AlertDialog 允許你自己定義畫面,可以根據你想要的畫面寫進去 AlertDialog,所以我們最後一個按鈕就是設定來顯示自訂義的畫面,如下圖所示。

上面顯示一個對話框是由我們傳入的 View 所構成的,如果要自訂一個客製化的畫面,可以透過 AlertDialog 的 setView 方法,他只需要傳入一個宣告出來的 View 物件,就可以塞入至 AlertDialog 來呈現,所以一開始我們先建立好一個 Layout 方便我們進行操作,如下面所示。 ```xml <?xml version="1.0" encoding="utf-8"?> ``` 這個簡單的 Layout 就是上面所呈現的畫面,裡面的輸入框讓使用者能夠填入自己的名字,接著我們透過 LayoutInflater 來產生這個 View 的物件。
val item = LayoutInflater.from(this@MainActivity).inflate(R.layout.item_layout, null)
接著我們把這個 View 塞進去 AlertDialog 就會看到如上圖顯示,請使用者輸入名字後,按下確定就可以做對應的處理了。
AlertDialog.Builder(this@MainActivity)
.setTitle(R.string.input_ur_name)
.setView(item)
.setPositiveButton(R.string.ok) { _, _ ->
val editText = item.findViewById(R.id.edit_text) as EditText
val name = editText.text.toString()
if (TextUtils.isEmpty(name)) {
Toast.makeText(applicationContext, R.string.input_ur_name, Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(applicationContext, getString(R.string.hi) + name, Toast.LENGTH_SHORT).show()
}
}
.show()
如果輸入了自己的名字就會顯示名字的提示訊息,如下圖。


如果沒有輸入名字就按下確定,那麼也會進入另外一個判斷式,顯示對應的訊息。

這樣就是一個簡單的 AlertDialog 操作方式了。