質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

91.03%

  • Android

    5834questions

    Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

  • Kotlin

    173questions

    Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

  • RxJava

    4questions

RxPropertyでイベントの発生順を制御したい

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 317

nakasho_dev

score 1204

前提・実現したいこと

RxProperty for Android の初心者です。KotlinでAndroidアプリを開発しています。

AndroidのDatePickerとTimePickerを使い時刻を指定する機能を作成しています。
現在時刻設定ボタンを設定し、ボタンをクリックしたら二つのPickerを現在時刻に合わせるとともに現在時刻を設定しているという状態(このサンプルではbuttonEnabled)を保持したいです。tapButton()がその処理です。
そして二つのPickerのいずれかが動かされたら、現在時刻を設定しているという状態を無効にしたいです。

【追記】
条件について強調していなかったのですが、「現在時刻を設定している」という状態はスピナーで現在時刻に設定しても有効にはしなくないです。
具体的にはYahoo乗換案内アプリでは時刻選択画面で現在時刻をタップしてホーム画面に戻った時のみ「現在時刻」という文字列が表示され、それ以外はスピナーで表示されていた固定の時刻が表示されています。これはスピナーを回して現在の時刻に設定しても固定の時刻が表示されます。
この「現在時刻」が設定されている状態で検索すると、検索ボタンを押した時刻を指定時刻として検索されます。
これを実現するためにはPickerでイベントが発生した際に保存している時刻と比較して同じだったら、という手法は適用できません。

発生している問題・エラーメッセージ

現在時刻設定ボタンをクリックしてPickerの値を現在時刻に設定し、現在時刻を設定しているという状態を有効にする(buttonEnabled.set(false))処理が走るのですが、その後にPickerのOnDateChanged、または、OnTimeChangedが実行されて現在時刻を設定しているという状態が無効(buttonEnabled.set(true))になります。

01-15 14:22:06.579 D/MainViewModel: tapButton finished
01-15 14:22:06.596 D/MainViewModel: changeDateTimePicker finished
01-15 14:22:06.597 D/MainViewModel: changeDateTimePicker finished

tapButton内の以下の処理がchangeDateTimePickerが終了した後に実行されるように制御したいです。

   buttonText.set("現在時刻設定中")
   buttonEnabled.set(false)


または、現在時刻設定ボタンのクリックイベント時での時刻指定ではonDateChanged, onTimeChangedが発生しないようにしたいです。

該当のソースコード

GitHubにコミットしています。

現在設定時刻ボタンのonClickイベント処理(tabButton)とPickerのOnDateChanged、OnTimeChangedのイベント処理(changeDateTimePicker)を以下に示します。

    //Pickerを現在時刻に設定する
    fun tapButton() {
        val calendar = Calendar.getInstance()
        year.set(calendar.get(Calendar.YEAR))
        month.set(calendar.get(Calendar.MONTH))
        day.set(calendar.get(Calendar.DAY_OF_MONTH))
        hour.set(calendar.get(Calendar.HOUR))
        minute.set(calendar.get(Calendar.MINUTE))

        // 現在時刻設定中なのでボタンテキストを変更、および、ボタンを無効化
        buttonText.set("現在時刻設定中")
        buttonEnabled.set(false)
        Log.d("MainViewModel", "tapButton finished")
    }

    //Pickerが動かされたら現在時刻から変更されたとしてボタンを有効化、テキストを戻す
    fun changeDateTimePicker() {
        buttonText.set("現在時刻を設定")
        buttonEnabled.set(true)
        Log.d("MainViewModel", "changeDateTimePicker finished")
    }

試したこと

Pickerに対しonDateChangedやonTimeChangedではなくonClickなどで、イベントを取れれば、現在時刻設定ボタンでの時刻指定でのイベントと重ならないとも考えたのですが、PickerにはonClickが実装されておらず実現できません。
tapButtonの処理中はEventListenerを無効にするという方法も考えましたが乱暴な気がします。
どのような対処が、より良いコードとなるのかが分からない状況です。

