DialogFragmentで５分刻みのタイムピッカー

## はじめに

記事にするのを１年間忘れてたので、備忘録といっても今更役に立つやら。

一般的な手法なのかわかりませんが、
「5分刻み TimePicker」とかで検索すると、
Android標準のタイムピッカーを改造する例がちょいちょい見つかります。

しかしこのタイムピッカーが、
API30以上がターゲットの場合にクラッシュするようになりました。

https://developer.android.com/about/versions/11/non-sdk-11
APIへのアクセスが禁止されたものの中に、TimePickerがあります。
今は新しいTimePickerが提供されているので、そっちを使ってねってことみたいです。

でもダイアル式のタイムピッカーが使いたいので、
クラッシュ原因以外の部分だけ切り取って、それっぽいものに復元しました。

画面表示する都合で、
最終的に取得できる時間と分を数値にしています。

## タイムピッカー

```Java:TimePickerDialogFragment.java
package com.example.myapplication;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.NumberPicker;

import androidx.fragment.app.DialogFragment;

import java.util.ArrayList;
import java.util.List;

public class TimePickerDialogFragment extends DialogFragment {

    public interface TimePickerDialogFragmentDelegate {
        void onClickPositiveButton(int hour, int minute);
        // キャンセル押した時の処理を追加したい場合はコメントを外す
//        void onClickNegativeButton();
    }

    private final static int TIME_PICKER_INTERVAL = 5; //５分刻みにする
    private final static int MIN_VALUE_OF_HOUR = 0;
    private final static int MAX_VALUE_OF_HOUR = 23;
    private final static int MIN_VALUE_OF_MINUTE = 0;
    private final static int MAX_VALUE_OF_MINUTE = 55;

    // タイムピッカーをループさせるためのもの
    private final static int TIME_PICKER_THRESHOLD_HOUR_ADD = 55;
    private final static int TIME_PICKER_THRESHOLD_HOUR_REMOVE = -55;

    private final int indexForHour;
    private final int indexForMinute;
    private final TimePickerDialogFragmentDelegate delegate;

    public TimePickerDialogFragment(int hour,
                                    int minute,
                                    TimePickerDialogFragmentDelegate delegate) {
        this.delegate = delegate;
        this.indexForHour = hour;
        this.indexForMinute = minute / TIME_PICKER_INTERVAL;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        final View view = inflater.inflate(R.layout.fragment_timepicker_dialog, null);

        final NumberPicker numberPickerHour = view.findViewById(R.id.numpicker_hours);
        numberPickerHour.setMinValue(MIN_VALUE_OF_HOUR);
        numberPickerHour.setMaxValue(MAX_VALUE_OF_HOUR);
        numberPickerHour.setValue(indexForHour);

        final NumberPicker numberPickerMinutes = view.findViewById(R.id.numpicker_minutes);
        numberPickerMinutes.setMinValue(MIN_VALUE_OF_MINUTE);
        numberPickerMinutes.setMaxValue(MAX_VALUE_OF_MINUTE / TIME_PICKER_INTERVAL);
        numberPickerMinutes.setDisplayedValues(createDisplayedValuesForMinute());
        numberPickerMinutes.setValue(indexForMinute);

        numberPickerMinutes.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
            @Override
            public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
                final int diff = (oldVal - newVal) * TIME_PICKER_INTERVAL;
                int val;
                switch (diff) {
                    case TIME_PICKER_THRESHOLD_HOUR_ADD:
                        val = 1;
                        break;
                    case TIME_PICKER_THRESHOLD_HOUR_REMOVE:
                        val = -1;
                        break;
                    default:
                        return;
                }
                numberPickerHour.setValue(numberPickerHour.getValue() + val);
            }
        });

        final Button negativeButton = view.findViewById(R.id.negativeButton);
        final Button positiveButton = view.findViewById(R.id.positiveButton);
        negativeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // キャンセル押した時の処理を追加したい場合はコメントを外す
//                delegate.onClickNegativeButton(); 
                dismiss();
            }
        });
        positiveButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (delegate == null) {
                    return;
                }
                delegate.onClickPositiveButton(numberPickerHour.getValue(), numberPickerMinutes.getValue() * TIME_PICKER_INTERVAL);
                dismiss();
            }
        });

        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setView(view);
        return builder.create();
    }

    private String[] createDisplayedValuesForMinute() {
        List<String> displayedValues = new ArrayList<>();
        for (int i = 0; i <= MAX_VALUE_OF_MINUTE; i += TIME_PICKER_INTERVAL) {
            displayedValues.add(String.format( "%02d", i));
        }
        return displayedValues.toArray(new String[0]);
    }
}

```

## レイアウト

```XML:fragment_timepicker_dialog.xml
<?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="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="16dp"
        android:gravity="center"
        android:orientation="horizontal">

        <NumberPicker
            android:id="@+id/numpicker_hours"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <NumberPicker
            android:id="@+id/numpicker_minutes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="16dp"
            android:layout_marginRight="16dp" />
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#dcdcdc" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <Button
            android:id="@+id/negativeButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#444444"
            android:background="@android:color/transparent"
            android:text="@android:string/cancel"
            android:textAllCaps="false" />

        <View
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="#dcdcdc" />

        <Button
            android:id="@+id/positiveButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textColor="#444444"
            android:background="@android:color/transparent"
            android:text="@android:string/ok"
            android:textAllCaps="false" />
    </LinearLayout>

</LinearLayout>
```

## 呼び出し元

新規プロジェクトを作って、Hello World!のところに表示してみる。

```Java:MainActivity.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.text);

        final String tag = MainActivity.class.getName();

        // ピッカーの初期値
        final int hour = 0;
        final int minute = 0;

        final TimePickerDialogFragment dialog = new TimePickerDialogFragment(hour, minute,
                (hourOfDay, miniteOfDay) -> {
                    textView.setText(String.format("%02d:%02d", hourOfDay, miniteOfDay));
                });
        dialog.show(getSupportFragmentManager(), tag);
    }
```

## 結果

分を一周すると時間が増えたり減ったりします。

![picker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329088/69bbf62a-51f8-0366-d17c-e14edfc5ad46.gif)