年月選択可能なダイアログ(Numberpicker+AlertDialog) | Kotlin


はじめに、
DatePickerで”年月”選択可能なダイアログを実装しようと
思っていたのに、全然できず。どうしても”年月日”になってしまう。
年度選択に使いたかったのに！！！日！！！日は入ってほしくない！！

結局、DatePikerでは日を除くことは出来ないんだ...:frowning2:
ってことで、NumberpickerをAlertDialogにのせた。

これからもAlertDialogを使う機会は全然あるので、全部残しておく。


## 完成図

①「年月表示」のボタンを押す
![ボタン表示.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2499099/07f8f32b-e67e-bab0-25db-1a976b3ee712.jpeg)
②年と月を選択できるダイアログが表示される
![シンプルダイアログ.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2499099/81092a78-787b-1c07-8499-a0184d81ad30.jpeg)
③OKボタンを押すと①の画面に戻り、選択した日付が表示される
![結果表示.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2499099/838104b4-a2af-f585-b539-d1104278f7b1.jpeg)

③でキャンセルを押した場合は、何もせず①の画面に戻る。

## 実装
#### Fragmentをつくる

```xml:DateFragment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    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"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Button
        android:id="@+id/yearMonthButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="284dp"
        android:text="@string/date" 
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/selectedDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="68dp"
        app:layout_constraintBottom_toTopOf="@+id/yearMonthButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>
```

「年月表示」のボタンと
ダイアログで年月選択した後に表示させる用のTextViewだけ配置

```Kotlin:DateFragment.kt

class DateFragment : Fragment(){
  
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)

        return inflater.inflate(R.layout.fragment_date, container, false)
    }


    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val dateButton = view.findViewById<Button>(R.id.yearMonthButton)

        dateButton.setOnClickListener {
            val dialog = NumberPickerDialog()
            val fragment = childFragmentManager
            dialog.show(fragment,"NumberPickerDialog")

        }
    }
}
```
・Fragmentを継承したDateFragmentを作成
・ボタンをクリックした時にダイアログを表示させる

`childFragmentManager `
getChildFragmentManager()
子Fragmentを管理するFragmentManager  = childFragmentManagerを取得
FragmentManagerとは、Fragmentの生成系にかかわる事を担当してるクラス(と思っている)

`dialog.show(fragment,"NumberPickerDialog) `
上記で取得したchildFragmentManagerとタグを渡してやる。
ちなみにparentFragmentManagerも存在するけど、ここでparentを渡すと落ちるので注意...
自分(ここでいうDateFragment)がどこにアクセスするのかによって、渡すFragmentManagerが異なる様子
いまは子FragmentにアクセスしたいのでchildFragmentManagerに頼む、みたいな


#### AlertDialog+Numberpickerをつくる


```xml:dialog_numberpicker.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:orientation="horizontal"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <NumberPicker
        android:id="@+id/yearNumberPicker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.348"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.499" />

    <NumberPicker
        android:id="@+id/monthNumberPicker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="40dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toEndOf="@+id/yearNumberPicker"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.499" />

</androidx.constraintlayout.widget.ConstraintLayout>

```
年のNumberpickerと月のNumberpickerを横に並べる


