Picker系DialogFragmentクラスの作成


【最終更新日】2023-04-07

Androidアプリで使用する日付PickerFragment, 時刻PickerFragment, 数値PickerDialogクラスの作成方法を解説します。

1.登録データ入力画面

当該画面の入力項目は全部で23項目有り全てEdit系ウィジェットを使った場合、入力の更新有無の管理が相当の困難が予想されました。今回は時刻の入力にTimePickerFragmentクラス、日付の入力にはDatePickerFragmentクラス、範囲の決まった数値の入力にはNumberPickerDialogクラスを自作して利用することにしました。

入力項目Picker系ダイアログフラグメント
測定日付DatePickerFragemt
起床時刻TimePickerFragment
夜間トイレ回数NumberPickerDialog
睡眠スコアNumberPickerDialog
睡眠時間TimePickerFragment
深い睡眠TimePickerFragment
血圧の測定時刻TimePickerFragment
最高血圧NumberPickerDialog
最低血圧NumberPickerDialog
脈拍NumberPickerDialog
体温の測定時刻TimePickerFragment
【データ登録画面とPicker系ダイアログ】
2.ApiDemosアプリのコードを再利用する

カスタムのUI部品をいちから作るのは工数がかかります。ApiDemosアプリには有用なコードが豊富なので今回は下記スクリーンショットの実装を再利用することにします。

【ApiDemosアプリのPicker系ダイアログサンプル】

【利用するソース】ApiDemosアプリ(Android Googlesource リポジトリから取得)

src/com/example/android/apis/view/
    DateWidgets1.java         -> DatePickerFragment, TimePickerFragment
    NumberPickerActivity.java -> NumberPickerDialog
src/com/example/android/apis/app/
    FragmentAlertDialog.java  -> NumberPickerDialog
res/layout/
    date_widgets_example_1.xml
    number_picker.xml
【公式ドキュメント】
選択ツール
https://developer.android.com/guide/topics/ui/controls/pickers?hl=ja
ダイアログ
https://developer.android.com/guide/topics/ui/dialogs?hl=ja
3.カスタム選択ダイアログクラス

自作のクラスは ApiDemosのコードと上記公式ドキュメントを参考にアプリの要件に合わせて作りました。 自作クラスを外部クラスとして定義するには呼び出し元のコンテキストとリスナークラスをコンストラクタに引き渡す必要があります。

※ ApiDemosのサンプル、公式ドキュメントの実装例はともにActivityの中で直接呼びだしする作りになっているので再利用性に問題があります。

【ソース】android-health-care-example/app/src/main/
java/com/example/android/healthcare/dialogs/PickerDialogs.java
【NumberPicker用レイアウト】
res/layout/dialog_number_picker.xml

(1) DatePickerFragmentクラス ※説明用にソースにないコメントを追加してます

public static class DatePickerFragment extends DialogFragment {
    private final Context mContext;
    private final DatePickerDialog.OnDateSetListener mListener;
    private Calendar mCalendar;

    // このコンストラクタでは呼び出し元のカレンダーを指定することにより当日以外の日付を指定して
    //  DatePickerDialogを開くことができます。
    public DatePickerFragment(@NonNull Context context,
                                Calendar cal,
                                @NonNull DatePickerDialog.OnDateSetListener listener) {
        mContext = context;   // 呼び出し元のコンテキスト (getActivity())
        mCalendar = cal;      // 呼び出し元で保持しているカレンダーインスタンス
        mListener = listener; // 呼び出し元で保持しているリスナーのインスタンス
    }

    // このコンストラクタでは常に当日のDatePickerDialogを開くしかできない。
    //  ※当日しか認めない要件ではこちらのコンストラクタを使用する想定で作りました
    public DatePickerFragment(@NonNull Context context,
                                @NonNull DatePickerDialog.OnDateSetListener listener) {
        this(context, null, listener);
    }

    // 公式ドキュメント(選択ツール) 「日付選択ツール用の DialogFragment の拡張」の実装をほぼ流用
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // final Calendar c = Calendar.getInstance(); 
        if (mCalendar == null) {
            mCalendar = Calendar.getInstance();
        }
        int year = mCalendar.get(Calendar.YEAR);
        int month = mCalendar.get(Calendar.MONTH); // 0-11
        int day = mCalendar.get(Calendar.DAY_OF_MONTH);
        return new DatePickerDialog(mContext, mListener, year, month, day);
    }
}

(2) TimePickerFragmentクラス ※説明用にソースにないコメントを追加してます

