kivyMDチュートリアル其の什陸 Components - Card篇

[追記 2021/05/09 午後あたり]

 \- MDCard単体でスワイプできないかあれこれ試していましたが、なんか出来たのでこちらに情報を追記します。
　コードの詳細の方はGitHub(mdcard_swipe.pyをxviフォルダにアップ済み)をご覧ください。

本当に試してんのか?という方のために実行結果を以下に添付しておきます。

![82.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/c2b4cecc-32a9-d1d1-770f-e1558ecd0e7a.png)

[追記 2021/05/09 夕方あたり]

 \- カード分だけ繰り返しウィジェットを列挙するだけでなく、イテレータを回す分だけ生成するということもできたので改めて
\# コードの方はGitHubにて(mdcard_swipe_autocreate.py)。

実行結果は以下になります。

![83.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/08ee7852-bfb0-29df-e737-e2f68744b900.png)

![84.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/9efcf3a4-886e-de04-2b28-64c53ba7cace.png)

---

みなさん、おはこんばんは。

GW君の勢いも大分後退してきましたが、いかがだったでしょうか。まだ、終わってないぞ！
という方もいらっしゃるかもしれませんね。私のGW君はとっくに置き手紙を残し、どこかに
消え去りました。おそらく、来年あたりに帰ってくるのですかね。

まぁそういうことは一旦置いておいて、GW企画も幸いにして第3弾を迎えることが出来ました！
ありがとうございます。誰に向かってお礼を言っているか分からないですが、とりあえず感謝を
述べておきます。

今日のお題は前回述べた通りで、Card篇となっています。これまでもいくつかのウィジェットを
扱ってきましたが、今日のテーマに関して言うと、とても協力な武器になり得るものです。GW企画
の3テーマで簡単なアプリが出来てしまうのではないでしょうか。RPGだと、中ボスくらいは倒せ
そうな勢いです。

まぁ、そんな嘘かホントか分からない話は置いておいてさっそく取り掛かることにします。

## Card

最初のマテリアルデザインはさることながら、概要が記載されています。

> Cards contain content and actions about a single subject.

あまり自信の精度の低い解釈を載せてしまうとダメなので、こちらも依頼をしてみます。

> カードには、一つのテーマに関するコンテンツやアクションが含まれています。

あ、今回から「Google翻訳」君を取りやめて「DeepL」君に依頼先を変更しました。
だって、翻訳精度がいいって言うから...

とまぁ、どうでもよいことは置いておいて、こちらに関しては馴染みが深みかもしれません。
Googleの検索結果の横方向にコンテンツが並べられたり、Google Mapの店の表示に使われたりで
枚挙にいとまがありません。こんな以外なところにも使われているよ！というのがありましたら
教えて下さいね。

で、KivyMDからは次のようなクラスらを提供するよということも書かれています。

* MDCard
* MDCardSwipe

で、さらにInfoとして次のようなことも触れられています。

> MDCard inherited from BoxLayout. You can use all parameters and attributes 
> of the BoxLayout class in the MDCard class.

> MDCardはBoxLayoutから継承しています。MDCardクラスでは、BoxLayoutクラスのすべての
> パラメータと属性を使用することができます。

結構大胆な翻訳をするなぁと思っていることは置いておいて、BoxLayoutクラスのように使える
らしいです。このあたりはこのあとのコードの方を見てみましょうか。

今日については少しコードの列挙が多いので、いつもと進行を変えコード → 結果というように
記載していきます。ではレッツゴ。

### MDCard

```python:xvi/card.py
from kivy.lang import Builder

from kivymd.app import MDApp

KV ='''
MDScreen:

    MDCard:
        size_hint: None, None
        size: "280dp", "180dp"
        pos_hint: {"center_x": .5, "center_y": .5}
'''


class TestCard(MDApp):
    def build(self):
        return Builder.load_string(KV)


TestCard().run()
```

うーん、実にシンプル。確かに、BoxLayoutでも使用できるsize_hintやpos_hintなどが
使われています。特に目新しいものはないので、一旦結果の方を見てみます。

#### 結果

![69.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/f3cc02dd-25f9-ce38-4116-56733be28968.png)

存在感がありますね。では、さくさく次に進んでみましょう。

### MDCard(add content)

次はカードにコンテンツを注入したものになります。依存性注入(DI)ではありません。

