TimePickerDialogのonTimeSet()が2回連続でコールされてしまう件の回避例

時間設定の便利なコンポーネントとしてTimePickerDialogがありますが、このダイアログはSDKバージョン4.1.x以降から想定外の動作をするケースがあります。今回はそれに対する回避策の例をメモ書きします。

まず、TimePickerDialogというのはどういうものかというと、以下のようなUIとして画面上に表示されます。

 

このダイアログで設定(OK)ボタンをタップすると、onTimeSet()というイベントリスナー関数が呼ばれ、この中で設定時刻が関数パラメータ経由で渡されてきます。そして、任意の処理が実行されるわけです。任意の処理というのは、例えば設定時刻をDBに登録したりとか、それはアプリの機能仕様に従ってお好きなように記述します。

ここで問題なのですが、SDKバージョン4.1.x(Jelly Bean)以降から、このダイアログの設定ボタンをタップすると、なぜかonTimeSet()が二回連続で実行されるようになりました。もしDBへのデータ登録処理を書いていれば、データ登録が二重に実行されてしまいます。APIのバグらしき現象。

この問題が修正されるまで待っているわけにもいかないと思いますので、とりあえず回避策を考えてみました。もちろん、ここで書く回避策は単なる一つの例ですので、他にもやりようによっては様々な方法が考えられるでしょう。

私の場合、回避策として以下の方針を考えました。TimePickerDialogを継承したクラスを作成し、そこで設定時刻取得の仕組みを新たに作るという考え方です。

  1. onTimeSet()の中では何もせず、代わりに設定ボタンのリスナーを再定義して、その中で実行したい処理を行う。
  2. 設定時刻を取得するために、TimePickerDialogを継承したクラスにて、時刻選択するたびに時刻をメンバ変数に保持する。
  3. 設定ボタンをタップした時、つまり1.のリスナーの中で、2.で保持されている時刻を取得する。
  4. 3.で時刻を取得した後に、DBへデータを書き込むなり任意の処理を行う。

この方針をソースコードに反映すると、以下のようになります。

// 設定時刻を格納するための変数(TimeDataクラスの定義は後述)
private TimeData mTimeData; 

//onCreateViewの中などでインスタンスを取得
 mTimeData = TimeData.getInstance();
// アプリの中で時刻設定を実行するときに呼ばれるメソッド
private void setTime() {
    
    TimePickerDialog.OnTimeSetListener listener = new TimePickerDialog.OnTimeSetListener() {
        // 問題のコールバック関数。ここではあえて何もしない。
        public void onTimeSet(TimePicker view, int hourOfDay, int minute) {
            /** DO NOTHING */
        }
    };

    // TimePickerDialogを継承したカスタムクラスのインスタンスを取得
    if (timePickerDialog == null) {
        timePickerDialog = new CustomTimePickerDialog(getActivity(), listener, 0, 0, true);
        // TimeDataのインスタンスを渡し、時刻選択される毎に時刻データが格納されることを期待
        timePickerDialog.setTimeData(mTimeData);
    }

    // ボタンの定義
    timePickerDialog.setButton(
            // onTimeSet()はBUTTON_POSITIVEのタップでコールされる
            DialogInterface.BUTTON_POSITIVE,
            "設定",
            new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    // ボタンがクリックされた時の動作
                    if (mTimeData != null) {
                        // 時刻を取得する
                        int hour = mTimeData.getHour();
                        int minute = mTimeData.getMinute();
                        //
                        // ここで任意の処理を記述する
                        //
                    }
                }
            }
            );

    timePickerDialog.show();
}
// TimePickerDialogを継承したカスタムクラス
// (TimeSettingは自前のインターフェスで後述にて定義)
public class CustomTimePickerDialog extends TimePickerDialog implements TimeSetting {

    private TimeData mTimeData;

    public CustomTimePickerDialog(Context context, OnTimeSetListener callBack,
            int hourOfDay, int minute, boolean is24HourView) {
        super(context, callBack, hourOfDay, minute, is24HourView);
    }

    // 時刻が選択される度にコールされるリスナー関数
    @Override
    public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
        super.onTimeChanged(view, hourOfDay, minute);
        if(mTimeData != null) {
            // 現在選択している時刻を格納
            mTimeData.setHour(hourOfDay);
            mTimeData.setMinute(minute);
        }
    }

    @Override
    public TimeData getTimeData() {
        return mTimeData;
    }

    @Override
    public void setTimeData(TimeData data) {
        mTimeData = data;
    }
}
// 設定時刻を格納するためのクラス
public class TimeData {
    private int mHour = 0;
    private int mMinute = 0;

    private static TimeData instance;

    private TimeData() {
    };

    public static synchronized TimeData getInstance() {
        if (instance == null) {
            instance = new TimeData();
        }
        return instance;
    }

    public int getMinute() {
        return mMinute;
    }

    public void setMinute(int minute) {
        this.mMinute = minute;
    }

    public int getHour() {
        return mHour;
    }

    public void setHour(int hour) {
        this.mHour = hour;
    }
}
public interface TimeSetting {
    public abstract TimeData getTimeData();
    public abstract void setTimeData(TimeData data);
}

以上のコードで、一応は今回の問題を回避できました。ちなみに、上記コードでsetButtonでボタン定義するとonTimeSet()は一度コールされ、設定しなければ二度コールされてしまうという謎挙動です。