12時間と24時間の切替ができ、前回入力値の保持が可能になるようクラスを実装

public static class TimePickerFragment extends DialogFragment {
    // 時刻(時:分)を保持するクラス
    public static class TimeHolder {
        private final int mHour;
        private final int mMinute;

        public TimeHolder(int hour, int minute) {
            mHour = hour;
            mMinute = minute;
        }

        public int getHour() {
            return mHour;
        }

        public int getMinute() {
            return mMinute;
        }
    }

    private final Context mContext;
    private final TimeHolder mTimeHolder;
    private final TimePickerDialog.OnTimeSetListener mListener;
    private final Boolean mIs24HourView;

    public TimePickerFragment(@NonNull Context context,
                                TimeHolder timeHolder,
                                @NonNull TimePickerDialog.OnTimeSetListener listener,
                                @NonNull boolean is24Hour) {
        mContext = context;        // 呼び出し元のコンテキスト (getActivity())
        mTimeHolder = timeHolder;  // 呼び出し元で保持している時刻オブジェクト
        mListener = listener;      // 呼び出し元で保持しているリスナーのインスタンス
        mIs24HourView = is24Hour;  // trueなら24時間入力, falseなら12時間入力 
    }

    public TimePickerFragment(@NonNull Context context,
                                TimeHolder timeHolder, @NonNull TimePickerDialog.OnTimeSetListener listener) {
        this(context, timeHolder, listener, DateFormat.is24HourFormat(context));
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        int hour;
        int minute;
        if (mTimeHolder == null) {
            // 時刻オブジェクトが未設定ならカレンダーの時刻を利用する
            // こちらは公式ドキュメントの実装と同じ
            final Calendar cal = Calendar.getInstance();
            hour = cal.get(Calendar.HOUR_OF_DAY);
            minute = cal.get(Calendar.MINUTE);
        } else {
            // 呼び出し元の時刻を設定
            hour = mTimeHolder.getHour();
            minute = mTimeHolder.getMinute();
        }
        return new TimePickerDialog(mContext, mListener, hour, minute, mIs24HourView);
    }
}

(3) NumberPickerDialogクラス

タイトル, 見出し, 単位, 初期値・最大値・最小値を指定できるようクラスを実装

【レイアウトファイル】res/layout/dialog_number_picker.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:paddingStart="@dimen/alertdialog_padding_start_end"
    android:paddingEnd="@dimen/alertdialog_padding_start_end"
    android:paddingTop="@dimen/alertdialog_padding_top"
    >
    <TextView
        android:id="@+id/label"
        android:text="タイトル"
        style="@style/TextViewOfAlertDialog"
        />
    <NumberPicker
        android:id="@+id/numPicker"
        android:layout_width="@dimen/alertdialog_numberpicker_width"
        android:layout_height="wrap_content"
        android:layout_marginStart="@dimen/alertdialog_numberpicker_margin_start"
        android:layout_gravity="center_vertical"
        />
    <TextView
        android:id="@+id/unitView"
        android:layout_marginStart="@dimen/unit_margin_start"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="単位"
        style="@style/TextViewOfAlertDialog"
        />
</LinearLayout>
public static class NumberPickerDialog {
    // 選択値を呼び出し元に提供するカスタムリスナークラス
    public interface ValueListener {
        // OKボタン押下時: 選択値の受取り
        void onDecideValue(int number);
        // 取消しボタン押下時 ※ 値を戻すとかの処理は呼び出し元で実装
        void onCancel();
    }

    // ダイアログを構成する可変項目のデータクラス
    public static class DialogItem {
        private final String mDialogTitle;
        private final String mTitleLabel;
        private final String mUnitLabel;
        private final int mInitValue;
        private final int mMinValue;
        private final int mMaxValue;

        public DialogItem(String dialogTitle,
                            String titleLabel, String unitLabel, int initValue, int minValue, int maxValue) {
            mDialogTitle = dialogTitle; 
            mTitleLabel = titleLabel;
            mUnitLabel = unitLabel;
            mInitValue = initValue;
            mMinValue = minValue;
            mMaxValue = maxValue;
        }

        public String getDialogTitle() {
            return mDialogTitle;
        }

        public String getTitleLabel() {
            return mTitleLabel;
        }

        public String getUnitLabel() {
            return mUnitLabel;
        }

        public int getMinValue() {
            return mMinValue;
        }

        public int getInitValue() {
            return mInitValue;
        }

