［Android］ DataBindingでButtonのダブルタップ誤動作を解決する

## はじめに

Buttonをダブルタップしたときに処理が2回実行されてしまうことってありますよね。
例えばActivityの遷移やFragmentの生成など1回でよいときは非常に困ることがあります。
今回はそのような問題を解決するための処理を作成していこうと思います。
特にMVVMでDataBindingを使ったサンプルが無かったので共有できたらよいです…

## どのような問題が起きているか？

次のようなButtonをクリックするカウントが増減するアプリがあったとします。
見るとわかるとおりダブルタップすると2回カウントしてしまっています。

![click-low.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/75d159ed-6b4c-5f98-9838-62c48df083bc.gif)

ちなみにですがコードはだいたいこんな感じです。
DataBindingを使った場合の対処法ということでMVVMで作ってます。

### View

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val viewModel = ViewModelProviders.of(this).get(MainActivityViewModel::class.java)
        val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.vm = viewModel
        binding.lifecycleOwner = this
    }
}
```

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="vm" type="kaleidot725.safeclicksample.MainActivityViewModel"/>
    </data>

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            tools:context=".MainActivity">

        <TextView
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="6"
                android:layout_gravity="center"
                android:gravity="center"
                android:textSize="100dp"
                android:text="@{vm.count}"/>

        <Button
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:text="plus"
                android:onClick="@{vm::plus}"/>

        <Button
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:text="minus"
                android:onClick="@{vm::minus}"/>
    </LinearLayout>
</layout>
```

### ViewModel

```MainActivityViewModel
class MainActivityViewModel : ViewModel() {
    private val counter : Counter = Counter()
    private val _count : MutableLiveData<String> = MutableLiveData()
    val count : LiveData<String>  get() = _count

    init {
        _count.value = counter.num.toString()
    }

    fun plus(v : View) {
        counter.plus()
        _count.value = counter.num.toString()
    }

    fun minus(v : View) {
        counter.minus()
        _count.value = counter.num.toString()
    }
}
```

### Model

```kotlin
class  Counter {
    var num : Int = 0
        private set
        get

    fun plus() {
        num++
    }

    fun minus() {
        num--
    }
}
```

## この問題をどうやって解決するか？

調べますとダブルタップしたときに2回目の処理を実行させないために、
1000ms以内にButtonがもう一度押された場合は実行しないようにする方法があるようです。

[🔗 How to resolve double tap on Button Issue in android?
](https://stackoverflow.com/questions/16662777/how-to-resolve-double-tap-on-button-issue-in-android)

これをDataBindingを使って上手くやりたいので、1000ms以内にButtonが
もう一度押された場合に実行しないようにするBindingAdapterを作ります。
では実際に実装していってみたいと思います。

### kotlin-kaptを有効にする

BindingAdapterを作るときにはkotlin-kaptが有効になっていなければなりませんので、
build.gradle(Module:app)に次のないようを記述しておきます。

```build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' 

android {
    ︙
}
    ︙
```

### BindingAdapterを作成する

BindingAdatperを次のようにもし前回押した時刻から
1000ms経過していなければ何もせずにreturnする処理を実装します。

```MainBindings.kt
@BindingAdapter("app:onSafeClick")
fun onSafeClick(view: View, listener : View.OnClickListener) {

    class SafeClickListener(
        private var defaultInterval: Int = 1000,
        private val listener : View.OnClickListener) : View.OnClickListener
    {
        private var lastTimeClicked: Long = 0
        override fun onClick(v: View) {
            if (SystemClock.elapsedRealtime() - lastTimeClicked < defaultInterval) {
                return
            }
            lastTimeClicked = SystemClock.elapsedRealtime()
            listener.onClick(v)
        }
    }

    view.setOnClickListener(SafeClickListener(1000, listener))
}
```

### XMLのDataBindingを変更する

そしてあとはxmlでDataBindingしているonClickの記述を
onSafeClickに変更してあげれば完成です。

```main_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable name="vm" type="kaleidot725.safeclicksample.MainActivityViewModel"/>
    </data>

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            tools:context=".MainActivity">

        <TextView
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="6"
                android:layout_gravity="center"
                android:gravity="center"
                android:textSize="100dp"
                android:text="@{vm.count}"/>
                
        <Button
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:text="plus(safe)"
                app:onSafeClick="@{vm::plus}"/>

        <Button
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"
                android:text="minus(safe)"
                app:onSafeClick="@{vm::minus}"/>
    </LinearLayout>
</layout>
```

### 動作を確認する

実行してみますと次のような感じになります。
ダブルタップしても1回のみカウントしてますね。
1000msと大雑把に定義しましたがこれは変更が必要かもですね…

![safeclick-low.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/c3847abb-833c-2d64-1f4b-1594ed526327.gif)

## おわりに

この実装をしたサンプルは次のGitHubリポジトリにあります。
上手く実装できなかったり、詳細がわからない場合は閲覧ください！！

🔗[android-mylab/SafeClickSample](https://github.com/kaleidot725/android-mylab/tree/master/SafeClickSample)