複数画像を表示する時によく見かけるやつをFlutterで作ってみた

前回作成したMulti image pickerに引き続き今回はそれを表示するための手法について書いていこうと思います。
以下のようなやつです..!
  
 ![ezgif-4-84d42413cee9.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535391/d2b3fef4-9396-b858-a4cc-64158292e07c.gif)


では早速。。。

##1 画像の表示を行う

今回使用するのは画像三枚なので、適当にAssetsフォルダ配下に画像を配置します。


初めはアニメーションなしで一通り書きます。


```dart
class _MyHomePageState extends State<MyHomePage>{
  
  List<String> images = ['images/image0.jpg', 'images/image1.jpg', 'images/image2.jpg'];
  String _path = 'images/image0.jpg';


  class _MyHomePageState extends State<MyHomePage>{

  List<String> images = ['images/image0.jpg', 'images/image1.jpg', 'images/image2.jpg'];
  String _path = 'images/image0.jpg';

  void _changeState(String path){
    setState(() {
      _path = path;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Image Sample"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Center(  //*
              child: Container(
                margin: EdgeInsets.symmetric(vertical: 10),
                height: 300,
                width: 300,
                child: Image.asset(
                  _path,
                  fit: BoxFit.cover,
                ),
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                smallImage(images[0]),
                smallImage(images[1]),
                smallImage(images[2]),
              ],
            ),
          ],
        ),
      ),
    );
  }
  Widget smallImage(String path) {
    return Container(
      decoration: BoxDecoration(
        border: Border.all(
          color: _path == path ? Colors.redAccent : Colors.white,
        )
      ),
      child: GestureDetector(
        child: Image.asset(
          path,
          width: 100,
          height: 100,
          fit: BoxFit.cover,
        ),
        onTap: () {
            if(_path!=path)_changeState(path);
        }
      ),
    );
  }
}

```

画面上部に表示される画像のパスを変数_pathに格納しています。
_changeState(path)を呼び出すことで_pathの値を更新してくれます。
また下の部分のborderの色は選択されている時に周りが赤くなるようにしています。

初めはこれで完成でいいかなと思ったのですが、少し物足りない気がしたので
少しだけAnimationを追加していきます。


##2. Animationの追加

まず初めにAnimationcontroller,Animationを使うための準備です。




```dart
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() { //1
    super.initState();
    controller =
        AnimationController(duration: const Duration(milliseconds: 250), vsync: this);//2
    animation = Tween<double>(begin: 0, end: 1).animate(controller)//3
    ..addStatusListener((status) {
      if(status == AnimationStatus.dismissed){//4
        controller.forward();
      }
    });
    controller.forward();
  }

  void dispose() {
    controller.dispose();//5
    super.dispose();
  }
  

```

1. controllerとかの準備はinitState()の内部で行なっています。

2.  今回はControllerのDurationを250msにしています。

3. またanimationはopacityを0->1, 1->0にするため、Tweenを用いて値を設定しています。

4. さらにaddStatusListenerを用いて、statusがdismissedになった時(今回の例だとopacityが0になった時)にcontroller.forwardでopacityが1になるようにしています。
(画像を切り替えた時に自動的に次の画像が表示されるようにしています。)

5. 最後にはしっかりdisposeを呼んでcontrollerをdispose()しています。


今回Animationを実装するためにAnimatedBuilderを用いました。
ほとんどが先ほどと同じコードなのでAnimatedBuilderの中身だけ書きます。

```dart

            AnimatedBuilder(
              animation: animation,
              child: Center(
                child: Container(
                  margin: EdgeInsets.symmetric(vertical: 10),
                  height: 300,
                  width: 300,
                  child: Image.asset(
                    _path,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              builder: (BuildContext context, Widget child) {
                return Opacity(
                  opacity: animation.value,
                  child: child,
                );
              }
            ),
```

builder内部のopacityはanimation.valueを渡してあげることで,
initStateで宣言した0~1のなかで変化をしてくれます。
AnimatedBuilderはAnimatedWidgetと異なり、Animationが起こるたびにbuilderの内部だけを
rebuildするのでbuilder内部のchildには可能な限りconst使ったりする方が効率はいいと思います...


最後に小さい画像を押した時の挙動を書き換えます。

```dart
onTap: () {
   if(_path != path){
     Future.delayed(const Duration(milliseconds: 250), () {
       _changeState(path);
     });
   controller.reverse();
  }
}
```

controller.forward もしくは controller.reverseはinitStateで宣言した通り、
250msで0->1または1->0へ変化します。
そこで、Future.delayedを用いることで、250ms経ったタイミング(画像がちょうどopacity:0によって表示されていない時)に_changeStateによって画像を切り替える処理を走らせています。

以上で初めのgitのようなものが出来上がります。

最後に完成形のコードを貼っておきます。


```dart

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class AnimatedImage extends AnimatedWidget {

  final List<String> images = ['images/image0.jpg', 'images/image1.jpg', 'images/image2.jpg'];
  static final _opacityTween = Tween<double>(begin: 0.0, end: 1);
  AnimatedImage({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: Container(
          margin: EdgeInsets.symmetric(vertical: 10),
          height: 300,
          width: 300,
          child: Image.asset(
            images[0],
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin{
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(milliseconds: 250), vsync: this);
    animation = Tween<double>(begin: 0, end: 1).animate(controller)
    ..addStatusListener((status) {
      if(status == AnimationStatus.dismissed){
        controller.forward();
      }
    });
    controller.forward();
  }

  void dispose() {
    controller.dispose();
    super.dispose();
  }

  List<String> images = ['images/image0.jpg', 'images/image1.jpg', 'images/image2.jpg'];
  String _path = 'images/image0.jpg';

  void _changeState(String path){
    setState(() {
      _path = path;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedBuilder(
              animation: animation,
              child: Center(
                child: Container(
                  margin: EdgeInsets.symmetric(vertical: 10),
                  height: 300,
                  width: 300,
                  child: Image.asset(
                    _path,
                    fit: BoxFit.cover,
                  ),
                ),
              ),
              builder: (BuildContext context, Widget child) {
                return Opacity(
                  opacity: animation.value,
                  child: child,
                );
              }
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                smallImage(images[0]),
                smallImage(images[1]),
                smallImage(images[2]),
              ],
            ),
          ],
        ),

      ),
    );
  }
  
  Widget smallImage(String path) {
    return Container(
      decoration: BoxDecoration(
        border: Border.all(
          color: _path == path ? Colors.redAccent : Colors.white,
        )
      ),
      child: GestureDetector(
        child: Image.asset(
          path,
          width: 100,
          height: 100,
          fit: BoxFit.cover,
        ),
        onTap: () {
          if(_path != path){
            Future.delayed(const Duration(milliseconds: 250), () {
              _changeState(path);
            });
            controller.reverse();
          }
        }
      ),
    );
  }
}
```

##参考
[Animations Tutorial](https://flutter.dev/docs/development/ui/animations/tutorial#refactoring-with-animatedbuilder)