BottomNavigationItemを任意のタイミングでぴょんぴょんさせる

![simple_bounce.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/335b4658-64b5-07d7-b39d-ea958b1f9f2d.gif)

例えばカートにアイテムを追加した時など、ある画面で行ったアクションが他画面に反映されることがあるかと思います。その際、他の画面に変化があったことを自然に知らせるために、BottomNavigationやToolbar上のアイコンにアニメーションを取らせることがあります

例えば、Amazonならカートに商品を追加した場合、画面右上のアプリバー上のアイコンがアニメーションでカウントアップすることで、カートにアイテムが追加されアプリバー上のアイコンから遷移できることを示唆しています。それと同じようにBottomNavigationItemを**以前に作ったWidgetを使って**ぴょんぴょんさせてみます

https://qiita.com/nashitake/items/f5c486d573d715249268

# 注意事項
このサンプルはriverpodを使ってかなり無理やりアニメーションをトリガーしています
他にいいやり方がありましたら教えてください

### riverpodのバージョン指定
```
flutter_riverpod: ^0.14.0+3
```

# シンプルに実装できる場合

```dart:main.dart
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => MyHomePageState();
}

class MyHomePageState extends State<MyHomePage> {
  final bounceIcon = BounceContainer(
      child: Icon(Icons.looks_two_rounded)
  );
  var screenIndex = 0;
  late List<Widget> screens;

  @override
  void initState() {
    super.initState();

    screens = [
      Center(
        child: ElevatedButton(
          onPressed: () {
            bounceIcon.bounce(); // Screen1のボタンをタップするとアクション開始
          },
          child: Text('jump!')
        )
      ),
      Center(child: Text('Screen2'))
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        toolbarHeight: 0,
      ),
      body: IndexedStack( // 画面を切り替えるたびに描画し直されるのを防ぐ
        index: screenIndex,
        children: screens,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.looks_one_rounded),
              label: 'screen 1'
          ),
          BottomNavigationBarItem(
              icon: bounceIcon, // 自作コンテナを指定する
              label: 'screen 2'
          ),
        ],
        currentIndex: screenIndex,
        onTap: (index) {
            setState(() {
              screenIndex = index;
            });
        },
      ),
    );
  }
}
```

![simple_bounce.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/335b4658-64b5-07d7-b39d-ea958b1f9f2d.gif)

Screen1の真ん中のボタンをタップすると、Screen2のBottomNavigationIconが飛び跳ねるようになりました
小規模なアプリであれば、riverpodなども使わず、シンプルにIconをBounceContainerでラップすれば実現可能です

# riverpodを使う場合
アプリがある程度の規模になってくると上記の例では厳しそうです
BounceContainerのインスタンスを、アニメーションをトリガーする子や孫、ひ孫Widgetにどんどん渡していく必要が出てきます
それを避けるために、riverpodでグローバルにトリガーをかけるような実装にしてみます

## riverpodを追加する
### ProviderScopeを追加する
```dart:main.dart
void main() {
  runApp(
      ProviderScope( // <<<<< 追加
          child: MyApp()
      )
  );
}

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

## StateProviderでアニメーションをトリガーする
### BounceContainerをConsumerWidgetでラップする
bounceItemProviderをwatchして、状態に変化があれば飛び跳ねるようにします
この実装は強引だと思います

```dart:bounce_item.dart
final bounceItemProvider = StateProvider((_) => false);

class BounceItem extends ConsumerWidget {
  final child = BounceContainer(child: Icon(Icons.looks_two_rounded));

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    watch(bounceItemProvider).state;
    child.bounce(); // このままだと初回でanimControllerが初期化されない
    return child;
  }
}
```

しかしこのままだと、最初にWidgetが描画されるときに、BounceContainer内のanimControllerが初期化される前にアニメーションを開始しようとしてしまい、「late変数が初期化されてないよ！」と言うエラーが出ます

### BounceContainerのbounce()をnullチェックする
animControllerをnullableにして、初期化されていない場合にはアニメーションをしないようにします
この実装は強引だと思います（2度目)

```dart:bounce_container.dart
class BounceContainer extends StatefulWidget {
  // ~~ 略 ~~
}

final _bounceUp = // ~~ 略 ~~
final _bounceDown = // ~~ 略 ~~
final _sequence = // ~~ 略 ~~

class _BounceContainerState extends State<BounceContainer> with SingleTickerProviderStateMixin {
  AnimationController? animController; // nullableにする
  late Animation<double> animTween;

  @override
  void initState() {
    super.initState();

    animController = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 2000),
    );
    animTween = _sequence.animate(animController!);
  }

  @override
  void dispose () {
    animController!.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: animController!,
        builder: (context, child) {
          return Transform.translate(
              offset: Offset(0, animTween.value),
              child: widget.child);
        }
    );
  }

  void animate() {
    // 初回描画時、animControllerが未初期化の場合はアニメーションを実行しないようにする
    if (animController != null) {
      animController!.reset(); // resetしないと2回目以降に動かない
      animController!.forward();
    }
  }
}
```

## トリガーをかける側の実装
### 全体レイアウト
StateProvider経由でトリガーをかけるので、BounceContainerインスタンスの受け渡しは一切なくなりました

```dart:main.dart
class MyHomePageState extends State<MyHomePage> {
  final bounceIcon = BounceItem();
  var screenIndex = 0;
  late List<Widget> screens;

  @override
  void initState() {
    super.initState();

    screens = [
      Screen1(),
      Screen2()
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        toolbarHeight: 0,
      ),
      body: IndexedStack(
        index: screenIndex,
        children: screens,
      ),
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(
              icon: Icon(Icons.looks_one_rounded),
              label: 'screen 1'
          ),
          BottomNavigationBarItem(
              icon: bounceIcon,
              label: 'screen 2'
          ),
        ],
        currentIndex: screenIndex,
        onTap: (index) {
            setState(() {
              screenIndex = index;
            });
        },
      ),
    );
  }
}
```

### トリガーをかける画面側の実装
BounceItemのStateProviderを使って、状態を反転させてトリガーします

```dart:screens.dart
class Screen1 extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final state = watch(bounceItemProvider).state;
    final controller = context.read(bounceItemProvider);

    return Center(
        child: ElevatedButton(
            onPressed: () {
              controller.state = !state; // 状態を反転させてトリガー
            },
            child: Text('jump!')
        )
    );
  }
}
```

もちろん結果は同じです
![simple_bounce.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/924658/335b4658-64b5-07d7-b39d-ea958b1f9f2d.gif)