2012年8月7日火曜日

TimePickerDialogをFragmentで実装する(処理編)

チームEGGの曽川です。

前回はTimePickerDialogをFragmentで実装しました。しかし、GingerBreadまでの動作は正しいことを確認しましたが、Jelly Beanでは以下の点で動作が想定外でした。(見た目には影響ありませんが...)
・縦横切替でonTimeSetが呼び出される
・バックキーを押すとonTimeSetが呼び出される
・完了を押すとonTimeSetが2回呼び出される
また、DialogFragmentはボタンの連打等で2つ以上のダイアログが重なる場合があります。そのため、以下の要件を満たすTimePickerFragmentを作成しました。
・完了時に1回だけonTimeSetが呼び出される
・連打されても1つしかダイアログが出ない
上記を満たしたコードは以下のとおりです。
public abstract class TimePickerFragment extends ExclusiveDialogFragment implements TimePickerDialog.OnTimeSetListener {

    /** キャンセルフラグ */
    private boolean mIsCancel;
    /** 時間設定コールバック通過フラグ */
    private boolean mIsTimeChange;
    /** フラグメント破棄フラグ */
    private boolean mIsDestory;
    /** 排他的にダイアログが消去されたフラグ */
    private boolean mIsExclusiveDialogDismiss;

    /** 時間設定コールバックで一時的に保存する時間 */
    private int mTmpHour;
    /** 時間設定コールバックで一時的に保存する分 */
    private int mTmpMinute;

    /**
     * {@inherited}
     */
    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mIsCancel = false;
        mIsTimeChange = false;
        mIsDestory = false;
        mIsExclusiveDialogDismiss = false;
        mTmpHour = 0;
        mTmpMinute = 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Dialog onCreateDialog(final Bundle savedInstanceState) {
        // プリファレンス読み込み
        final Activity activity = getActivity();

        int theme;
        // GingerBread以前
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
            theme = android.R.style.Theme_Panel;
        } else {
            // HoneyComb以降
            theme = android.R.style.Theme_Holo_Panel;
        }
        // 24時間設定にしている場合はそちらに合わせる
        return new TimePickerDialog(activity, theme, this, getHour(), getMinute(), DateFormat.is24HourFormat(activity));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onTimeSet(final TimePicker view, final int hourOfDay, final int minute) {
        // すでに時間を変更している場合は変更しない
        if (mIsTimeChange) {
            return;
        }

        // JBで完了時にコールバックが2回来る、キャンセル時にコールバックが来る、
        // という2つの事象があったため一時的に値を保存するように修正
        mTmpHour = hourOfDay;
        mTmpMinute = minute;
        mIsTimeChange = true;
    }

    /**
     * {@inherited}
     */
    @Override
    public void onCancel(final DialogInterface dialog) {
        super.onCancel(dialog);
        mIsCancel = true;
    }

    /**
     * {@inherited}
     */
    @Override
    public void onDismiss(final DialogInterface dialog) {
        super.onDismiss(dialog);

        // 時間の更新がない場合は変更しない
        if (mIsTimeChange) {
            return;
        }

        // キャンセル時は処理なし
        if (mIsCancel) {
            return;
        }

        // バックグラウンドで破棄された場合は処理なし
        if (mIsDestory) {
            return;
        }

        // 排他的にダイアログが消去された場合は処理なし
        if (mIsExclusiveDialogDismiss) {
            return;
        }

        onUpdateTime(mTmpHour, mTmpMinute);
    }

    /**
     * {@inherited}
     */
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        mIsDestory = true;
    }

    /**
     * {@inherited}
     */
    @Override
    protected void onDismissExclusiveDialog() {
        super.onDismissExclusiveDialog();
        mIsExclusiveDialogDismiss = true;
    }

    /**
     * TimePickerで扱う時間を取得します。
     * 
     * @return 時間
     */
    protected abstract int getHour();

    /**
     * TimePickerで扱う分を取得します。
     * 
     * @return 分
     */
    protected abstract int getMinute();

    /**
     * 時間設定を通知します。
     * 
     * @param hour
     *            時間
     * @param minute
     *            分
     */
    protected abstract void onUpdateTime(final int hour, final int minute);

}
長々と書いてはいますが、このコードではダイアログが完了ボタンを押して消えるときに、一度だけonUpdateTimeというメソッドを呼び出します。
/** キャンセルフラグ */
    private boolean mIsCancel;
    /** 時間設定コールバック通過フラグ */
    private boolean mIsTimeChange;
    /** フラグメント破棄フラグ */
    private boolean mIsDestory;
    /** 排他的にダイアログが消去されたフラグ */
    private boolean mIsExclusiveDialogDismiss;
    .....
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mIsCancel = false;
        mIsTimeChange = false;
        mIsDestory = false;
        mIsExclusiveDialogDismiss = false;
        mTmpHour = 0;
        mTmpMinute = 0;
    }
フィールドで、完了ボタン以外で消える条件をフラグとして持っています。 ・バックキーなどでのキャンセル(mIsCancel) ・OSのメモリ管理により消された(mIsDestory) ・ダイアログが複数出ようとして消された(mIsExclusiveDialogDismiss) もう一つは、onTimeSetが2回実行されないようにする実行済フラグです。 (※通常、フラグメント内で永続化したい変数はフィールドに持たせませんが、ここではライフサイクル上で一度しか使わないため、フィールドとして定義しています。) あとは、それぞれのフラグを適切なタイミングで立てています。 バックキーなどでのキャンセル(mIsCancel)
public void onCancel(final DialogInterface dialog) {
        super.onCancel(dialog);
        mIsCancel = true;
    }
OSのメモリ管理により消された
public void onDestroyView() {
        super.onDestroyView();
        mIsDestory = true;
    }
ダイアログが複数出ようとして消された
protected void onDismissExclusiveDialog() {
        super.onDismissExclusiveDialog();
        mIsExclusiveDialogDismiss = true;
    }
onTimeSetが実行された
public void onTimeSet(final TimePicker view, final int hourOfDay, final int minute) {
        .....
        mIsTimeChange = true;
    }
onTimeSet内では2回目が呼び出されないようにガードして、時間の更新をします。
public void onTimeSet(final TimePicker view, final int hourOfDay, final int minute) {
        if (mIsTimeChange) {
            return;
        }

        mTmpHour = hourOfDay;
        mTmpMinute = minute;
        mIsTimeChange = true;
    }
最後にonDismissでフラグでガードしつつonUpdateTimeを呼び出します。
public void onDismiss(final DialogInterface dialog) {
        super.onDismiss(dialog);

        // 時間の更新がない場合は変更しない
        if (mIsTimeChange) {
            return;
        }

        // キャンセル時は処理なし
        if (mIsCancel) {
            return;
        }

        // バックグラウンドで破棄された場合は処理なし
        if (mIsDestory) {
            return;
        }

        // 排他的にダイアログが消去された場合は処理なし
        if (mIsExclusiveDialogDismiss) {
            return;
        }

        onUpdateTime(mTmpHour, mTmpMinute);
    }
あとは、このTimePickerFragmentを継承するクラスを作成してください。

今回はExclusiveDialogFragmentを継承しましたが、複数ダイアログの表示を考慮しない場合はDialogFragmentを継承して構築してください。その場合Exclusive系のフラグやメソッドは削除します。
コードに不備がありましたので、修正しました。(2012/08/10)
・Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1の条件文
・onDismissメソッドでのmIsTimeChangeのチェック(2.3系のキャンセルボタン押下時にガード)

0 件のコメント:

コメントを投稿