[Flutter] animations の OpenContainer で少しハマったところ

# この記事は
FlutterのPackageである[animations](https://github.com/flutter/packages/tree/master/packages/animations)を使った時に少しハマったことがあったので書きました

# 経緯
比較的リッチなアニメーションを簡単に実装できるanimationsを試してみたく、[example](https://github.com/flutter/packages/blob/master/packages/animations/example/lib/container_transition.dart)を参考に以下のようなコードを書きました。

```main_screen.dart
class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFFF7F7F8),
      appBar: AppBar(
        title: const Text('猫ちゃん'),
      ),
      body: ListView(
        children: <Widget>[
          _OpenContainerWrapper(
            openPage: const CatPage(),
            closedBuilder: (BuildContext _, VoidCallback openContainer) {
              return ReusableCard(
                imagePath: 'assets/images/cat.jpg',
                contentTitle: '猫ちゃん',
                onPress: openContainer,
              );
            },
          ),
        ],
      ),
    );
  }
}

class _OpenContainerWrapper extends StatelessWidget {
  const _OpenContainerWrapper({
    this.closedBuilder,
    this.openPage,
  });

  final OpenContainerBuilder closedBuilder;
  final Widget openPage;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
      child: OpenContainer(
        transitionType: ContainerTransitionType.fade,
        openBuilder: (BuildContext context, VoidCallback _) {
          return openPage;
        },
        tappable: false,
        closedBuilder: closedBuilder,
      ),
    );
  }
}
```

Itemをタップすることで、アニメーションしながら`CatPage()`へ遷移する想定です。
ここで私は「`ListView`の子には角丸のCardを使いたいから、`ReusableCard`は`Card()`を使うべきだろう」と安直に考えて、以下のように書きました。

```reusable_card.dart
class ReusableCard extends StatelessWidget {
  const ReusableCard({
    @required this.imagePath,
    @required this.contentTitle,
    @required this.onPress,
  });
  final String imagePath;
  final String contentTitle;
  final VoidCallback onPress;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: InkWell(
        onTap: onPress,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Image.asset(
              imagePath,
              fit: BoxFit.fill,
            ),
            Padding(
              padding: const EdgeInsets.fromLTRB(16, 24, 0, 24),
              child: Text(
                contentTitle,
                style: Theme.of(context).textTheme.bodyText2.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

そしてbuildしてみると、、、

![Simulator Screen Shot - iPhone SE (2nd generation) - 2020-06-24 at 10.54.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292799/26e7307e-17b3-a8bc-1ab4-2c65cb7fdf21.png)

なぜCardが二重に。。。

# 原因
原因は`OpenContainer`の`closedShape`でした。

```open_container.dart
  const OpenContainer({
    Key key,
    this.closedColor = Colors.white,
    this.openColor = Colors.white,
    this.closedElevation = 1.0,
    this.openElevation = 4.0,
    // ↓これ
    this.closedShape = const RoundedRectangleBorder(
      borderRadius: BorderRadius.all(Radius.circular(4.0)),
    ),
    this.openShape = const RoundedRectangleBorder(),
    @required this.closedBuilder,
    @required this.openBuilder,
    this.tappable = true,
    this.transitionDuration = const Duration(milliseconds: 300),
    this.transitionType = ContainerTransitionType.fade,
  })  : assert(closedColor != null),
        assert(openColor != null),
        assert(closedElevation != null),
        assert(openElevation != null),
        assert(closedShape != null),
        assert(openShape != null),
        assert(closedBuilder != null),
        assert(openBuilder != null),
        assert(tappable != null),
        assert(transitionType != null),
        super(key: key);
```

closedShapeとは、タップアニメーション前の形(この場合ListのItemの形)を指定するパラメータです。
closedShapeにはデフォルトで角丸4dpの`RoundedRectangleBorder`が指定されており、その子にCardを入れていたがために、角丸のものが二重で配置されてしまったという訳でした。

今回の場合は、ReusableCardのCard部分を取り除いてあげれば解決です。

```reusable_card.dart
class ReusableCard extends StatelessWidget {
  const ReusableCard({
    @required this.imagePath,
    @required this.contentTitle,
    @required this.onPress,
  });
  final String imagePath;
  final String contentTitle;
  final VoidCallback onPress;

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: onPress,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Image.asset(
            imagePath,
            fit: BoxFit.fill,
          ),
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 24, 0, 24),
            child: Text(
              contentTitle,
              style: Theme.of(context).textTheme.bodyText2.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
            ),
          ),
        ],
      ),
    );
  }
}
```

![Simulator Screen Shot - iPhone SE (2nd generation) - 2020-06-24 at 10.54.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292799/370c255e-1db3-122e-532b-c08a00d7af27.png)

別のshapeにする際は、closedShapeへ明示的に指定してあげましょう。

# 終わりに
今思えば何故こんな単純な間違いに気づかなかったのかと恥ずかしい気持ちですが、同じようにつまづいた方がいるかもしれないので、その時の助けになれば幸いです。