Flutterのリップルエフェクトは一癖ある

下記環境でFlutterアプリを開発中です。

|ツールなど|バージョンなど|
|:--|:--|
| MacBook Air Early2015|macOS Mojave 10.14.5|
|Android Studio |3.6.1|
|Java |1.8.0_131|
|Flutter|1.12.13+hotfix.5|
|Dart |2.7.0|

少し凝ったレイアウトを作っていてリップルエフェクトに苦労したのでちょっとまとめます。

# 1.リップルエフェクトとは

マテリアルデザインでアクション可能なウィジェットをタップしたときに、波紋のように広がるアニメーションのことです。

![ezgif.com-video-to-gif_rippleeffect.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/0be06a4e-3a4a-b00b-4433-0c0ec8d37785.gif)


# 2.基本的な設定の仕方

## (1)デフォルトで使えるウィジェットを使う
**RaisedButton**などの`MaterialButton`の派生クラスや、**IconButton**などに
標準装備されています。

↑のサンプルは、`RaisedButton`と`IconButton`です。

<details><summary>サンプルのコードはこちら</summary><div>

```dart
class MyHomePage extends StatelessWidget {
  MyHomePage({Key key, this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: <Widget>[
            RaisedButton(
              padding: EdgeInsets.all(20),
              child: Text(
                'RaisedButton',
                style: TextStyle(fontSize: 28),
              ),
              onPressed: () => debugPrint('RaisedButton pushed.'),
            ),
            IconButton(
              iconSize: 42,
              icon: Icon(
                Icons.thumb_up,
              ),
              onPressed: () => debugPrint("IconButton tapped"),
            ),
          ],
        ),
      ),
      // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

```
</div></details>

`onPressed()`がnullだとdisabledになるので何か設定して下さいね。

## (2)InkWellを使う
**InkWell**というウィジェットの子にすると、リップルエフェクトを掛けることが出来ます。

![ezgif.com-video-to-gif_inkwell.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/15618d37-5b18-5882-bac2-22ad4b8b0c72.gif)

```dart
            InkWell(
              onTap: () => debugPrint("InkWell tapped"),
              child: Padding(
                padding: EdgeInsets.all(20.0),
                child: Text(
                  'InkWell',
                  style: TextStyle(fontSize: 28),
                ),
              ),
            ),
```

`onTap()`がnullだと反応しないので何か設定して下さい。

# 3.ちょっとコツが必要なパターン

## (1)Card
Flutterには便利なCardウィジェットというのがあります。マテリアルデザインらしく、浮いた状態に出来るやつですね。

![flutter_card.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/5d688c33-4e93-6c2f-58a9-5edf4d866450.png)

Cardウィジェットは、「子」にInkWellを持たせないと、リップルエフェクトが表示されません。

**NGなパターン**

```dart
            InkWell(
              onTap: () => debugPrint("Card tapped"),
              child: Card(
                child: Padding(
                  padding: EdgeInsets.all(20.0),
                  child: Text(
                    'Card',
                    style: TextStyle(fontSize: 28),
                  ),
                ),
              ),
            ),
```

![ezgif.com-video-to-gif_inkwell_card.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/bb63481d-dcd0-752b-e155-f3b68cb3597d.gif)

なにやらカードの後ろにいるのが分かりますね。

**OKなパターン**

```dart
            Card(
              child: InkWell(
                onTap: () => debugPrint("Card tapped"),
                child: Padding(
                  padding: EdgeInsets.all(20.0),
                  child: Text(
                    'Card',
                    style: TextStyle(fontSize: 28),
                  ),
                ),
              ),
            ),
```

![ezgif.com-video-to-gif_card_inkwell.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/25239b60-3503-1f22-e52c-663ef4d91236.gif)


## (2)子に色が付いているとリップルエフェクトが表示されない

多分(1)はこれと関連しているのですが、最初に出したInkWellのみでCardを使っていないパターンで、`Container`なんかで括って`color:`などで色を付けると、リップルエフェクトが出なくなります。

以下のコードどちらも、ダメです。

**NGパターン1**

InkWell-Container-*child widget* の構成

```dart
            InkWell(
                onTap: () => debugPrint("InkWell tapped"),
                child: Container(
                  color: Colors.pink[100],
                  child: Padding(
                    padding: EdgeInsets.all(20.0),
                    child: Text(
                      'InkWell',
                      style: TextStyle(fontSize: 28),
                    ),
                  ),
                )),
```

**NGパターン2**

Container-InkWell-*child* の構成

```dart
            Container(
                color: Colors.pink[100],
                child: InkWell(
                  onTap: () => debugPrint("InkWell tapped"),
                  child: Padding(
                    padding: EdgeInsets.all(20.0),
                    child: Text(
                      'InkWell',
                      style: TextStyle(fontSize: 28),
                    ),
                  ),
                )),
```

これは、実は`FlatButton`や`RaisedButton`, `MaterialButton`等の標準でリップルするウィジェットでも同様で、子に色を付けると出なくなります。

解決方法は、子ウィジェットが単独のウィジェットの場合は、`Mateiral`ウィジェットの子にしてやり、そちらに色を付けてやれば良いです。

```dart
            new Material(
                type: MaterialType.button,
                color: Colors.pink[100],
                child: InkWell(
                  highlightColor: Colors.pink.shade300,
                  splashColor: Colors.pink.shade100,
                  onTap: () => debugPrint("InkWell tapped"),
                  child: Padding(
                    padding: EdgeInsets.all(20.0),
                    child: Text(
                      'InkWell',
                      style: TextStyle(fontSize: 28),
                    ),
                  ),
                )),
```

# 3.複雑なウィジェット構成の場合

