【Kotlin】NumberPickerを使用したダイアログの実装

2020.05.11

どうも、むつたくです。
今回は、Swiftでお馴染みのUIPickerViewをAndroidのダイアログ上で実装する方法を紹介します。
Androidでは「NumberPicker」と言います。
ユーザに複数選択させたい時とかは便利ですね。イメージはこんな感じです。

 

 

今回使用するダイアログは「AlertDialog」になります。

 

今回やること

  1. NumberPickerの使い方
  2. AlertDialogにNumberPickerを実装
  3. 親画面に値を渡す

 

公式リファレンス

 

開発環境

  • macOS Catalina 10.15.3
  • AndroidStudio 3.6.2
  • Kotlin 1.3.61

 

NumberPickerの使い方

まずは、NumberPickerで選択したアイテムをTextViewに表示するということを実装します。
さて、NumberPickerですが、デザインのPaletteから選択することが出来ません。なぜか一覧にない…。
なのでXMLから作成していきます。まずはXML全体から。

 

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

    <NumberPicker
       android:id="@+id/numberPicker"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

    <TextView
       android:id="@+id/textSelectedItem"
       android:layout_width="wrap_content"
       android:layout_height="19dp"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="@+id/pickNumber" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

まずは、こんな感じになっていればOKです。
NumberPickerを使用する際には、以下のXMLコードをコピペすればデザイン上でもドラッグ出来るようになります。

 

1
2
3
4
    <NumberPicker
        android:id="@+id/NumberPicker1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

 

 

 

次はktファイルです。

 

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.NumberPicker
import android.widget.TextView

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        this.initNumberPicker()
    }

    /**
     * NUmberPicker初期化メソッド
     **/

    private fun initNumberPicker() {
        val np = findViewById<NumberPicker>(R.id.numberPicker)
        np.minValue = 1  // NumberPickerの最小値設定
        np.maxValue = 10 // NumberPickerの最大値設定
        np.value = 5     // NumberPickerの初期値

        // NumberPickerのアイテムチェンジリスナー
        np.setOnValueChangedListener { picker, oldVal, newVal ->
            println("前回選択値: $oldVal")
            println("現在選択値: $newVal")

            // 選択したアイテムをTextViewに表示
            findViewById<TextView>(R.id.textSelectedItem).text = newVal.toString()
        }
    }

}

 

要所にコメントは書いてありますが、NumberPickerで使用しているのは以下のプロパティになります。

プロパティ名 説明
minValue  NumberPickerの最小値を設定
maxValue  NumberPickerの最大値を設定
value  NumberPickerの初期値を設定

 

NumberPickerで選択した値をTextViewに表示するのは「setOnValueChangedListener」になります。
引数には以下の内容が格納されます。

引数 説明
picker  アイテムチェンジしたNumberPickerのクラス
oldVal  該当NumberPickerの前回値
newVal  該当NumberPickerの現在値

 

NumberPickerの使い方は以上になります。

 

AlertDialogの作成

次は、AlertDialogを作成していきます。このダイアログの中にNumberPickerを配置します。
作成手順は以下の3ステップになります。

  1. LayoutXML作成
  2. CustomDialogClass作成
  3. MainActivity.ktからコール

LayoutXML作成

まずはXMLを書きます。「res/layout」ディレクトリ直下に「Layout XML File」を作成します(New > XML > Layout XML File)。
ファイル名はここでは「number_picker_dialog.xml」にしました。
ダイアログのXMLは以下になります。

 

number_picker_dialog.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

    <androidx.cardview.widget.CardView
       android:layout_width="match_parent"
       android:layout_height="match_parent" >

        <NumberPicker
           android:id="@+id/numberPicker"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_gravity="center"/>

    </androidx.cardview.widget.CardView>
</LinearLayout>

 

CustomDialogClass作成

次はダイアログのコードを書きます。「java/[PackageName]」ディレクトリ直下に「Kotlin Class」を作成します(New > Kotlin File/Class)。
作成する際は「Class」を選択して下さい。
ファイル名はここでは「NumberPickerDialog.kt」にしました。

 