補足情報(言語/FW/ツール等のバージョンなど)

Android Studio 3.0.1
Kotlin 1.1.51
io.reactivex.rxjava2:rxjava:2.0.7
com.github.k-kagurazaka.rx-property-android:rx-property:4.0.0
com.github.k-kagurazaka.rx-property-android:rx-property-kotlin:4.0.0
io.reactivex.rxjava2:rxkotlin:2.1.0

サンプルアプリキャプチャ

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+2

こんな感じでしょうか。

まず、 MainViewModel は、 DatePicker と TimePicker で入力された日付時刻を保持することに専念(_dateTimeStr)します。_dateTimeStr には、DatePicker の onDateChanged、 TimePicker の onTimeChanged を素直に受信して、それらから YYYYMMDDHHMM な文字列を設定します(※YYYYMMDDHHMMな文字列にしたのはデバッグが分かりやすかっただけです、ミリ秒とか Date/Calendar クラスでもよいと思います。)。

次に TwoWay Binding はやめて、 _dateTimeStr から 年月日と時刻それぞれの ReadonlyRxProperty を生成し、OneWay で DatePicker と TimePicker にバインドさせます。

最後に、ボタンを押した時に、その時点での現在日時を保存しておき(_savedCurrentDateTime)、ボタンのタイトルや使用可否は、保存された現在日時とDatePicker+TimePickerで入力された日時が一致しているかどうかで定義された ReadOnlyRxProperty として用意します。

データバインドする ReadOnlyRxProperty は全て _dateTimeStr から生成しているので、 _dateTimeStr が変われば連動して変化するのをイメージすると分かりやすいと思います。
そして _dateTimeStr を変化させるのは DatePicker/TimePicker を操作した時、またはボタンを押したとき、です。

 MainViewModel.kt

package nakasho.github.io.datepickersample

import android.arch.lifecycle.ViewModel
import android.text.TextUtils
import android.util.Log
import jp.keita.kagurazaka.rxproperty.RxProperty
import jp.keita.kagurazaka.rxproperty.toReadOnlyRxProperty
import java.util.*

class MainViewModel : ViewModel() {
    private var _savedCurrentDateTime: String = "" // ボタン押下時の現在日時を保存(YYYYMMDDHHMM)
    private val _dateTimeStr = RxProperty<String>() // DatePicker+TimePicker に表示する日付文字列(YYYYMMDDHHMM)

    private var _buttonTapped: Boolean = false // 現在時刻ボタンが押されたらtrueに

    // YYYYMMDDHHMM から各成分に該当する桁を数値に変換してバインディング用の RxProperty とする
    val year = _dateTimeStr.map { dt -> dt.substring(0, 4) }.map { it->it.toInt() }.toReadOnlyRxProperty()
    val month = _dateTimeStr.map { dt -> dt.substring(4, 6) }.map { it->it.toInt() - 1 }.toReadOnlyRxProperty() // month は 0 始まりなので
    val day = _dateTimeStr.map { dt -> dt.substring(6, 8) }.map { it->it.toInt() }.toReadOnlyRxProperty()
    val hour = _dateTimeStr.map { dt -> dt.substring(8, 10) }.map { it->it.toInt() }.toReadOnlyRxProperty()
    val minute = _dateTimeStr.map { dt -> dt.substring(10, 12) }.map { it->it.toInt() }.toReadOnlyRxProperty()

    // 現在時刻ボタンが押され、ボタン押下時の現在日時と DatePicker+TimePickerの日時が同じなら現在時刻設定中であり enabled=false とする
    val buttonEnabled = _dateTimeStr.map { dt -> !_buttonTapped && !TextUtils.equals(dt, _savedCurrentDateTime) }.toReadOnlyRxProperty()
    val buttonText = buttonEnabled.map { enable -> if (enable) "現在日時を設定" else "現在時刻設定中" }.toReadOnlyRxProperty()