        public int getMaxValue() {
            return mMaxValue;
        }
    }

    private final Context mContext;
    private final DialogItem mDialogItem;
    private final ValueListener mListener;

    public NumberPickerDialog(Context context, DialogItem item, ValueListener listener) {
        mContext = context; // 呼び出し元のコンテキスト (getActivity())
        mDialogItem = item; // 呼び出し元の可変項目オブジェクト
        mListener = listener; // 呼び出し元で保持しているリスナーのインスタンス
    }

    public AlertDialog createNumberPickerDialog() {
        LayoutInflater factory = LayoutInflater.from(mContext);
        // ダイアログ内のコンテンツ生成
        final View entryView = factory.inflate(R.layout.dialog_number_picker, null);
        // 書き換える見出し、単位ビューを取得する
        final TextView labelView = entryView.findViewById(R.id.label);
        final TextView unitView = entryView.findViewById(R.id.unitView);
        labelView.setText(mDialogItem.getTitleLabel());
        unitView.setText(mDialogItem.getUnitLabel());
        // NumberPickerオブジェクトを取得
        final NumberPicker picker = entryView.findViewById(R.id.numPicker);
        // 初期値, 最小値, 最大値を設定
        picker.setMinValue(mDialogItem.getMinValue());
        picker.setMaxValue(mDialogItem.getMaxValue());
        picker.setValue(mDialogItem.getInitValue());
        return new AlertDialog.Builder(mContext)
                .setTitle(mDialogItem.getDialogTitle())
                .setView(entryView)
                .setPositiveButton(R.string.alert_dialog_ok,
                        (dialog, whichButton) -> mListener.onDecideValue(picker.getValue())
                )
                .setNegativeButton(R.string.alert_dialog_cancel, (dialog, whichButton) -> mListener.onCancel()
                )
                .create();
    }
}
4.アプリからPicker系ダイアログを使用する

[ソース] app/src/main/java/com/example/android/healthcare/ui/main/AppTopFragment.java

(1) 測定日付をDatePickerFragmentから取得する ※ダイアログの生成と値の取得部分のみ抜粋
【DatePickerFragment】
import com.examples.android.healthcare.dialogs.PickerDialogs.DatePickerFragment;

public class AppTopFragment extends Fragment {
    // ...中略...
    // DatePickerDialogに連動するカレンダーオブジェクト
    private final Calendar mMeasurementDayCal = Calendar.getInstance();
    
    // 日付ピッカーダイアログ起動イベントリスナー
    private final View.OnClickListener mDatePickerViewClickListener = this::showDatePicker;

    /**
     * 日付ピッカーダイアログを表示する
     */
    private void showDatePicker(View v) {
        DialogFragment newFragment = new DatePickerFragment(
                requireActivity(), mMeasurementDayCal, (view, year, month, dayOfMonth) -> {
            // 測定日付ウィジットを更新するためのタグ値を生成
            String tagValue = String.format(getString(R.string.format_tag_date),
                    year, month + 1/*カレンダー月 +1*/, dayOfMonth);
            DEBUG_OUT.accept(TAG, "showDatePicker:v.id=" + v.getId() + ",tag:" + tagValue);
            updateDateView((TextView) v, tagValue);
            // カレンダーオブジェクトを更新
            mMeasurementDayCal.set(year, month, dayOfMonth);
            // カレンダーで選択した日によって条件を満たさなければサーバーに問い合わせる
        });
        //--------------------------
        // 選択後の細かい処理は省略 
        //--------------------------
        newFragment.show(requireActivity().getSupportFragmentManager(), "DatePickerFragment");
    }
(2) 起床時刻をTimePickerFragmentから取得する ※ダイアログの生成と値の取得部分のみ抜粋
【TimePickerFragment】
    // 時刻ピッカーダイアログ起動イベントリスナー
    private final View.OnClickListener mTimePickerViewClickListener = v -> {
        if (v.getId() == R.id.inpMeasurementTime) {
            // 血圧測定時刻([時間帯] 午前/午後): 各時間帯の入力値はウィジットの時間帯毎のキー付きTAGに設定
            showBloodPressureTimePicker(v);
        } else {
            // 起床時刻, 睡眠時間, 深い睡眠, 体温測定時刻
            showTimePicker(v);
        }
    };

    /**
     * 時刻ピッカーダイアログ用の時刻データを生成する
     */
    private TimePickerFragment.TimeHolder createTimeHolder(String tagTime) {
        String[] times = tagTime.split(":");
        int hour = Integer.parseInt(times[0]);
        int minute = Integer.parseInt(times[1]);
        return new TimePickerFragment.TimeHolder(hour, minute);
    }