```kotlin:NumberPickerDialog.kt

class NumberPickerDialog():DialogFragment(){

    //Dialogのレイアウト定義
    private lateinit var dialogView: View
    // OKキャンセルボタンが押された時のリスナー定義
    private lateinit var listener: NoticeDialogListener
    // 選択された年の定義
    private var selectedYearItem by Delegates.notNull<Int>()
    // 選択された月の定義
    private var selectedMonthItem by Delegates.notNull<Int>()

   //OK、キャンセルボタンがが押された時用のインターフェース
    interface NoticeDialogListener {
        fun onNumberPickerDialogPositiveClick(dialog: DialogFragment, selectedYearItem: Int, selectedMonthItem:Int)
        fun onNumberPickerDialogNegativeClick(dialog: DialogFragment)
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        try{
            val fragment = parentFragment
     //親フラグメントがNoticeDialogListenerを継承していることを明示している
            this.listener = fragment as NoticeDialogListener
        }catch (e: ClassCastException){
            throw  ClassCastException("$context must implement NoticeDialogListener")
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        //レイアウトの指定
        dialogView = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_numberpicker,null)
        val builder = AlertDialog.Builder(requireContext())
        //現在日付の取得
        val current = LocalDateTime.now()

        builder.setView(dialogView)
        builder.setTitle("タイトル")
        builder.setMessage("メッセージ")

        //OKボタンが押された時の処理
        builder.setPositiveButton("OK"){_,_ -> this.listener.onNumberPickerDialogPositiveClick(this,selectedYearItem,selectedMonthItem)}
        //キャンセルボタンが押された時の処理
        builder.setNegativeButton("キャンセル"){_,_ -> this.listener.onNumberPickerDialogNegativeClick(this) }


        /**
         * 年のNumberpickerの初期化
         * デフォルト値：現在年
         * 最小値：現在年 - 1
         * 最大値：現在年 + 30
         */
        val yearNumberPicker = dialogView.findViewById<NumberPicker>(R.id.yearNumberPicker)
        //現在値の変更があった時に、現在フォーカスの当たっている値を取得し、selectedYearItemに格納
        yearNumberPicker.setOnValueChangedListener(object :NumberPicker.OnValueChangeListener{
            override fun onValueChange(picker: NumberPicker?, old: Int, new: Int) {
               selectedYearItem = new
            }
        })
        //最小値の設定
        yearNumberPicker.minValue = current.year-1
        //最大値の設定
        yearNumberPicker.maxValue = current.year+30
        //yearNumberPickerの初期値に今年を入れた
        yearNumberPicker.value = current.year
        //selectedYearItemにも同じく今年を入れておく
        selectedYearItem = yearNumberPicker.value

        /**
         * 月のNumberpickerの初期化
         * デフォルト値：今月
         * 最小値：1
         * 最大値：12
         */
        val monthNumberPicker = dialogView.findViewById<NumberPicker>(R.id.monthNumberPicker)
        monthNumberPicker.setOnValueChangedListener(object :NumberPicker.OnValueChangeListener{
            override fun onValueChange(picker: NumberPicker?, old: Int, new: Int) {
                selectedMonthItem = new
            }
        })
        monthNumberPicker.minValue = 1
        monthNumberPicker.maxValue = 12
        monthNumberPicker.value = current.monthValue
        selectedMonthItem = monthNumberPicker.value

        return builder.create()
    }
}

```

大体はコメントに記載した通りのことをしてるので、
私的に躓いたとこだけ残しておく:angel:

```kotlin
 val fragment = parentFragment
 this.listener = fragment as NoticeDialogListener
```
「親フラグメンをListenner型に変換？！わけわからん」
って感じだったのですが、現段階での私の理解は以下。
初期化遅延中のlistenerに”NoticeDialogListenerを継承したparentFragment”を入れてる。
ただlistenerはNoticeDialogListener型で定義しているのでNoticeDialogListener型に変換。
そうするとlistenerの正体は、parentFragmentのNoticeDialogListener。

もっと詳しく・・・・？？
　・全く関連のないクラス同士でのオブジェクトのキャストは出来ない。
  ・今回オブジェクトのキャストが出来るのは、スーパークラスとサブクラスの関係があるから。
　・サブクラスの中にはスーパークラスのメソッド等がそろってるから可能
　・使い時は、今回のように親フラグメントに書いてあるメソッドを使用する場面。
　・親フラグメントのNoticeDialogListenerだけを参照できる。
　・この親フラグメント(fragment)が誰(DateFragment)とは指定していない。
　　` val fragment = parentFragment`(←ざっくり親ってだけ。)
　　ダイアログとかって使いまわすことがあるから、
　　親がだれか決めてしまうと使いまわしにくいんだな～と思った(使う親分の処理を書くはめになる的な)
　　でも一応` this.listener = fragment as DateFragment`として親指定することもできた。

