〈Kivy〉シングルタップ・ダブルタップで別の動作をするボタンを作る

# 目標

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3099743/5516bd45-b193-bada-e6e7-588d9d004cd6.gif" width="700">

こんなふうに, シングルタップとダブルタップで別の動作をするボタンを作成する.  
具体的には, シングルタップで「+1」, ダブルタップで「数字を 0 に戻す」動作を行うボタンを実装する.

# サンプルコード（ダブルタップ実装前）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3099743/37cc0e85-95e7-af0f-5554-fd30745c70c3.gif" width="700">

左のボタンを押すと表示された数値に +1, 右のボタンを押すと表示された数値に +2 するシンプルなボタン.  
これを修正してダブルタップ処理を追加する.
以下にコードを記載する.

## 1. double_tap_sample.py

プログラムを動かすメインファイル.

```python
from kivy.app import App
from kivy.core.window import Window
from kivy.lang import Builder

from layout import DoubleTapSample

Window.size = (500, 500)
Builder.load_file('double_tap_sample.kv')


class DoubleTapSampleApp(App):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def build(self):
        return DoubleTapSample()

if __name__ == '__main__':
    DoubleTapSampleApp().run()
```

## 2. double_tap_sample.kv

Label に数字を表示, PlusOneButton と PlusTwoButton の on_press: で, それぞれ root.plus_one(), root.plus_two() を呼び出している.

```
#:kivy 1.2.0
#:import PlueOneButton layout.PlusOneButton
#:import PlueTwoButton layout.PlusTwoButton
#:import DoubleTapSample layout.DoubleTapSample

<DoubleTapSample>:
    id: double_tap_samle
    BoxLayout:
        orientation: 'vertical'
        BoxLayout:
            size_hint: (1., 0.5)
            Label:
                text: str(root.number)
                font_size: 100
        BoxLayout:
            size_hint: (1., 0.5)
            PlusOneButton:
                text: '+1'
                font_size: 100
                on_press: root.plus_one()
            PlusTwoButton:
                text: '+2'
                font_size: 100
                on_press: root.plus_two()
```

## 3. layout.py

各ボタンから呼び出された処理の具体的な内容を記載する python ファイル.
PlusOneButton, PlusTwoButton は Kivy に最初からあるボタンクラスを継承している.
```python
from kivy.uix.button import Button
from kivy.properties import NumericProperty
from kivy.uix.screenmanager import Screen


class DoubleTapSample(Screen):
    number = NumericProperty(0)

    def plus_one(self):
        self.number += 1

    def plus_two(self):
        self.number += 2

class PlusOneButton(Button):
    pass 

class PlusTwoButton(Button):
    pass
```

# サンプルコード（実装後）

改良後のサンプルコードがこちら.
左のボタンをシングルタップすると表示された数字に+1, ダブルタップで数字を 0 に戻せている.

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3099743/a0781a5b-a7e5-cfac-b860-8e0a3faef743.gif" width="700">

## 1. double_tap_sample.py

前回と変化がないので省略

## 2. double_tap_sample.kv

```
#:kivy 1.2.0
#:import DoubleClickButton layout.DoubleClickButton
#:import PlusOneButton layout.PlusOneButton
#:import PlueTwoButton layout.PlusTwoButton
#:import DoubleTapSample layout.DoubleTapSample

<DoubleTapSample>:
    id: double_tap_sample
    BoxLayout:
        orientation: 'vertical'
        BoxLayout:
            size_hint: (1., 0.5)
            Label:
                text: str(root.number)
                font_size: 100
        BoxLayout:
            size_hint: (1., 0.5)
            PlusOneButton:
                id: plus_one_button
                text: '+1'
                font_size: 100
            PlusTwoButton:
                text: '+2'
                font_size: 100
                on_press: root.plus_two()

<DoubleClickButton>:
    background_color:(0, 0, 0, 0)
    background_normal:''
    canvas.before:
        Color: 
            rgba: (0/256, 175/256, 216/256, 1) if self.touched else (88/256,88/256,88/256,1)
        Rectangle: 
            pos: self.pos
            size: self.size
```

ダブルタップを実装したので ```on_press: root.plus_one()``` がなくなっている.
タップ時にボタンの色を変えるために