    /**
     * 時刻ピッカーダイアログに必要な引数を保持するデータを生成する
     */
    private TimePickerFragment.TimeHolder getTimeHoler(View v, int tagId) {
        String tagValue = (String) v.getTag(tagId);
        TimePickerFragment.TimeHolder holder;
        if (!tagValue.equals(getString(R.string.init_tag_time_value))) {
            holder = createTimeHolder(tagValue);
        } else {
            // 午前 / 午後
            String timeClassValue;
            if (mSelectedRadioId == mRadioMorning.getId()) {
                timeClassValue = getString(R.string.time_class_am);
            } else {
                timeClassValue = getString(R.string.time_class_pm);
            }
            holder = createTimeHolder(timeClassValue);
        }
        return holder;
    }

    /**
     * 時刻設定ダイアログを表示する
     */
    private void showTimePicker(View v) {
        TimePickerFragment.TimeHolder holder = getTimeHoler(v, v.getId());
        DialogFragment newFragment = new TimePickerFragment(
                requireActivity(), holder, (view, hourOfDay, minute) -> {
            //--------------------------
            // 選択後の細かい処理は省略  
            //--------------------------
            TextView tv = (TextView) v;
            updateTimeView(tv, tagValue, timeFormat);
            // 時刻入力ウィジットの変更通知
            mOnTimeViewChanged.onChanged(tv, tagValue);
        });
        newFragment.show(requireActivity().getSupportFragmentManager(), "TimePickerFragment");
    }
(3) 睡眠スコアをNumberPickerDialogから取得する ※ダイアログの生成と値の取得部分のみ抜粋
【NumberPickerDialog】
    // NumberPickerDialog起動イベントリスナー
    private final View.OnClickListener mNumberPickerClickListener = v -> {
        // デフォルトのTAG値から入力対象のキーを取得する
        int viewTag = (Integer) v.getTag();
        String title = mAlertDialogTitles[viewTag];
        String label = mNumberPickerLabels[viewTag];
        String unit = mNumberPickerUnits[viewTag];
        TextView inpView = mNumberPickerViews[viewTag];
        // 初期値は数値系入力ウィジットから取得
        Integer initValue = getInitNumberValueOfTextView(inpView);
        // NumberPickerDialogに引き渡す最小値, 最大値
        String strRange = mNumberPickerRanges[viewTag];
        String[] strRanges = strRange.split(",");
        int minValue = Integer.parseInt(strRanges[0]);
        int maxValue = Integer.parseInt(strRanges[1]);
        // NumberPickerDialogを表示する
        showNumberPickerDialog(title, inpView, label, unit, initValue, minValue, maxValue);
    };

    /**
     * 数値ピッカーダイアログを表示する
     * @param dialogTitle ダイアログタイトル
     * @param inpView 入力対象のウィジット
     * @param lbl 入力対象のタイトルラベル
     * @param unit 入力対象の単位ラベル
     * @param initValue 入力対象の初期値
     * @param minValue 入力対象の最小値
     * @param maxValue 入力対象の最大値
     */
    private void showNumberPickerDialog(
            String dialogTitle,
            TextView inpView, String lbl, String unit,
            int initValue, int minValue, int maxValue) {
        // ダイアログ可変項目情報生成
        NumberPickerDialog.DialogItem item = new NumberPickerDialog.DialogItem(
                dialogTitle, lbl, unit, initValue, minValue, maxValue
        );
        // 選択値取得リスナー
        NumberPickerDialog.ValueListener listener = new NumberPickerDialog.ValueListener() {
            @Override
            public void onDecideValue(int number) {
                // 選択肢た数値を文字列に変換してTextViewに設定
                String sNumber = String.valueOf(number);
                inpView.setText(sNumber);
                  //---------------------------
                  // 選択後の細かい処理は省略  
                  //---------------------------
                }
            }
            @Override
            public void onCancel() {/* No ope*/}
        };
        // NumberPickerDialog生成
        NumberPickerDialog picker = new NumberPickerDialog(requireActivity(), item, listener);
        // NumberPickerDialog表示
        picker.createNumberPickerDialog().show();
    }
メニューページへ
戻る
Androidアプリのソースコードはこちら
https://github.com/pipito-yukio/personal_healthcare/tree/main/src/android-health-care-example