NumberPickerDialog.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.widget.NumberPicker
import androidx.fragment.app.DialogFragment

class NumberPickerDialog: DialogFragment() {

    /**
     * ダイアログ作成
     **/

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val inflater = activity!!.layoutInflater
        val dialogView = inflater.inflate(R.layout.number_picker_dialog, null)!!
        val builder = AlertDialog.Builder(context)

        // Dialogの設定
        builder.setView(dialogView)
        builder.setTitle("NumberPickerDialog")
        builder.setPositiveButton("OK") { dialog, id ->
        }
        builder.setNegativeButton("CANCEL") { dialog, id ->
        }

        // NumberPickerの設定
        val np = dialogView.findViewById<NumberPicker>(R.id.numberPicker)
        np.minValue = 1  // NumberPickerの最小値設定
        np.maxValue = 10 // NumberPickerの最大値設定
        np.value = 5     // NumberPickerの初期値

        return builder.create()
    }
}

 

ここでのポイントが2つあります。

 

1つ目のポイントは、DialogFragmentクラスを継承します。
そうすることで、ダイアログを扱うことが出来る様になります。

 

2つ目ポイントは、AlertDialog.Builderです。
このメソッドを使用することでAlertDialogのViewを変更できたり、ボタンやタイトルを設定することができます。
19行目の「builder.setView」に先ほど作成したLayoutファイルを指定します。
20行目の「builder.setTitle」ではダイアログのタイトルを設定してます。
21行目の「builder.setPositiveButton」では後述しますが、ボタンを配置します。こちらはダイアログの内容を完了させる意味合いを持ちます。
23行目の「.setNegativeButton」ではこちらも後述しますが、同じくボタンを配置します。このボタンはダイアログの内容をキャンセルする意味合いを持ちます。

 

MainActivity.ktからコール

ダイアログを作成したので、親からこのダイアログをコールします。

 

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

    <TextView
       android:id="@+id/textSelectedItem"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textSize="24sp"
       android:text="選択したアイテムを表示"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />

    <Button
       android:id="@+id/btnShowDialog"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="Dialog"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@+id/textSelectedItem"
       app:layout_constraintVertical_bias="0.19999999" />


</androidx.constraintlayout.widget.ConstraintLayout>

 

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.btnShowDialog).setOnClickListener {
            val dialog = NumberPickerDialog()
            dialog.show(supportFragmentManager, "NumberPickerDialog")
        }
    }
}

 

DIALOGボタンをタップするとこんな画面が表示されればOKです。

 

 

親画面に値を渡す

今度はOKボタンをタップしたら親画面に値を渡す処理をやっていきます。

 

NumberPickerDialog.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.NumberPicker
import androidx.fragment.app.DialogFragment

class NumberPickerDialog: DialogFragment(), NumberPicker.OnValueChangeListener {

    private lateinit var listener: NoticeDialogListener // 親に渡すためのリスナー定義
    private var selectedItem: Int = 0 // 選択したアイテム格納

    interface NoticeDialogListener {
        fun onNumberPickerDialogPositiveClick(dialog: DialogFragment, selectedItem: Int)
        fun onNumberPickerDialogNegativeClick(dialog: DialogFragment)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            this.listener = context as NoticeDialogListener
        } catch (e: ClassCastException) {
            throw ClassCastException(("$context must implement NoticeDialogListener"))
        }
    }

    /**
     * ダイアログ作成
     **/

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val inflater = activity!!.layoutInflater
        val dialogView = inflater.inflate(R.layout.number_picker_dialog, null)!!
        val builder = AlertDialog.Builder(context)

        // Dialogの設定
        builder.setView(dialogView)
        builder.setTitle("NumberPickerDialog")
        builder.setPositiveButton("OK") { _, _ ->
            this.listener.onNumberPickerDialogPositiveClick(this, this.selectedItem) //
        }
        builder.setNegativeButton("CANCEL") { _, _ ->
            this.listener.onNumberPickerDialogNegativeClick(this)
        }

        // NumberPickerの設定
        val np = dialogView.findViewById<NumberPicker>(R.id.numberPicker)
        np.setOnValueChangedListener(this)
        np.minValue = 1  // NumberPickerの最小値設定
        np.maxValue = 10 // NumberPickerの最大値設定
        np.value = 5     // NumberPickerの初期値

        return builder.create()
    }

    override fun onValueChange(picker: NumberPicker?, oldVal: Int, newVal: Int) {
        this.selectedItem = newVal
        Log.d("item", this.selectedItem.toString())
    }

     override fun onDestroy() {
        super.onDestroy()
    }

    override fun onDetach() {
        super.onDetach()
    }

}

 