```
<DoubleClickButton>:
    background_color:(0, 0, 0, 0)
    background_normal:''
    canvas.before:
        Color: 
            rgba: (0/256, 175/256, 216/256, 1) if self.touched else (88/256,88/256,88/256,1)
        Rectangle: 
            pos: self.pos
            size: self.size
```

を追加したのが大きな変化である.

## 3. layout.py

```python
from kivy.app import App
from kivy.uix.button import Button
from kivy.clock import Clock
from kivy.properties import NumericProperty, BooleanProperty
from kivy.uix.screenmanager import Screen

class DoubleTapSample(Screen):
    number = NumericProperty(0)

    def plus_one(self, *args):
        self.number += 1

    def plus_two(self):
        self.number += 2

    def reset_num(self):
        self.number = 0

class DoubleClickButton(Button):
    pass

class PlusOneButton(DoubleClickButton):

    scheduled_event = None
    touched = BooleanProperty(False)
    def on_touch_down(self, touch):
        if self.scheduled_event is not None:
            self.scheduled_event.cancel()
            self.scheduled_event = None
        if self.collide_point(*touch.pos):
            self.touched = True
            if touch.is_double_tap:
                App.get_running_app().root.reset_num()
            else:
                double_tap_wait_s = 0.2
                self.scheduled_event = Clock.schedule_once(App.get_running_app().root.plus_one, double_tap_wait_s)
    
    def on_touch_up(self, touch):
        if self.collide_point(*touch.pos):
            self.touched = False

class PlusTwoButton(Button):
    pass
```
まずは, 数値を 0 に戻すための ```def reset_num()``` を追加.
```on_touch_down()``` でタッチを検知し, ダブルタップかどうかを判別している（詳細は後述）. 
```touched = BooleanProperty(False)``` はタッチ中にボタンの色を変えるために使う. タッチされた時に, ```on_touch_down()``` 内で ```True``` になり, タッチ終了時に呼ばれる, ```on_touch_up()``` で ```False``` に戻す.

# 個人的にハマったところ

## 1. ダブルタップ時にシングルタップの処理が呼ばれないようにする

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3099743/5a5a1c37-ee50-fb40-c304-a62334e4e63c.gif" width="700">

ダブルタップの判定には, [kivyのドキュメンテーション](https://kivy.org/doc/stable/guide/inputs.html?highlight=double%20tap)のサンプルコードを参考にダブルタップを実装し, else 文でシングルタップ時の処理を書けば良いように思えるが...

```python
def on_touch_down(self, touch):
    if self.collide_point(*touch.pos):
        if touch.is_double_tap:
            App.get_running_app().root.reset_num()
           else:
            App.get_running_app().root.plus_one()
```

上のコードを実行すると**シングルタップ処理の後でダブルタップ処理が呼ばれてしまう**
参考にしたのは[Python3 & Kivy: React only to double tap "not" single tap](https://stackoverflow.com/questions/64741710/python-3-kivy-react-only-to-double-tap-not-single-tap)のコード

```python
def on_touch_down(self, touch):
    if self.scheduled_event is not None:
        self.scheduled_event.cancel()
        self.scheduled_event = None
        if self.collide_point(*touch.pos):
            self.touched = True
            if touch.is_double_tap:
                App.get_running_app().root.reset_num()
            else:
                double_tap_wait_s = 0.2
                self.scheduled_event = Clock.schedule_once(App.get_running_app().root.plus_one, double_tap_wait_s)
```

kivy の Clock 機能を使って, 0.2 秒後にシングルタップ時の処理が行われるように登録を行う.  
もし, 0.2 秒以内にもう一度タップされたら登録されたイベントは削除, ダブルタップ処理が行われる.  
また, ```if self.collide_point(*touch.pos):```の判定がないと, 画面のどこを押してもシングルタップ or ダブルタップ処理が行われてしまう.

## 2. 別クラスに記載された処理を呼び出す.

初めは, ```on_press: root.plus_one()```で root(DoubleTapSample クラス)に書かれた plus_one 処理を呼び出していた. 同じ処理を .py ファイルで書くために```App.get_running_app().root.reset_num()```を使っている. 
```def plus_one(self, *args):```は *args がないと clock takes 1 positional argument but 2 were given というエラーになる.

# まとめ

ダブルタップとシングルタップで処理を変えたいというときの実装方法がわかった.