Kivyでゲーム用のMenuを作る

(この記事を書いた当時は`RecycleView`をまだ扱った事がなかったので使わないで実装していますが、使った方が綺麗なコードになります。)

# Kivyにて以下のようにSlideして現れるMenuを作りました。

![final.gif](https://qiita-image-store.s3.amazonaws.com/0/144673/8ac4c260-882f-42a1-5b38-9729f9b9121a.gif)

上の図では分かりませんが、Menuの項目は６つ以上ありスクロールして選ぶことが出来るようになっています。

# まずはアニメーション抜きでMenuを作る

![Screenshot at 2017-03-23 19:34:34.png](https://qiita-image-store.s3.amazonaws.com/0/144673/ebd1581d-de25-2437-9a00-cc2c6b6e20c0.png)

```python3:
# -*- coding: utf-8 -*-

from kivy.base import runTouchApp
from kivy.lang import Builder
from kivy.factory import Factory

Builder.load_string(r'''
<MenuItem@ButtonBehavior+Label>:
    font_size: self.height
    canvas.before:
        Color:
            rgba: [.2, .2, .2, 1] if self.state == 'down' else [0, 0, 0, 0]
        Rectangle:
            pos: self.pos
            size: self.size
        Color:
            rgba: [1, 1, 1, 1]
        Line:
            rectangle: [self.x, self.y, self.width, self.height]
    on_press: print("'{}' button was pressed.".format(self.text))

<Menu@BoxLayout>:
    layout: id_layout
    orientation: 'vertical'
    padding: [5]
    spacing: 20
    Label:
        size_hint_y: 1
        text: 'Menu'
        font_size: self.height
    ScrollView:
        size_hint_y: 5
        do_scroll_x: False
        GridLayout:
            id: id_layout
            size_hint_y: None            # B
            height: self.minimum_height  # B
            cols: 1
            row_default_height: self.parent.height / 7  # A
            row_force_default: True                     # A
            spacing: 10
''')


def _test():
    menu = Factory.Menu()
    MenuItem = Factory.MenuItem
    add_widget = menu.layout.add_widget
    for text in 'Python Ruby C++ Smalltalk Javascript C HTML Swift'.split():
        add_widget(MenuItem(text=text))
    runTouchApp(menu)


if __name__ == '__main__':
    _test()
```

　MenuのInstanceを作りMenu.layout.add_widget()を呼ぶことでMenuの項目を追加する事ができます。ここではMenuItemというクラスをわざわざ用意してはいますが、別に何のWidgetでも構いません。
　実装ですが、Menu項目の親にBoxLayoutではなくGridLayoutを使っているのには理由があり、それはA行B行で使っている便利なPropertyの為です。
　A行は子Widgetの高さを強制的にScrollViewの高さの7分の1にする為のもので、これによって子Widget達が持っている[高さに関するProperty(heightやsize_hint_y)]を無視して無理やり高さを揃えることができます。
　B行はGridLayoutの高さを常に[全ての子Widgetが入る最低限の大きさ]になるよう伸縮させる為のもので、適切に縦スクロールさせるのに必要です。又Kivy1.9.2からはBoxLayoutにもこのminimum_heightが追加されるようなので、GridLayoutの代わりに使うことでもっと簡潔なコードを書けるかもしれません。

# アニメーションする土台を作る

　次に上で作ったMenuを乗せる土台Widgetを作っていくのですが、ここでは最終的にModalView（俗に言うモーダルウィンドウ)として呼び出せるMenuを作ろうとしているので親クラスをModalViewにしています。

![modalview.gif](https://qiita-image-store.s3.amazonaws.com/0/144673/29dad2ad-b6b9-5548-fd9b-e3de152c20cd.gif)

```python3:
# -*- coding: utf-8 -*-

from kivy.base import runTouchApp
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.animation import Animation


class SlideinModalView(Factory.ModalView):

    def open(self, *args, **kwargs):
        size_hint_x = self.size_hint_x
        self.pos_hint = {'x': -size_hint_x}
        animation = Animation(
            pos_hint={'x': (1 - size_hint_x) / 2},
            duration=0.3,
            transition='in_cubic'
        )
        super(SlideinModalView, self).open(*args, **kwargs)
        animation.start(self)

    def dismiss(self, *args, **kwargs):
        size_hint_x = self.size_hint_x
        animation = Animation(
            pos_hint={'x': -size_hint_x},
            duration=0.3,
            transition='in_out_bounce'
        )
        super(SlideinModalView, self).dismiss(*args, **kwargs)
        animation.start(self)


def _test():
    import textwrap
    modalview = SlideinModalView(size_hint=(0.8, 0.8,))
    root = Builder.load_string(textwrap.dedent('''
        FloatLayout:
            Button:
                id: id_button
                size_hint: None, None
                size: 300,100
                text: 'open SlideinModalView'
    '''))
    root.ids.id_button.bind(on_press=modalview.open)
    runTouchApp(root)


if __name__ == '__main__':
    _test()
```

やってる事はModalViewを開くMethod(open)と閉じるMethod(dismiss)を上書きしてアニメーションの処理を追加しているだけです。
中で使っているAnimationのKeyword引数ですが

- durationはアニメーション全体の長さ(秒)
- stepはアニメーションの細かさ(何秒毎に値を書き換えるか)
- transitionはどのように値を変化させるか。假に一定の速度で変化させたければ'linear'にすればいい。使える値の一覧は[こちら](https://kivy.org/docs/api-kivy.animation.html#kivy.animation.AnimationTransition)。
- そして残りのKeyword引数がアニメーションさせたいPropertyとその最終値の指定になっています。(上記の場合はpos_hint)

そしてアニメーションの対象とするWidgetは.start()の引数で指定します。アニメーション対象のWidgetの指定とアニメーション対象のPropertyの指定が別々なのが少し分かりづらいですね。

# 上のSlideinModalViewにMenuを乗っける

![merge_menu_and_modalview.gif](https://qiita-image-store.s3.amazonaws.com/0/144673/f607d5d2-2bde-13b0-8650-214ea7db6f25.gif)

```python3
# -*- coding: utf-8 -*-

from kivy.base import runTouchApp
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.animation import Animation

Builder.load_string(r'''
同じなので略
''')


class SlideinModalView(Factory.ModalView):
    同じなので略


class SlideinMenu(SlideinModalView):

    def __init__(self, **kwargs):
        super(SlideinMenu, self).__init__(**kwargs)
        self.menu = menu = Factory.Menu()
        self.layout = menu.layout
        self.add_widget(menu)


def _test():
    import textwrap
    slideinmenu = SlideinMenu(size_hint=(0.8, 0.8,))
    layout = slideinmenu.layout
    for text in 'Python Ruby C++ Smalltalk Javascript C Swift'.split():
        layout.add_widget(Factory.MenuItem(text=text))
    root = Builder.load_string(textwrap.dedent('''
        FloatLayout:
           Button:
               id: id_button
               size_hint: None, None
               size: 300,100
               text: 'open menu'
    '''))
    root.ids.id_button.bind(on_press=slideinmenu.open)
    runTouchApp(root)


if __name__ == '__main__':
    _test()
```

### 見た目を改良

ModalViewが元々もってる背景色がなんともダサいので自前で描画することにします。

```:KvLanguageのコードにこれを追記
<SlideinMenu>:
    canvas:
        Clear:  # ModalViewが元々持っていた描画命令を削除
        Color:
            rgba: 0, 0, 0, 0.7
        Rectangle:
            pos: 0, 0
            size: self.size if self.parent is None else self.parent.size
```

![final.gif](https://qiita-image-store.s3.amazonaws.com/0/144673/8ac4c260-882f-42a1-5b38-9729f9b9121a.gif)

###[ソースコード全体](https://gist.github.com/gottadiveintopython/a40198678a2ada7882edfb410bdb33e8)

# 環境

- Python 3.4.3
- Kivy 1.9.1