カテゴリー: Tech

【Flutter】ボタンタップ時に展開するアニメーションがしたい!

はじめに

Flutterでボタンなどを設置するのはとても簡単ですが、タップした時に展開するようなアニメーションをつけたいと思い今回は記事にしました。

完成イメージは以下の様な形です。

Flowウィジェット

今回Flowウィジェットを使用し実装したいと思います。Flowウィジェットは以下の様にして使用します。

Flow(
  delegate: MyFlowDelegate(),
  children: [
    Icon(Icons.add),
    Icon(Icons.mail),
  ],
),

delegateにはFlowDelegateを継承したクラス、childrenには他のウィジェット同様に子となるウィジェットを配置します。FlowDelegateを継承したクラスMyFlowDelegateを作成すると以下のようになります。

class MyFlowDelegate extends FlowDelegate {
  @override
  void paintChildren(FlowPaintingContext context) {
    // TODO: implement paintChildren
  }

  @override
  bool shouldRepaint(covariant FlowDelegate oldDelegate) {
    // TODO: implement shouldRepaint
    throw UnimplementedError();
  }
}

paintChildrenではcontext.paintChildを使用します。第一引数には子ウィジェットのindexを指定し、第二引数で位置の調整を行います。

void paintChildren(FlowPaintingContext context) {
  context.paintChild(0 ,transform: Matrix4.identity());

なので、以下の様に設定すると相対的に位置を設定し、今回作成したいボタンの展開など、同じ動きのアニメーションを複数のウィジェットに指定することができます。

void paintChildren(FlowPaintingContext context) {
  for (int i = 0; i < context.childCount; ++i) {
    final offset = i * 50.0;
    context.paintChild(
      i,
      transform: Matrix4.translationValues(
        -offset,
        -offset,
        0,
      ),
    );
  }
}

Animation

Flutterのアニメーションの基本となるクラスです。現在の値とアニメーションの状態(終了したかアニメーション中かなど)だけを持ちます。Listenableを継承しているので値の変更を監視することはできますが、このクラスは値を保持するだけなので、 時間経過に合わせて値を変更したり外から値を変更するためにはAnimationControllerを使う必要があります。

AnimationController

自分の値を動的に変更できるAnimationのサブクラスです。外から現在の値を渡す方法と、 指定されたDurationで自動的にアニメーションを動かす方法の二種類でアニメーションを管理することができます。
自動でアニメーションする場合はforwardで0.0~1.0で値が変化し、reverseで1.0~0.0で値が変化します。今回のサンプルでは以下の様に実装しています。

AnimationController menuAnimation;

menuAnimation = AnimationController(
  duration: const Duration(milliseconds: 250),
  vsync: this,
);

vsyncはTickerProviderです、AnimationControllerを保持しているStateクラスにSingleTickerProviderStateMixinをmixinすると、 そのStateクラスがTickerProviderになるので、AnimationControllerにはthisを渡すだけで済みます。

class _FlowMenuState extends State<FlowMenu>
    with SingleTickerProviderStateMixin {
  AnimationController menuAnimation;

  @override
  void initState() {
    super.initState();
    menuAnimation = AnimationController(
      duration: const Duration(milliseconds: 250),
      vsync: this,
    );
  }
}

これであとはボタンタップ時にAnimationの状態を確認して、forwardかreverseを渡してあげます。

onPressed: () {
  _updateMenu(icon);
  menuAnimation.status == AnimationStatus.completed
      ? menuAnimation.reverse()
      : menuAnimation.forward();
},

 

実装

それではFlowウィジェットを使用したサンプルを作成します。コードは以下になります。

import 'package:flutter/material.dart';

void main() => runApp(const FlowApp());

class FlowApp extends StatelessWidget {
  const FlowApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flow Example'),
        ),
        body: const FlowMenu(),
      ),
    );
  }
}

class FlowMenu extends StatefulWidget {
  const FlowMenu({Key key}) : super(key: key);

  @override
  State<FlowMenu> createState() => _FlowMenuState();
}

class _FlowMenuState extends State<FlowMenu>
    with SingleTickerProviderStateMixin {
  AnimationController menuAnimation;
  IconData lastTapped = Icons.notifications;
  final List<IconData> menuItems = <IconData>[
    Icons.menu,
    Icons.new_releases,
    Icons.notifications,
    Icons.settings,
    Icons.home,
  ];

  void _updateMenu(IconData icon) {
    if (icon != Icons.menu) {
      setState(() => lastTapped = icon);
    }
  }

  @override
  void initState() {
    super.initState();
    menuAnimation = AnimationController(
      duration: const Duration(milliseconds: 250),
      vsync: this,
    );
  }

  Widget flowMenuItem(IconData icon) {
    final double buttonDiameter =
        MediaQuery.of(context).size.width / menuItems.length;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: RawMaterialButton(
        fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
        splashColor: Colors.amber[100],
        shape: const CircleBorder(),
        constraints: BoxConstraints.tight(Size(buttonDiameter, buttonDiameter)),
        onPressed: () {
          _updateMenu(icon);
          menuAnimation.status == AnimationStatus.completed
              ? menuAnimation.reverse()
              : menuAnimation.forward();
        },
        child: Icon(
          icon,
          color: Colors.white,
          size: 45.0,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Flow(
      delegate: FlowMenuDelegate(menuAnimation: menuAnimation),
      children:
      menuItems.map<Widget>((IconData icon) => flowMenuItem(icon)).toList(),
    );
  }
}

class FlowMenuDelegate extends FlowDelegate {
  FlowMenuDelegate({@required this.menuAnimation})
      : super(repaint: menuAnimation);

  final Animation<double> menuAnimation;

  @override
  bool shouldRepaint(FlowMenuDelegate oldDelegate) {
    return menuAnimation != oldDelegate.menuAnimation;
  }

  @override
  void paintChildren(FlowPaintingContext context) {
    for (int i = context.childCount - 1; i >= 0; i--) {
      print(menuAnimation.value);
      double dx = (context.getChildSize(i).height + 10) * i;
      context.paintChild(
        i,
        transform: Matrix4.translationValues(0, dx * menuAnimation.value + 10, 0),
      );
    }
  }
}

これでボタンタップで展開する以下の様なアニメーションが実装できました。

 

さいごに

FABをタップした時に展開するアニメーションなど、それ以外にも相対的に位置を判定して、同じ動きをするアニメーションが作成できるので、要所要所で使っていきたいです。

おすすめ書籍

nukky

シェア
執筆者:
nukky
タグ: Flutter

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

2週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

4週間 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前