Androidのダイアログを作ろう - サンプルコードで学ぶAlertDialogの使い方の基本からカスタマイズ
Androidでの通知の仕組み「Dialog」の代表格であるAlertDialogには、ユーザーに選択を促す場合以外に、自由度の高い画面を作れる機能があります。この記事では、サンプルコードとともにAlertDialog.Builderクラスを使って用途別にダイアログを構成する情報を設定する方法、カスタマイズを行う方法について説明します。
PCでもモバイルでもWebのサービスでも、ユーザーに重要な情報を伝えたいときや選択肢から選ばせたい場合などに、「ダイアログ」というUIが使われます。Androidでは、ユーザーに対する通知の仕組みとして「Snackbar」「Toast」「Dialog」などが用意されています。
コンポーネント | 主な用途 |
---|---|
Snackbar | ユーザーの行動を阻害せず、自動的に消える(メールを送信しました、など) |
Toast | Snackbarが使えない場合(画面が閉じたあとにメッセージを表示する、など) |
Dialog | ユーザーに強制的に行動させる(規約の同意、などユーザーになんらかのアクションを求める) |
SnackbarやToastはシンプルなAPIを持っているのですが、Dialogの代表格であるAlertDialogには、ユーザーに選択を促す場合以外にも自由度の高い画面を作ることのできる機能があります。この記事では、サンプルコードとともにAlertDialog.Builderクラスを使って用途別にダイアログを構成する情報を設定する方法、さまざまなカスタマイズを行う方法について説明します。
本記事では、macOS Catalina(10.15.2)/Android Studio 3.5.3/Kotlin 1.3.61/BuildToolsVersion 29.0.2で動作確認を行っています。
AlertDialogの簡易的な使い方
それでは早速、AlertDialogを使って簡単なダイアログを表示してみましょう。
ダイアログといえば次のような画面をイメージするのではないでしょうか。
▲基本的なコンポーネントを指定したAlertDialog
AlertDialogは、それ自身を直接インスタンス化して使うこともできますが、多くの項目を設定できるため、Builderクラスを使って値を設定するやり方が一般的です。
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) setupViews() } private fun setupViews() { // TextViewがタップされたときにダイアログを表示 showAlertDialogTextView.setOnClickListener { // BuilderからAlertDialogを作成 val dialog = AlertDialog.Builder(this) .setTitle(R.string.title) // タイトル .setMessage(R.string.message) // メッセージ .setPositiveButton(R.string.ok) { dialog, which -> // OK Toast.makeText(context, "OKがタップされた", Toast.LENGTH_SHORT).show() } .create() // AlertDialogを表示 dialog.show() } }
上記のように、タイトル、メッセージ、そしてOKボタンを表示させるダイアログは、Builderクラスのそれぞれのsetterメソッドを使用して値を設定していきます。titleやmessageのように文字列を設定するメソッドについては、文字列リソースのIDを指定しますが、通常の文字列(String)を指定することもできます。文字列リソースはリソースフォルダ以下のXMLファイルに定義します。
<resources> <string name="title">タイトル</string> <string name="message">メッセージ</string> <string name="ok">OK</string> <!-- 省略 --> </resources>
またボタンについては、表示する文字列(文字列リソース)とタップしたときの処理(OnClickListener)を設定します。
この例ではAlertDialogの作成(create)と表示(show)を別々に行っていますが、Builderのshowメソッドを使うことで、生成と表示をまとめて実行できます。
ダイアログを構成する3つの要素
さて、ダイアログは3つの要素から成り立っています。「タイトル」「コンテンツ領域」「アクションボタン」です。ダイアログの使いみちとして、ユーザーに追加情報の入力を促す(コンテンツ領域)、ユーザーへ意思決定を促す(アクションボタン)などがありますが、目的に応じて何をどのように表示させるか、ユーザーの使い勝手をよく考えてデザインするようにしてください。
要素 | 概要 |
---|---|
タイトル | ダイアログのタイトル。コンテンツ領域がリストの場合や複雑な内容を表示している場合に指定する。省略可 |
コンテンツ領域 | 単純な文字列、リスト、カスタムビューなどが表示される |
アクションボタン | ユーザーからのアクションを受け取るためのボタンが表示される。最大3つ |
DialogFragmentをダイアログのコンテナとして使用する
AlertDialogでは、単独でshow()メソッドを呼び出してやれば、ダイアログを表示できます。これは動作確認を行う上ではとても楽ちんなのですが、画面の回転と合わせてダイアログも回転させたい場合などには、Activityとの間にDialogFragmentを導入する必要があります。
FragmentはActivityに複数配置できる、いわば「断片的なActivity」で、UIと振る舞いを記述できます。Activityは、startActivityメソッドで表示を開始しますが、具体的に画面を作成する(onCreate)、画面に表示される(onResume)、一時停止状態になる(onPause)、破棄される(onDestroy)などのメソッドをオーバーライドすることで、それぞれのタイミングで行いたい処理を記述します。Fragmentも同様のメソッドを持っており、Activityと連動してそれぞれのタイミングで必要な処理を実装します。ダイアログを表示するときに、これらのタイミングに合わせてダイアログの表示を管理する場合には、DialogFragmentを用いることになります。
では、具体的な例も見てみましょう。
// DialogFragmentを継承したクラス class SimpleAlertDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = // AlertDialogの作成 AlertDialog.Builder(requireContext()) .setTitle(R.string.title) .setMessage(R.string.message) .setIcon(android.R.drawable.btn_star_big_on) .setPositiveButton(R.string.ok) { dialog, which -> // ボタンがタップされたときの処理 Toast.makeText(context, "OKがタップされた", Toast.LENGTH_SHORT).show() } .create() }
DialogFragmentをダイアログのコンテナとして利用する場合には、DialogFragment#onCreateDialog()メソッドでAlertDialog.Builderによって作成したDialog(AlertDialog)のインスタンスを返すようにするだけです。上のサンプルでも、DialogFragmentを継承したクラスを作成しています。
DialogFragmentはAndroidXに含まれるもの、またはサポートライブラリに含まれるものを使用してください。標準ライブラリに含まれるandroid.app.DialogFragmentは現在は非推奨となっています。
DialogFragmentを継承したクラスを使ってダイアログを表示するには、DialogFragment#show()メソッドに、FragmentActivity#getSupportFragmentManager()で取得したFragmentManagerとタグを渡します。
class MainActivity : AppCompatActivity() { // 省略 private fun setupViews() { showAlertDialogTextView.setOnClickListener { // DialogFragment#show()メソッドでダイアログを表示 SimpleAlertDialogFragment().apply { show(supportFragmentManager, "SimpleAlertDialogFragment") } } // 省略 } }
アクションボタン
AlertDialogでは、アクションボタンとして最大で3つまで表示することができます。それらは肯定(Positive)、否定(Negative)、中立(Neutral)の3つで、ダイアログの役割である「ユーザーによる意思決定」を取得するために活用されています。
▲3つのボタンを表示
ボタンを表示させ、それぞれのボタンがタップされた時に何かの処理を行うには、次のように記述します。
class ButtonsAlertDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext()) .setTitle(R.string.title) .setMessage(R.string.message) // あなたは毎朝朝食を食べますか? .setPositiveButton(R.string.yes) { dialog, which -> // Positiveボタンがタップされたときに実行される処理 Toast.makeText(context, "「はい」がタップされた", Toast.LENGTH_SHORT).show() } .setNegativeButton(R.string.no) { dialog, which -> // Negativeボタンがタップされたときに実行される処理 Toast.makeText(context, "「いいえ」がタップされた", Toast.LENGTH_SHORT).show() } .setNeutralButton(R.string.no_answer) { dialog, which -> // Neutralボタンがタップされたときに実行される処理 Toast.makeText(context, "「無回答」がタップされた", Toast.LENGTH_SHORT).show() } .create() }
各ボタンを設定するメソッドの2番目の引数には、DialogInterface.OnClickListenerの実装インスタンスが渡されます。ここでは、Kotlinの記法で簡略化していますが、これを簡略化せずに記述すると次のようになります。
AlertDialog.Builder(context) // 省略 .setPositiveButton(R.string.yes, object : DialogInterface.OnClickListener { override fun onClick(dialog: DialogInterface?, which: Int) { Toast.makeText(context, "「はい」がタップされた", Toast.LENGTH_SHORT).show() } })
ボタンは表示させたいが何も特別な処理は記述しないという場合には、setXxxButtonの2番めの引数(OnClickListenerのインスタンス)としてnullを渡します。
リストの表示
AndroidのライブラリにはDatePickerDialogという日付を選択するためのダイアログが標準で用意されていますが、その他にも、何らかのリストから1つ(または複数)の値を選択するというのは、よくあるダイアログの使いみちです。AlertDialogではこれを実現するための仕組みが標準で備わっています。
後述するカスタムビューを使えばどのような形式でも扱うことは可能ですが、定型的なリストであれば、以下のようなソースを利用するのが簡単です。
種別 | 概要 |
---|---|
Arraysリソース | XMLで定義される配列リソース |
文字列型の配列 | CharSequence型の配列 |
ListAdapter | ListView(内部で利用している)のリスト管理用アダプタ |
Cursor | SQLiteなどのデータベース・カーソルオブジェクト |
▲リストとして表示可能なリソース種別
シンプルなリスト表示
最初に例として文字列の配列を表示し、どれかをタップしたらダイアログを閉じる方法について紹介します。
▲文字型の配列を指定して一覧から選択させる
やり方はシンプルで、文字列の配列とOnClickListenerをsetItemsに渡します。配列の項目が選択された場合には、whichには配列のインデックス値が渡されますので、それを用いてどれが選択されたかを判定できます。
class ItemsAlertDialogFragment : DialogFragment() { companion object { // リストに表示する値を配列として定義 val GENDERS = arrayOf("男性", "女性", "無回答") } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext()) .setTitle(R.string.title) // 性別を選択してください .setItems(GENDERS) { dialog, which -> // GENDERSのリストのどれかが選択されたときに実行される処理 // whichはGENDERS配列の選択されたインデックス Toast.makeText(context, "${GENDERS[which]} が選択されました", Toast.LENGTH_SHORT).show() } .setNegativeButton(R.string.close, null) .create() }
リストの表示(1つだけ選択)
次に、項目をラジオボタンで選択状態にし、アクションボタンで選択したものを確定させるリストを表示する方法について紹介します。
▲配列から1つだけ選択可能なリストを表示
先ほどのsetItemsと似ていますが、setSingleChoiseItemsを使います。第1引数、第3引数はsetItemsと同じですが、第2引数としてint型の値を受け取ります。これはダイアログを開いた際に特定の項目を選択状態としたい場合に利用します。デフォルトで何も選択状態にしない場合には-1を指定します。
class SingleChoiceAlertDialogFragment : DialogFragment() { companion object { val GENDERS = arrayOf("男性", "女性", "無回答") } // 選択された値を保持しておくための変数 private var chosenGender: String? = null override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { chosenGender = null return AlertDialog.Builder(requireContext()) .setTitle(R.string.title) .setSingleChoiceItems(GENDERS, -1) { dialog, which -> // GENDERSのリストのどれかが選択されたときに実行される処理 // whichはGENDERS配列の選択されたインデックス Toast.makeText(context, "${GENDERS[which]} が選択されました", Toast.LENGTH_SHORT).show() // 選択された値をchosenGenderに設定 chosenGender = GENDERS[which] } .setPositiveButton(R.string.ok) { dialog, which -> // Positiveボタンがタップされたときに実行される処理 if (chosenGender == null) { // 何も選択されていない場合 Toast.makeText(context, "性別は選択されていません", Toast.LENGTH_SHORT).show() } else { // 何らかの値が選択されている場合 Toast.makeText(context, "選択された性別は $chosenGender です", Toast.LENGTH_SHORT).show() } } .setNegativeButton(R.string.close, null) .create() } }
リストの表示(複数選択)
リスト表示の最後に紹介するのは、項目を複数選択可能にするためのメソッドです。画面上では項目はチェックボックスで表されています。
▲配列から複数選択可能なリストを表示
名前から想像のつくとおり、setMultiChoiceItemsを使います。第1引数は他と同様ですが、第2引数としてboolean型の配列を、第3引数には OnMultiChoiceClickListenerを受け取ります。最初からチェックマークを付けておくには第2引数に対応するインデックスの値をtrueにしておいた配列を渡します。nullが渡された場合にはすべて未チェックマークの状態になります。
class MultiChoiceAlertDialogFragment : DialogFragment() { companion object { val DRINKS = arrayOf("水", "牛乳", "オレンジジュース", "コーヒー", "紅茶", "その他") } // 配列の要素が選択されたかどうかを保持するためのBoolean配列 private var chosenDrinks: Array<Boolean> = emptyArray() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // すべてfalseとして初期化 chosenDrinks = (1..DRINKS.size).map { false }.toTypedArray() return AlertDialog.Builder(requireContext()) .setTitle(R.string.title) // 朝食に飲む飲み物をを選択してください(複数選択可) .setMultiChoiceItems(DRINKS, null) { dialog, which, isChecked -> if (isChecked) { // whichで指定されたアイテムは選択された Toast.makeText(context, "${DRINKS[which]} が選択されました", Toast.LENGTH_SHORT).show() } else { // whichで指定されたアイテムは選択解除された Toast.makeText(context, "${DRINKS[which]} が選択解除されました", Toast.LENGTH_SHORT).show() } // タップした値が選択されたかどうかを設定 chosenDrinks[which] = isChecked } .setPositiveButton(R.string.ok) { dialog, which -> // mapIndexedによりisChecked=trueの場合の値をDRINKSから取得 val drinks = chosenDrinks.mapIndexed { index, isChecked -> when (isChecked) { true -> DRINKS[index] else -> null } } .filterNotNull() // isChecked=falseの場合にはnullなのでここで除外 .joinToString(",") // 選択された値をカンマで連結 if (drinks.isEmpty()) { // 空文字列の場合には選択された値がない Toast.makeText(context, "飲み物は選択されていません", Toast.LENGTH_SHORT).show() } else { // カンマ区切りで選択された値を表示 Toast.makeText(context, "選択された飲み物は、$drinks です", Toast.LENGTH_SHORT).show() } } .setNegativeButton(R.string.close, null) .create() } }
ダイアログを閉じる
AlertDialogでは、PositiveやNegativeなどのボタンをタップした場合やダイアログの範囲外をタップした場合には閉じられますが、ダイアログの外側から閉じるにはdismissまたはcancelメソッドを使用します。AlertDialogなど、Dialogクラスを継承したクラスはDialogInterfaceインターフェイスを実装しており、これらのメソッドはこのインターフェイスに定義されています。
この2つのメソッドは、振る舞いとしてはどちらもダイアログを閉じるというものですが、リスナなどの呼び出しについては次のような違いがあります。
ダイアログを閉じる際に発行されるイベント
AlertDialog.Builderではダイアログを閉じる際に呼び出されるコールバックとしてOnCancelListenerとOnDismissListenerの2種類のリスナを登録できます。それぞれに定義されているメソッドは1種類ずつで、次のような場合に呼び出されます。
(1)OnCancelListener#onCancel
- cancel()メソッドが呼び出された
- ダイアログの範囲外をタップした
- 端末のBackキーをタップした
(2)OnDismissListener#onDismiss
- dismiss()メソッドが呼び出された
- ダイアログのアクションボタンがタップされた
- 上記のOnCancelListenerで発行されるイベント
例えば、
- どのアクションボタンをタップされたときにも同じ処理を実行させたい場合はOnDismissListenerを
- 時間のかかる処理の進捗状況をダイアログで表示している最中に、バックボタンをタップされて処理をキャンセルさせたい場合はOnCancelListenerを
それぞれ利用します。
class MainActivity : AppCompatActivity() { // 省略 private fun setupViews() { showOnCancelListenerTextView.setOnClickListener { AlertDialog.Builder(this) .setTitle(R.string.title) // アンケート .setMessage(R.string.message) // アンケートに回答していただけますか? .setPositiveButton(R.string.answer) { dialog, which -> Toast.makeText(context, "アンケートを開始します", Toast.LENGTH_SHORT).show() } .setNegativeButton(R.string.cancel) { dialog, which -> Toast.makeText(context, "またの機会に", Toast.LENGTH_SHORT).show() } .setOnCancelListener { dialog -> // キャンセルされた場合に実行される処理 Toast.makeText(context, "Cancelされました", Toast.LENGTH_SHORT).show() } .setOnDismissListener { dialog -> // ダイアログが閉じた場合に実行される処理。キャンセルされた場合にも呼び出される Toast.makeText(context, "Dismissされました", Toast.LENGTH_SHORT).show() } .show() } } }
DialogFragmentを使っている場合には、次のようにonDismissとonCancelをオーバーライドしてイベントをハンドリングすることもできます。
class ListenerAlertDialogFragment : DialogFragment() { override fun onDismiss(dialog: DialogInterface?) { super.onDismiss(dialog) // ダイアログが閉じた場合に実行される処理。キャンセルされた場合にも呼び出される Toast.makeText(context, "Dismissされました", Toast.LENGTH_SHORT).show() } override fun onCancel(dialog: DialogInterface?) { super.onCancel(dialog) // キャンセルされた場合に実行される処理 Toast.makeText(context, "Cancelされました", Toast.LENGTH_SHORT).show() } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext()) .setTitle(R.string.title) .setMessage(R.string.message) .setPositiveButton(R.string.answer) { dialog, which -> Toast.makeText(context, "アンケートを開始します", Toast.LENGTH_SHORT).show() } .setNegativeButton(R.string.cancel) { dialog, which -> Toast.makeText(context, "またの機会に", Toast.LENGTH_SHORT).show() } .create() }
Cancelさせないようにするには
ダイアログの仕様によっては、Backボタンなどで閉じさせず、アクションボタンのいずれかを確実に選択させたいケースがあります。そのような場合にはCancelableフラグをfalseに設定します。
class MainActivity : AppCompatActivity() { // 省略 private fun setupViews() { showCancelableTextView.setOnClickListener { AlertDialog.Builder(this) .setTitle(R.string.title) .setMessage(R.string.message) .setCancelable(false) .setNegativeButton(R.string.close, null) .show() } }
DialogFragmentを使っている場合には、次のようにonCreateなどでisCancelableにfalseを設定します。
class NoCancelableAlertDialogFragment : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) isCancelable = false } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(requireContext()) .setTitle(R.string.title) .setMessage(R.string.message) .setNegativeButton(R.string.close, null) .create() }
カスタムビューを表示
ここまでは、AlertDialog.Builderに用意されたメソッドを使ってコンテンツ領域に表示する内容を設定してきましたが、コンテンツ領域には独自に定義したビューの指定もできます。ここからは、ダイアログ内に任意のサイトを開くというサンプルを通じて、カスタムビューをダイアログに表示する方法について説明します。
▲AlertDialogにWebViewでサイト画面を表示
上記のように、ダイアログに任意のサイト(この例ではエンジニアHub)を表示するには、コンテンツ領域に表示するためのビューを作成する必要があります。すべてプログラム側(KotlinまたはJava)で実装することもできますが、本サンプルではレイアウトファイルとKotlinを使ってビューの定義と設定を実装しています。
class WebViewAlertDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // (1) レイアウトファイルからViewをinflateする val view = LayoutInflater.from(requireContext()).inflate(R.layout.view_custom_webview, null) // (2) WebViewの設定と任意のURLを表示 val webView = view.findViewById<WebView>(R.id.webView) webView.webViewClient = WebViewClient() webView.loadUrl("https://employment.en-japan.com/engineerhub/") return AlertDialog.Builder(requireContext()) .setView(view) // (3) 作成したビューをコンテンツ領域に設定 .setPositiveButton(R.string.close, null) .create() } }
<?xml version="1.0" encoding="utf-8"?> <WebView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" />
レイアウトファイルからビューをインスタンス化します(1)。次にWebViewの設定と読み込むURLを指定します(2)。ビューの設定が終わったら、それをAlertDialog.BuilderのsetViewメソッドでダイアログのコンテンツ領域に設定します(3)。
この例ではWebViewのみを指定していますが、カスタムビュー用のレイアウトファイルには、より複雑なレイアウトも定義できます。
スタイルをカスタマイズ
最後に、ダイアログの見た目を変更する方法について紹介します。ダイアログも他の画面と同様にテーマに従ったスタイルで色や文字の大きさなどが定義されています。
例えば、リストを表示させる場合、表示項目が多くなると、下の図のように画面をダイアログが覆い尽くしてしまうこともあります。こうした表示がデザインの観点で許容されない場合、スタイルの変更で高さなどを調整することで見栄えを良くできます。
▲配列の数が多く、画面に収まりきらない例
上記のダイアログの高さと、ボタンの文字サイズおよび色を変更するやり方について紹介します。
▲ダイアログの高さ、Positiveボタンの色と大きさを変更
カスタマイズするスタイルの定義は、res/values/styles.xmlに記述します。
<style name="CustomAlertDialogStyle" parent="ThemeOverlay.AppCompat.Dialog.Alert"> <item name="windowFixedHeightMajor">65%</item> <!-- (1)高さ:最大 --> <item name="windowFixedHeightMinor">65%</item> <!-- (1)高さ:最小 --> <item name="buttonBarNegativeButtonStyle">@style/buttonBarNegativeButtonStyle</item> </style> <style name="buttonBarNegativeButtonStyle" parent="Widget.AppCompat.Button.Borderless.Colored"> <item name="android:textColor">@color/colorPrimary</item> <!-- (2) --> <item name="android:textSize">20sp</item> <!-- (3) --> </style>
この例ではダイアログの高さを画面全体の65%(1)に、Positiveボタンの色をprimaryと同じ(2)に、ボタンテキストのサイズを20sp(3)に設定しています。スタイル定義は階層的に書くことができないため、ボタンなどは別の要素として定義したスタイルを参照するという形で指定しています。
Kotlinからは、AlertDialog.Builderのコンストラクタ第2引数としてスタイルを指定します。
class CustomStyleAlertDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val days = (1..31).map { "${it}日" }.toTypedArray() // Builderコンストラクタの第2引数にスタイル名を指定する return AlertDialog.Builder(requireContext(), R.style.CustomAlertDialogStyle) .setTitle(R.string.title) // 日にちを選択してください .setItems(days) { dialog, which -> Toast.makeText(context, "${days[which]} が選択されました", Toast.LENGTH_SHORT).show() } .setNegativeButton(R.string.close, null) .create() } }
まとめ
ダイアログはユーザーの注意を引いたり、ちょっとした値を入力させたりするのに便利に使える画面です。しかし「デザイナーからの指示がそうだから」「簡単に使えるから」といった理由で安易にダイアログを使い回すと、本来の意図とは別の解釈をされてしまう可能性があります。あくまでもユーザーの意思決定を求める場合、重要な情報を提示する場合など、用途に合う状況で使うようにしたいものです。
ダイアログは単純ながらも、どういうケースにふさわしいのかを考えるにはなかなか奥深いコンポーネントです。この記事をきっかけにダイアログに興味を持った方は、Android Developers「ダイアログ」やマテリアルデザイン「Dialogs」などのドキュメントから読み進めてみることをおすすめします。
著者プロフィール
微糖ぜんざい
監修:WINGSプロジェクト 山田 祥寛
WINGS @yyamada WINGSプロジェクト