    init {
        _dateTimeStr.set("201712312345") // DatePicker+TimePickerの既定値(YYYYMMDDHHMM)
    }

    //Pickerを現在時刻に設定する
    //onClickが発生したら実行
    fun tapButton() {
        _buttonTapped = true
        val cal = Calendar.getInstance()
        val dateTimeStr = String.format("%4d%02d%02d%02d%02d",
                cal.get(Calendar.YEAR),
                cal.get(Calendar.MONTH) + 1, // month は 0 始まりなので
                cal.get(Calendar.DAY_OF_MONTH),
                cal.get(Calendar.HOUR),
                cal.get(Calendar.MINUTE))
        _savedCurrentDateTime = dateTimeStr
        _dateTimeStr.set(dateTimeStr)
    }

    fun changeDate(year:Int, monthIndex:Int, day:Int) {
        val dateStr = String.format("%4d%02d%02d", year, monthIndex+1, day) // month は 0 始まりなので
        Log.d("MainViewModel", "changeDate ${dateStr} finished")

        // 日付部を変更して更新
        var dateTimeStr = _dateTimeStr.get()
        _dateTimeStr.set(dateStr + dateTimeStr.substring(8))
    }

    fun changeTime(hour:Int, minute:Int) {
        val timeStr = String.format("%02d%02d", hour, minute)
        Log.d("MainViewModel", "changeTime ${timeStr} finished")

        // 時刻部を変更して更新
        var dateTimeStr = _dateTimeStr.get()
        _dateTimeStr.set(dateTimeStr.substring(0, 8) + timeStr)
    }
}

 activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
            name="viewModel"
            type="nakasho.github.io.datepickersample.MainViewModel"/>
    </data>

    <android.support.constraint.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="nakasho.github.io.datepickersample.MainActivity">
        <DatePicker
            android:id="@+id/datePicker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:calendarViewShown="false"
            android:datePickerMode="spinner"
            android:spinnersShown="true"
            android:year="@{viewModel.year.value}"
            android:month="@{viewModel.month.value}"
            android:day="@{viewModel.day.value}"
            android:onDateChanged="@{(view, year, month, day)-> viewModel.changeDate(year, month, day)}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <TimePicker
            android:id="@+id/timePicker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:timePickerMode="spinner"
            android:hour="@{viewModel.hour.value}"
            android:minute="@{viewModel.minute.value}"
            android:onTimeChanged="@{(view, hour, minute)-> viewModel.changeTime(hour, minute)}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/datePicker" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{viewModel.buttonText.value}"
        android:onClick="@{()-> viewModel.tapButton()}"
        android:enabled="@{viewModel.buttonEnabled.value}"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/timePicker"
        app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>
</layout>

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/01/16 12:21

    回答ありがとうございます。
    KotlinやRxPropertyの記述の仕方についても勉強になりました。

    「現在時刻を設定している」という状態について説明不足だったので追記しました。
    Yahoo乗換案内では実現している機能なので何かしら実現方法はあると考えています。

    キャンセル

  • 2018/01/16 12:53

    「現在時刻を設定している」について、ちょっとコードを修正してみました。ボタンが押されたことをフラグで保持しておいて、buttonEnabled の定義で、そのフラグを加味することで実現できそうな気がしますね。

    キャンセル

  • 2018/01/16 20:03

    迅速な回答ありがとうございました。非常に助かりました。
    Pickerを回したり現在時刻ボタンを押したりを繰り返し操作されることを想定しているため、_buttonTappedをfalseにする機会が必要だったので、tapButtonの最後に実行したら期待通りの動作をしました。
    ただ、実機では問題ないのですが、エミュレータだと_buttonTappedをfalseにする処理がイベント処理よりも先になってしまうため想定通りの操作とならないことがありました。HandlerクラスのpostDelayedで300ミリ秒くらい遅らせるとエミュレータでも想定通りの動作となりました。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 91.03%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • Android

    5834questions

    Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

  • Kotlin

    173questions

    Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

  • RxJava

    4questions