0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

simple_bounce.gif

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

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

注意事項

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

riverpodのバージョン指定

flutter_riverpod: ^0.14.0+3

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

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

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

riverpodを使う場合

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

riverpodを追加する

ProviderScopeを追加する

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して、状態に変化があれば飛び跳ねるようにします
この実装は強引だと思います

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度目)

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インスタンスの受け渡しは一切なくなりました

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を使って、状態を反転させてトリガーします

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?