ActivityMain.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.fragment.app.DialogFragment

class MainActivity : AppCompatActivity(), NumberPickerDialog.NoticeDialogListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<Button>(R.id.btnShowDialog).setOnClickListener {
            val dialog = NumberPickerDialog()
            dialog.show(supportFragmentManager, "NumberPickerDialog")
        }
    }

    override fun onNumberPickerDialogPositiveClick(
        dialog: DialogFragment,
        selectedItem: Int
    ) {
        val text = findViewById<TextView>(R.id.textSelectedItem)
        text.text = selectedItem.toString()
    }

    override fun onNumberPickerDialogNegativeClick(dialog: DialogFragment) {
        return
    }
}

 

細かく見ていくと、まずはインターフェースを作成します。このインターフェースで親とのやり取りを行うと思えばOKです。

 

1
2
3
4
    interface NoticeDialogListener {
        fun onNumberPickerDialogPositiveClick(dialog: DialogFragment, selectedItem: Int)
        fun onNumberPickerDialogNegativeClick(dialog: DialogFragment)
    }

 

親側ではダイアログで選択したアイテム、もしくはキャンセルした時の状態を受け取るので受け皿を準備します。「NumberPickerDialog.NoticeDialogListener」の継承も忘れずに。インターフェース名はダイアログで定義した名前になります。

今回は、OKボタンがタップされたらTextViewに選択したアイテムを表示する処理を、キャンセルボタンをタップしたら特に何もしないので、上記のような処理になってます。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MainActivity : AppCompatActivity(), NumberPickerDialog.NoticeDialogListener

    ...

    override fun onNumberPickerDialogPositiveClick(
        dialog: DialogFragment,
        selectedItem: Int
    ) {
        val text = findViewById<TextView>(R.id.textSelectedItem)
        text.text = selectedItem.toString()
    }

    override fun onNumberPickerDialogNegativeClick(dialog: DialogFragment) {
        return
    }

 

onAttachメソッドを使用することで、ActivityMainへイベントを伝搬することが出来るようになります。(ActivityではなくContextですが)

 

1
2
3
4
5
6
7
8
9
10
    private lateinit var listener: NoticeDialogListener

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try {
            this.listener = context as NoticeDialogListener
        } catch (e: ClassCastException) {
            throw ClassCastException(("$context must implement NoticeDialogListener"))
        }
    }

 

そして、ダイアログのボタンタップイベントで先に定義したインターフェースのメソッドを実行して親に渡しているといった感じになります。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        ...

        builder.setPositiveButton("OK") { _, _ ->
            this.listener.onNumberPickerDialogPositiveClick(this, this.selectedItem)
        }
        builder.setNegativeButton("CANCEL") { _, _ ->
            this.listener.onNumberPickerDialogNegativeClick(this)
        }

        ...
    }

 

これで親に選択したアイテムを渡すことが出来るようになりました。インターフェースを使用して親に選択したアイテムを渡すのはCustomDialogだけではなく、CheckDialog、RadioDialog等々の他のダイアログでも使用できるので、この方法は覚えておくといいかもしれません。

 


 

以上になります。AlertDialogを使用したNumberPickerの実装は意外と簡単に出来ちゃいます。
それでは良いKotlinライフを!


Top