こんなレイアウトを考えました。
([Prott](https://prottapp.com/)で作成しました)

![prott.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/0cc780b2-5c2f-c29c-5c44-2f1b88bcdc34.png)

細かいとこは説明しませんが、分類ごとにカードにして、各行はリストにしたいです。分類によって行数が違います。
星マークの部分は、画像をいれるところ。
`-`/`+`アイコンを押すと、その間にある数字がカウントダウン/アップします。

こんなレイアウトを作りたいのだけど・・・

`-`/`+`アイコンと、かつカード全体にリップルエフェクトを出したくて、非常に苦労しました。


私が最初に考えたカード内のウィジェット構成は、こうなります。

```
Card - Row
        +-- TextWidget(Aのラベル部分)
        +-- Expanded(残りのスペースをすべて使うため)
              +- ListView
```

ListViewの行レイアウトは、このように考えました。

```
Row 
 +-- Expanded
       +-- Text(分類タイトル/補足のテキスト)
 +-- Image
 +-- Text(固定数字)
 +-- Icons.remove_circle_outline
 +-- Text(カウントダウン/アップする数字)
 +-- Icons.add_circle_outline
```

Paddingなどは除外しています。

`TextWidget(Aのラベル部分)`が親の高さにまで引き延ばせれば、テキストの背景色でなんとかなりそうでもあるんですが(Textの背景色のみ、リップルエフェクトに影響しませんでした)、その方法が分からず、「行数に応じた高さでラベルの文字入り画像を作る」というなんともアナログな手法で対応しています。

![ezgif.com-video-to-gif_card_list_icon_ripple.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103886/3510a6f7-de1f-ba01-6b8c-e1ade42745b7.gif)

カウントダウン/アップはまだ実装してないので動いていません(汗)

<details><summary>実際のコードはこちら</summary><div>

```dart
import 'package:flutter/material.dart';

import '../data/RecordClassification.dart';
import 'input_styles.dart';

/// 区分別カード用ウィジェット
class TypeCardWidget extends StatelessWidget {
  TypeCardWidget(this.majorType);

  final MajorClassification majorType;

  @override
  Widget build(BuildContext context) {
    return Card(
      semanticContainer: true,
      clipBehavior: Clip.antiAliasWithSaveLayer,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(6.0),
      ),
      child: InkWell(
        onTap: () => debugPrint("Card tapped."),
        child: Row(
          children: <Widget>[
            // 種類ラベル
            Padding(
              padding: EdgeInsets.only(right: 6.0),
              child: Image.asset(majorType.imageAssetName, width: 20),
            ),
            // データ行ウィジェット
            Expanded(
              child: TypeBoard(majorType),
            ),
          ],
        ),
      ),
    );
  }
}

/// 大分類ラベルを除いた小分類行を載せるレイアウト部分のウィジェット
class TypeBoard extends StatelessWidget {
  TypeBoard(this.majorType);

  final MajorClassification majorType;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      shrinkWrap: true,
      physics: new NeverScrollableScrollPhysics(),
      itemBuilder: (BuildContext context, int index) {
        return index != 0
            ? Container(
                decoration: BoxDecoration(
                  border: Border(
                    top: BorderSide(
                      width: index == 0 ? 0.0 : 0.5,
                      color: Theme.of(context).dividerColor,
                    ),
                  ),
                ),
                child: TypeRow(majorType.minors[index]))
            : TypeRow(majorType.minors[index]);
      },
      itemCount: majorType.minors.length,
    );
  }
}

/// データ行ウィジェット
class TypeRow extends StatelessWidget {
  TypeRow(this.minorType);

  final MinorClassification minorType;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
        // 項目ラベル
        Expanded(
          child: Padding(
            padding: EdgeInsets.all(6.0),
            child: Text(
                "${minorType.name} ${minorType.notes ?? ''}\n${minorType.description}",
                style: minorLabelStyle),
          ),
        ),
        // 画像
        Padding(
          padding: EdgeInsets.all(6.0),
          child: Icon(
            Icons.star,
            size: 36.0,
          ),
        ),
        // 基礎点
        Padding(
          padding: EdgeInsets.only(left: 12.0, right: 12.0),
          child: Text(minorType.basePoint.toStringAsFixed(1),
              style: basePointLabelStyle),
        ),
        // -ボタン
        Padding(
          padding: EdgeInsets.only(left: 8.0),
          child: IconButton(
            icon: Icon(Icons.remove_circle_outline),
            onPressed: () => debugPrint("icon tapped"),
          ),
        ),
        // 現在点数
        Text(
          "3",
          style: countLabelStyle,
        ),
        // +ボタン
        Padding(
          padding: EdgeInsets.only(),
          child: IconButton(
            icon: Icon(Icons.add_circle_outline),
            onPressed: () => debugPrint("icon tapped"),
          ),
        ),
      ],
    );
  }
}
```
</div></details>

当然、画像の高さの調整は何度も修正が必要です。HotReloadが無ければこんなコトしようとは思わなかったでしょう・・・

気持ちが悪いので、なんとか背景画像を使わずに済ませられる方法を探しています。是非コメントお寄せ下さい(懇願)


なお、画像は角丸では作っていません。四角形で、文字入りです。
角丸画像で無くても、Cardに以下の設定をしておくと、自動で角丸からはみ出た部分はクリップしてくれます。

```dart
      semanticContainer: true,
      clipBehavior: Clip.antiAliasWithSaveLayer,
```

# 参考

Flutter 背景色ありで Ripple Effect を効かせる
https://qiita.com/najeira/items/db6edb1c8d8f75042b98

InkWell and FlatButton do not ripple when inside a container with opaque background
https://github.com/flutter/flutter/issues/3782