```python:xvi/card_content.py
from kivy.lang import Builder

from kivymd.app import MDApp

KV = '''
MDScreen:

    MDCard:
        orientation: "vertical"
        padding: "8dp"
        size_hint: None, None
        size: "70dp", "45dp"
        pos_hint: {"center_x": .5, "center_y": .5}

        MDLabel:
            text: "Title"
            theme_text_color: "Secondary"
            size_hint_y: None
            height: self.texture_size[1]

        MDSeparator:
            height: "1dp"

        MDLabel:
            text: "Body"
'''


class TestCard(MDApp):
    def build(self):
        return Builder.load_string(KV)


TestCard().run()
```

さっきよりか、BoxLayoutの特徴が顕著ですね。orientationとか出るともう出ちゃって
ますねーと言いたくなります。やってよかった、Layout篇。なんだか良く分からんと言う方は
GW企画の中のLayout篇を一読願います。というかKivyのBoxLayoutのマニュアル見た方が
早いかも。

[kivyMDチュートリアル其の什肆 Components - Layout篇](https://qiita.com/virty/items/d5fa0a529912e6b0beb0)

とくにここも目新しいものは少ないですが、sizeというプロパティがありますね。これは
見ての通りということと、マニュアルの記載がないので詳細は省略します。というか見ての
通りしかないですね。

あとは、いままでほったらかしにしてきたMDSeparatorに触れ込みたいと思います。仕様
としては以下になります。なぜかCardに紛れ込んで、記載されています。謎しかない。。

##### class kivymd.uix.card.MDSeparator(**kwargs)

`A separator line.`

> color

>　Separator color in rgba format.
>　color is a ColorProperty and defaults to None.

> on_orientation(self, *args)

ただ線を引くだけでなく、色も選べるのですね。あとは、on_orientationというメソッド?
というものもありますね。なんにせよ、思わぬ武器が手に入りました。

#### 結果

さて、結果の方を見てみましょう。
ただカードを表示するだけでは面白みがないので、サイズを1/4ほど縮めます。
\# いやそれでも面白くねーよというツッコミは受け付けておりません

![70.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/6563dfa1-fdc9-8db7-2cdd-c01545981078.png)

小っさっ！と言いたくなるほどですね。これはダメです。ダメよ〜ダメダメ。

懐かしいギャグをしたところで、ダメということを強調しておきます。というのもMaterial-
-Designの仕様としてもこんな仕様は認めていません。NGパターンにもこんな例はあるはずも
なく、コントリビューターが見ていたら開いた口が塞がらない状態に陥りそうです。どうか
このページを見ていませんように。

意味はないかもしれませんが、念のため正規パターンも載せておきます。これはマニュアル
通りのコードです。

![71.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/c210c639-9c7a-6212-d72f-09ec1a2d007d.png)

んー、ばっちし！

### MDCardSwipe

こちらは、タイトルにある通りCardをスワイプしたり削除できたりするものになります。
使用方法と仕様が簡潔にマニュアルに記載されていますね。

> To create a card with swipe-to-delete behavior, 
> you must create a new class that 
> inherits from the MDCardSwipe class:

どうやら、このカードを使う場合はクラス宣言とkv側の両方を書かないとダメらしいです。
具体的な使用方法はマニュアル通りですが、以下みたいです。

```python
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height

    MDCardSwipeLayerBox:

    MDCardSwipeFrontBox:

        OneLineListItem:
            id: content
            text: root.text
            _no_ripple_effect: True
```

```python
class SwipeToDeleteItem(MDCardSwipe):
    text = StringProperty()
```

クラス宣言から言うと、textプロパティはクラス側でもkv側でも用意した方が良い
ということですね。理由としては後述のコードを見てもらった方がいいかもです。

kv側というと、なんかこのパターン見たことあるぞ！という方はKivyMDマスターです。(何様だ)
そうです、そうなのです。Componentsのいきなり最初で洗礼を浴びせられたBackdrop篇です！
そんなの知らねーよという方はマニュアルを見直すか以下リンクを参照ください。
[kivyMDチュートリアル其の漆 Components - Backdrop篇](https://qiita.com/virty/items/e29be66fffc19e16621d)

そのときと同様に、フロント・バックレイヤーを2つ用意してフロントの方にコンテンツを
仕込みます。バックレイヤーの方は、この後でも出てきますが削除ボタンなどを配置する
こともあるようですね。とりあえず、良く分からんという方はこの形を覚えてもらえれば
と思います。

で、あれこれ言ってもどうしようもないのでさっそくコードに入っていきましょう。
なんの変哲もないですが、マニュアルそのままのコードです。

```python:xvi/card_swipe.py
from kivy.lang import Builder
from kivy.properties import StringProperty

from kivymd.app import MDApp
from kivymd.uix.card import MDCardSwipe

KV = '''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height

    MDCardSwipeLayerBox:
        # Content under the card.

    MDCardSwipeFrontBox:

        # Content of card.
        OneLineListItem:
            id: content
            text: root.text
            _no_ripple_effect: True


MDScreen:

    MDBoxLayout:
        orientation: "vertical"
        spacing: "10dp"

        MDToolbar:
            elevation: 10
            title: "MDCardSwipe"

        ScrollView:
            scroll_timeout : 100

            MDList:
                id: md_list
                padding: 0
'''


class SwipeToDeleteItem(MDCardSwipe):
    '''Card with `swipe-to-delete` behavior.'''

    text = StringProperty()


class TestCard(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.screen = Builder.load_string(KV)

    def build(self):
        return self.screen

    def on_start(self):
        '''Creates a list of cards.'''

        for i in range(20):
            self.screen.ids.md_list.add_widget(
                SwipeToDeleteItem(text=f"One-line item {i}")
            )


TestCard().run()
```

##### import

特に触れなくても良いかと思っていましたが、新たな発見というか当たり前の話ですが
上記のSwipeTo~クラス内で、以下のようにクラスを使うときというか以下は継承を使用
するときですが、import文を追加する必要があります。すみません、言わずもがなという
ところかもです。

```python
(略)
from kivymd.uix.card import MDCardSwipe

(略)

class SwipeToDeleteItem(MDCardSwipe):
    '''Card with `swipe-to-delete` behavior.'''

(略)
```

##### kv側

本来ここも目新しいものは少なく、SwipeToDeleteItemのレイアウトも先述通りなので
取り上げる必要はないのですが、ポツンと見慣れないScrollViewが姿を現しています。

```python
MDScreen:

    MDBoxLayout:

(略)
        
        ScrollView:
            scroll_timeout : 100

(略)            
```

このscroll_timeoutってなんだろうなと思いマニュアルも見ていたのですが、以下リンク
の解説がめちゃ×2分かりやすかったので参照しておきます。
\# ありがとうございます、gotta_dive_into_pythonさん！

[Kivy小ネタ集(随時更新) - Scroll関係](https://qiita.com/gotta_dive_into_python/items/d18271b1aa5c5fb361ab)

念のため公式マニュアル(Kivy)の定義も触れておきます。

> Timeout allowed to trigger the scroll_distance, in milliseconds. 
> If the user has not moved scroll_distance within the timeout, 
> the scrolling will be disabled, and the touch event will go to the children.

> scroll_timeout is a NumericProperty and defaults to 55 (milliseconds) 
> according to the default value in user configuration.

> ScrollView
> https://kivy.org/doc/stable/api-kivy.uix.scrollview.html

##### クラス側

SwipeToDeleteItemも先述の通りなので、再掲はしません。TestCardについては注目
するべき箇所は以下になります。

```python
class TestCard(MDApp):

(略)

    def on_start(self):
        '''Creates a list of cards.'''

        for i in range(20):
            self.screen.ids.md_list.add_widget(
                SwipeToDeleteItem(text=f"One-line item {i}")
            )
```

まぁ、これも絶対見たことありそうですが再掲をしました。kv側で宣言をしたmd_listが
on_startメソッドで使用されています。今まで触ってこなかったですが、add_widgetに
ついてもマニュアルから仕様を再確認したいと思います。

> add_widget(self, widget, index=0, canvas=None)

> Add a new widget as a child of this widget.

> 　Parameters　widget: Widget
> 　　　　　　　　　Widget to add to our list of children.

※ 他にもindexとかcanvasとかもありますが元バージョンではないということなので
　省略しています

要はこれさえ記載されていれば何でも?ウィジェットを追加できるようですね。
コードの方でもカスタムクラスのSwipeToDeleteItemを生成してウィジェットを
20個分入れ込んでいます。

#### 結果

ということでこれも結果を見てみましょう。

![72.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/73dc94d5-d6ef-a329-cd2d-cf7cf93a0796.png)

うーん、問題なさそうです。

![73.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/c510fa8e-72dc-2cf2-3d4d-647716cffc9f.png)

思いきりスワイプをしても、現時点では削除が出来ません。

先述できていないところですが、以下のように記載を加えるとスワイプする方向を
変更できるようです。

```python
<SwipeToDeleteItem>:
    # By default, the parameter is "left"
    anchor: "right"
```

![74.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/29332aad-77bb-f675-6671-4e491b80310a.png)

### MDCardSwipe(Removing)

コンテンツを残しておく前提であれば問題ないと思いますが、消去したい場合はどう
しましょうか。まぁ、こんな遠い言い回ししなくても答えは出てますけどね。そう、
公式マニュアルならね。

すみません、調子乗りました。。大人しくマニュアルから使用方法など抜粋します。

> Removing an item using the type_swipe = "auto" parameter

> The map provides the MDCardSwipe.on_swipe_complete event. 
> You can use this event to remove items from a list:

```python
<SwipeToDeleteItem>:
    on_swipe_complete: app.on_swipe_complete(root)

def on_swipe_complete(self, instance):
    self.screen.ids.md_list.remove_widget(instance)
```
上記のように指定すれば良いとのことです。on_swipe_completeでのコールバックメソッドの
引数にrootウィジェットごと渡していますね。この辺はmd_listが持っているremove_widget
メソッドがごにょごにょやっているんだなと思うしかありません。

で、さっそくサンプルコードに入っていきたいですが、すべてコードを載せても重複している
ところばかりなので先程のcard_swipe.pyからの差分を載せたいと思います。どこを変更
すれば良いのか分かりやすくなるかと思われます。

```python:xvi/card_swipe_delete.py

(略)

KV = '''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height
+    type_swipe: "auto"
+    on_swipe_complete: app.on_swipe_complete(root)

(略)

    def build(self):
        return self.screen

+    def on_swipe_complete(self, instance):
+        self.screen.ids.md_list.remove_widget(instance)

    def on_start(self):
        for i in range(20):
            self.screen.ids.md_list.add_widget(
                SwipeToDeleteItem(text=f"One-line item {i}")
            )

(略)

```

まぁ、これも先述の使用方法通りですね。1つ変わっているのはtype_swipeプロパティが
追加されていることくらいですかね。これは任意プロパティ?と思われるかもですが、どちらか
というとあった方がいいかもしれません。これをなくすと(type_swipe: "hand"に戻る)どう
なるかというと、スワイプを途中で止めてカードを離すと逆方向に戻ってしまいます。ちょっと、
動作としては不自然に見えます。

[補足]
変更部分は他でもありますが、コメントの削除とかなので差分として出すのは本質ではない
と思い、省略しています。ご了承のほどを。

#### 結果

ではどうなるか見てみましょう。

![76.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/c6341439-55df-b620-c0df-c069d7b8743c.png)

初期画面は問題なさそうです。

![77.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/e25d1fdc-bbd4-0ab2-9300-0f52704ef8ce.png)

スワイプする様子がないのは申し訳ないですが、スワイプしても問題なく消去できます。

![75.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/d38e397e-a71f-c299-08d6-3a66dffb0caf.png)

上記画面としては忘備録に近い内容ですが、スワイプを途中で止めて下の方向にずらすと
カードが途中で止まってしまいます。これもスマホ・タブレットとかだとどうなるか、
興味がある点になりますね。

### MDCardSwipe(Add content to the bottom layer of the card)

ちょっと、疲れてきたw
うーん、後少しなのでがんばりマッスル。

という冗談を挟みながらですが、先述したバックレイヤーのほうにウィジェットを追加する方法です。
ここでは削除ボタンが題材ですが、SNSに共有など色々選択肢があるところです。

使用方法は以下になります。

> To add content to the bottom layer of the card, use the MDCardSwipeLayerBox class.

```python
<SwipeToDeleteItem>:

    MDCardSwipeLayerBox:
        padding: "8dp"

        MDIconButton:
            icon: "trash-can"
            pos_hint: {"center_y": .5}
            on_release: app.remove_item(root)
```

うーん、簡単ですね。バックレイヤーにボタンを追加するだけですね。ボタンもGW企画で
触っているしでバッチリですね！知らないという方は以下リンクを参照！

[kivyMDチュートリアル其の什伍 Components - Button篇](https://qiita.com/virty/items/29d792d5ecf4ad0f86be)

サンプルコードもcard_swipe_delete.pyから微小たる変更になります。

```python:card_swipe_delete_trashicon.py

(略)

KV = '''
<SwipeToDeleteItem>:
    size_hint_y: None
    height: content.height
-   type_swipe: "auto"
-   on_swipe_complete: app.on_swipe_complete(root)

    MDCardSwipeLayerBox:
        padding: "8dp"

+       MDIconButton:
+           icon: "trash-can"
+           pos_hint: {"center_y": .5}
+           on_release: app.remove_item(root)

(略)

class TestCard(MDApp):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.screen = Builder.load_string(KV)

-    def on_swipe_complete(self, instance):
+    def remove_item(self, instance):
        self.screen.ids.md_list.remove_widget(instance)

(略)
```

必要がないということからtype_swipeなどプロパティがなくなっています。あとは
ボタンの追加も先述通りですね。最後の差分としては、メソッドのリネームだけ。。
以上になります。

#### 結果

では、結果の方を見てみましょう。

![78.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/3c3e0eb9-c94e-1d5f-640b-b4b99b8f50af.png)

初期画面は問題ありません。

![79.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/a39f70c3-dffb-c085-eb61-5beeb1d72491.png)

スワイプしても途中で止まったりすることはありません。

![80.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/6c80e5b8-d1e4-8ad0-643f-caaf0c335cd2.png)

アイコンボタンを見事に消去されます。止まったりする心配事をなくすという意味では
こちらの方がいいかもしれませんね。止まることがなければ選択基準はフラットになり
ますが。

### Focus behavior

こちらについては体力の限界が見えてきたので(大げさ)、おまけということにしたいと
思います。コードの方も著作権がめんどくさいということでフリー画像に変更している
くらいで特にそれ以外は変更していません。詳細を確認したい方はGitHubの方にて。
→card_starbutton.py

#### 結果

実行結果は以下になります。
動かすためには、なにがしか画像ファイルを用意する必要があります。ないと動作しません。

![81.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82826/1a3f2591-6eb5-96d9-c4bf-cbd94863613b.png)

うーん、可愛い。猫はいつみても可愛いですね。
少し画面サイズが大きいのは、スマホサイズにすると途端に文字などが読みにくくなる
ためです。なのでスマホ基準に考えると、設計を注意しなければいけません。

あとは、Ripple behaviorが動作しなかったことも注意が必要です。
今後に期待ということでしょうか。それとも私が動作の仕方わかってないだけだったりして。

## 補足

最後のまとめに入る前に、全然マニュアル最下部の仕様に触れてなかったので、使用
したものについてはここで触れておきます。

###  class kivymd.uix.card.MDCard(**kwargs)

`focus_behavior`

> Using focus when hovering over a card.

> focus_behavior is a BooleanProperty and defaults to False.

`ripple_behavior`

> Use ripple effect for card.

> ripple_behavior is a BooleanProperty and defaults to False.

###  class kivymd.uix.card.MDCardSwipe(**kw)

`on_swipe_complete`
> Called when a swipe of card is completed.


`anchor`
> Anchoring screen edge for card. Available options are: ‘left’, ‘right’.
> anchor is a OptionProperty and defaults to left.


`type_swipe`

> Type of card opening when swipe. Shift the card to the edge or 
> to a set position max_opened_x. Available options are: ‘auto’, ‘hand’.

> type_swipe is a OptionProperty and defaults to auto.

## まとめ

んーーー、今日も長かったぁー！

というどうでもよいことは良くて、いかがだったでしょうか。
少し予定よりもページが長くなったので、読み応えがあろうかと思われます。

せっかく身についたのですから、これから積極的に使っていきたいですね。

ということで、今日はこれにて終わろうと思います！また次回にて。
次回はDialogの続きのDropdown Itemに入っていく予定です。

それでは、ごきげんよう。

## 参照

> Components » Card
> https://kivymd.readthedocs.io/en/latest/components/card/

> 猫ジャーナル
> https://nekojournal.net/?p=2436

> ScrollView
> https://kivy.org/doc/stable/api-kivy.uix.scrollview.html

> [Kivy小ネタ集(随時更新) - Scroll関係](https://qiita.com/gotta_dive_into_python/items/d18271b1aa5c5fb361ab)