スーパークラス、サブクラス、親フラグメント、子フラグメントって頭おかしくなる。
今回は、
親フラグメント：DateFragment (ほかに親がいる場合はそれもまた親である～)
子フラグメント：NumberPickerDialog
スーパークラス：NoticeDialogListener
サブクラス：DateFragment
って思ってる:point_up: 


```kotlin
builder.setPositiveButton("OK"){_,_ -> this.listener.onNumberPickerDialogPositiveClick(this,selectedYearItem,selectedMonthItem)}
```
OKボタンの処理なのは理解したけど、いまいち初心者の私にはピンとこず。
まず、setPositiveButtonとは↓
` setPositiveButton(CharSequence text, final OnClickListener listener) `
第一引数：表示するテキストを指定する(OK)
第二引数：DaialogInterface.OnClickListener()
ラムダとSAM変換を利用しているみたいなんだけど現段階の私の理解は以下。
・第二引数がメソッドの場合は()の外に{}で処理が書ける
・でも今回使うlistennerは自前のものでOnClickListener()は使わないので_,_と書く(ここ怪しい)
・{パラメータ　-> 処理}
これはまた今度にしよ......:hand_splayed:

```kotlin
  monthNumberPicker.setOnValueChangedListener(object :NumberPicker.OnValueChangeListener{
            override fun onValueChange(picker: NumberPicker?, old: Int, new: Int) {
                selectedMonthItem = new
            }
        }) 
```
`setOnValueChangedListener `
値が変更されたときに通知を受け取れるリスナー
` onValueChange`
現在値が変更されたときに呼ばれるメソッド
今回はここで現在の値をselectedMonthItem に入れてる
初期値として、年には今年、月には今月を入れているので
(selectedMonthItem = monthNumberPicker.value)
値の変更がされずにOKされた時にも値が入ってる状態



#### OKまたはキャンセルの処理をかく

初めに登場したDateFragment.ktに以下追加

```kotlin:DateFragment.kt
class DateFragment : Fragment(),NumberPickerDialog.NoticeDialogListener {

    override fun onNumberPickerDialogNegativeClick(dialog: DialogFragment) {
        return
    }

    override fun onNumberPickerDialogPositiveClick(
        dialog: DialogFragment,
        selectedYearItem: Int,
        selectedMonthItem: Int
    ) {
        val selectedDate = view?.findViewById<TextView>(R.id.selectedDate)
        val date = selectedYearItem.toString() + "年" + selectedMonthItem.toString() + "月"
        selectedDate?.text = date
    }
 //以下省略
}
```




#### この記事の内容だけ試す方は以下追加

これ書かないとDateFragmentにたどり着いてないはず。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

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

          val fragment = DateFragment()
          val transaction =supportFragmentManager.beginTransaction()

          transaction.add(R.id.container,fragment)
          transaction.commit()
    }
}
```
xmlに以下のidを追加
```xml:activity_main.xml
android:id="@+id/container"
```

これで一応フラグメント→ダイアログで選択→選択した年月がフラグメントで表示される、
ってところまでいけると思う。

## AlertDialogのデザイン変更

この記事では実装内容について記載しましたが、
長くなりすぎたのでデザイン変更部分は別記事に分けちゃた:upside_down:

　![完成形.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2499099/9536fa28-1208-7fd3-bbe7-8c9b052aeadf.jpeg)

センスは置いといて...ちょっと可愛い感じになってない？:thinking:

タイトル、メッセージ、アイコン、ボタンの色や位置の変更などは、こっちにまとめた↓

https://qiita.com/hanaaaa/items/dd58ab6946d8ff4a0994

## まとめ

親フラグメントが子フラグメントの中に存在しているInterfaceのサブクラス...:innocent:
頭おかしくなりそうでした～

参考記事：https://developer.android.com/reference/kotlin/android/widget/NumberPicker
　　　　　https://swallow-incubate.com/archives/blog/20200511/
　　　　　https://akira-watson.com/android/numberpicker.html
　　　　　https://alpha-netzilla.blogspot.com/2010/09/blog-post.html
　　　　　https://manga.crocro.com/?cat=java&pg=cast_object