30分単位の時刻を選択するTimePicker


こんな感じのダイアログで30分単位の時刻を選択するやつを作ったので書いておく。
![time_picker_dialog.png](https://qiita-image-store.s3.amazonaws.com/0/47389/10c72774-64e6-d23f-3254-eea741d14d73.png)



- Fragmentから呼び出して使う（DialogFragmentベース）
- Gingerbread（API Level 10）でも使える
- 選択した時と分をintで取得
- 初期値をCalenderか、intの時と分で指定できる
- v11以上ではNumberPickerを使う
- v10以下ではSpinnerを使う



## 使い方

ダイアログを開くときはこんな感じで呼び出す。
初期値はCalendarで渡すか、 set(int hour, int minute)で渡す。

```java
TimePickerFragment picker = TimePickerFragment.newInstance(this);
picker.set(calendar);
picker.show(getFragmentManager(), "time_picker");
```

選択された値は、呼び出し元FragmentのonTimeSetで受け取る。

```java:HogeFragment
public class HogeFragment extends Fragment implements TimePickerFragment.TimePickerListener {
    @Override
    public void onTimeSet(int hourOfDay, int minute) {
        // 選択された時、分がhourOfDay、minuteに入ってくる
    }
}
```



## 本体コード

DialogFragmentの全体はこんな感じ。

```java:TimePickerFragment.java
public class TimePickerFragment extends DialogFragment {

    public interface TimePickerListener {
        void onTimeSet(int hourOfDay, int minute);
    }

    public static TimePickerFragment newInstance(Fragment fragment) {
        TimePickerFragment dialog = new TimePickerFragment();
        dialog.setTargetFragment(fragment, 0);
        return dialog;
    }

    private TimePickerListener getListener() {
        try {
            return (TimePickerListener) getTargetFragment();
        } catch (ClassCastException e) {
            return null;
        }
    }

    private View hour;
    private View minute;

    public void set(int hour, int minute) {
        Bundle args = getArguments();
        if (args == null) {
            args = new Bundle();
        }
        args.putInt("hour", hour);
        args.putInt("minute", minute);
        setArguments(args);
    }

    public void set(Calendar calendar) {
        this.set(calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE));
    }

    private View createPickerView() {
        View view = LayoutInflater.from(getActivity()).inflate(R.layout.time_picker, null);

        int hourValue;
        int minuteValue;
        Bundle args = getArguments();
        if (args != null && args.containsKey("hour")) {
            hourValue = args.getInt("hour");
            minuteValue = args.getInt("minute");
        } else {
            // 初期値が指定されなかった場合は現在時刻
            Calendar now = Calendar.getInstance();
            hourValue = now.get(Calendar.HOUR_OF_DAY);
            minuteValue = now.get(Calendar.MINUTE);
        }
        // 30分刻みに繰り上げる
        if (minuteValue == 0) {
            minuteValue = 0;
        } else if (minuteValue <= 30) {
            minuteValue = 30;
        } else {
            minuteValue = 0;
            hourValue = (hourValue + 1 > 23 ? 0 : hourValue + 1);
        }

        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
            NumberPicker hour = (NumberPicker) view.findViewById(R.id.picker_hour);
            hour.setMinValue(0);
            hour.setMaxValue(23);
            hour.setValue(hourValue);
            this.hour = hour;

            NumberPicker minute = (NumberPicker) view.findViewById(R.id.picker_minute);
            minute.setMinValue(0);
            minute.setMaxValue(5);
            minute.setDisplayedValues(new String[]{"00", "30", "00", "30", "00", "30"});
            minute.setValue(minuteValue > 29 ? 1 : 0);
            this.minute = minute;
        } else {
            Spinner hour = (Spinner) view.findViewById(R.id.picker_hour);
            hour.setSelection(hourValue);
            this.hour = hour;

            Spinner minute = (Spinner) view.findViewById(R.id.picker_minute);
            minute.setSelection(minuteValue > 29 ? 1 : 0);
            this.minute = minute;
        }

        return view;
    }

    private int getHour() {
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
            NumberPicker picker = (NumberPicker) hour;
            return picker.getValue();
        } else {
            Spinner spinner = (Spinner) hour;
            return Integer.parseInt((String) spinner.getSelectedItem());
        }
    }

    private int getMinute() {
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
            NumberPicker picker = (NumberPicker) minute;
            return (picker.getValue() % 2) == 1 ? 30 : 0;
        } else {
            Spinner spinner = (Spinner) minute;
            return Integer.parseInt((String) spinner.getSelectedItem());
        }
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        super.onCreateDialog(savedInstanceState);

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setView(createPickerView());
        builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                TimePickerListener listener = getListener();
                if (listener != null) {
                    listener.onTimeSet(getHour(), getMinute());
                }
            }
        });
        builder.setNegativeButton(android.R.string.cancel, null);
        builder.setCancelable(true);
        return builder.create();
    }
}
```



ポイントとしては、"00"と"30"の2値だけだと、クルクル～ってスクロールしないので、00と30を数回繰り返した値にしてる。

```java
minute.setDisplayedValues(new String[]{"00", "30", "00", "30", "00", "30"});
```



Layoutファイルはv11以上向けのNumberPicker版とv10以下向けのSpinner版を用意

```xml:layout-v11/time_picker.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="horizontal">
        <NumberPicker
            android:id="@+id/picker_hour"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:text=":"
            android:textSize="24sp" />
        <NumberPicker
            android:id="@+id/picker_minute"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </LinearLayout>
</FrameLayout>
```

```xml:layout/time_picker.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="horizontal"
        android:padding="16dp">
        <Spinner
            android:id="@+id/picker_hour"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:entries="@array/picker_item_hour"
            android:gravity="center_vertical|right" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginLeft="8dp"
            android:layout_marginRight="8dp"
            android:gravity="center"
            android:text=":"
            android:textColor="#ffffff"
            android:textSize="24sp" />
        <Spinner
            android:id="@+id/picker_minute"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:entries="@array/picker_item_minute"
            android:gravity="center_vertical|left" />
    </LinearLayout>
</FrameLayout>
```

```xml:values/picker_item.xml
<resources>
    <string-array name="picker_item_hour">
        <item>0</item>
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
        <item>5</item>
        <item>6</item>
        <item>7</item>
        <item>8</item>
        <item>9</item>
        <item>10</item>
        <item>11</item>
        <item>12</item>
        <item>13</item>
        <item>14</item>
        <item>15</item>
        <item>16</item>
        <item>17</item>
        <item>18</item>
        <item>19</item>
        <item>20</item>
        <item>21</item>
        <item>22</item>
        <item>23</item>
    </string-array>
    <string-array name="picker_item_minute">
        <item>00</item>
        <item>30</item>
    </string-array>
</resources>
```