【Flutter】Heroアニメーションを使って画面遷移してみた

## まえがき
画像をタップして画面遷移するときに、そのタップしたWidgetから広がるように画面遷移するようにしたいな、と思ってました。

いろいろWidgetを調べていく中で`Hero`というWidgetがありました。

## `Hero`とは
`Hero`は画面遷移するときのアニメーションを指定できるWidgetです。

`Hero()`では`child`と`tag`を指定できます。

画面遷移間の元のWidgetと、遷移先のクラスの中のWidgetの両方を`Hero`で囲み、同じ値の`tag`を指定するとその位置関係を把握して、元のWidgetを起点に画面が中央に広がるようアニメーションで遷移します。

## 実際のアニメーションとコード

![Hero.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311390/5ae05b06-1a83-4b60-dae8-da62f9531c88.gif)

実際にはこんな感じのアニメーション

元の画像のコード

```dart
Widget _photoItem(String image, int index) {
  return GestureDetector(
    child: Hero(
      tag: index,
      child: Container(
        decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
        child: Image.asset(
          image,
          fit: BoxFit.cover,
        ),
      ),
    ),
    onTap: () {
      Navigator.push(
        context,
        MaterialPageRoute(
          builder: (context) => FullScreenImage(image: image, index: index),
        ),
      );
    },
  );
}
```

遷移先のコード

```dart
@override
Widget build(BuildContext context) {
  return Hero(
    tag: index,
    child: Scaffold(
      appBar: AppBar(),
      body: Container(
        color: Colors.white,
        child: Image.asset(image),
      ),
    ),
  );
}
```


今回は`GridView`で並べた画像をタップすると、その画像のパスとindexが遷移先のクラスに渡されます。
`Hero`の`tag`にindexを入れておきます。

また`Hero`の入れ子に`Scaffold`を入れています。もし`Scaffold`の`body`に`Hero`を入れると、`appBar`が建てられるのと`Hero`のアニメーションが同時になりちょっと違和感がありました。

これがそのアニメーション

![Hero_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311390/35752ded-e235-f64c-de81-b05cc1436a50.gif)

## 他のWidgetを組み合わせる

`carousel_slider`を組み合わせると結構いい感じです。
[carousel_sliderの使い方](https://qiita.com/ryota47/items/fb8e32f1a409026ba065)は以前こちらで説明しました。

![carousel_slider_Hero.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311390/31babc01-ead7-83e6-f237-a15df3ae8d5a.gif)

(`Scaffold`のなかに`Hero`を入れてます。)

## ソースコード

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

class SampleHero extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _SampleHero();
}

class _SampleHero extends State<SampleHero> {
  final List images = [
    "images/sample1.png",
    "images/sample2.png",
    "images/sample3.png",
    "images/sample4.png",
    "images/sample5.png",
    "images/sample6.png"
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Hero'),
      ),
      body: GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
        ),
        itemCount: images.length,
        itemBuilder: (BuildContext context, int index) {
          return _photoItem(images[index], index);
        },
      ),
    );
  }

  Widget _photoItem(String image, int index) {
    return GestureDetector(
      child: Hero(
        tag: index,
        child: Container(
          decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
          child: Image.asset(
            image,
            fit: BoxFit.cover,
          ),
        ),
      ),
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => FullScreenImage(image: image, index: index),
          ),
        );
      },
    );
  }
}

class FullScreenImage extends StatefulWidget {
  final String image;
  final int index;

  FullScreenImage({this.image, this.index});

  @override
  State<StatefulWidget> createState() => _FullScreenImage(image, index);
}

class _FullScreenImage extends State<FullScreenImage> {
  String image;
  int index;

  _FullScreenImage(this.image, this.index);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Hero(
        tag: index,
        child: Container(
          color: Colors.white,
          child: Image.asset(image),
        ),
      ),
    );
  }
}
```

## カルーセルスライダーを使った方のソースコード

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

class ImageList extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _ImageList();
}

class _ImageList extends State<ImageList> {
  final List images = [
    "images/sample1.png",
    "images/sample2.png",
    "images/sample3.png",
    "images/sample4.png",
    "images/sample5.png",
    "images/sample6.png"
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('画像を表示'),
      ),
      body: GridView.builder(
        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
        ),
        itemCount: images.length,
        itemBuilder: (BuildContext context, int index) {
          return _photoItem(images[index], index);
        },
      ),
    );
  }

  Widget _photoItem(String image, int index) {
    return GestureDetector(
      child: Hero(
        tag: index,
        child: Container(
          decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
          child: Image.asset(
            image,
            fit: BoxFit.cover,
          ),
        ),
      ),
      onTap: () {
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) =>
                SampleCarouselSlider(images: images, index: index),
          ),
        );
      },
    );
  }
}

class SampleCarouselSlider extends StatefulWidget {
  final List images;
  final int index;

  SampleCarouselSlider({this.images, this.index});

  @override
  State<StatefulWidget> createState() => _SampleCarouselSlider(images, index);
}

class _SampleCarouselSlider extends State<SampleCarouselSlider> {
  List images;
  int index;

  _SampleCarouselSlider(this.images, this.index);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('carousel_slider_Hero'),
      ),
      body: CarouselSlider.builder(
        options: CarouselOptions(
          height: 600.0,
          initialPage: index,
          viewportFraction: 1,
          enableInfiniteScroll: false,
        ),
        itemCount: images.length,
        itemBuilder: (BuildContext context, int index) {
          return Hero(
            tag: index,
            child: Image.asset(
              images[index],
            ),
          );
        },
      ),
    );
  }
}
```