RIVEをFlutterで使ってみる

# はじめに
こんばんは。OREOです。
2024/6/12, 2024/6/13に開催された国内初のグローバルFlutterイベント[FlutterNinjas](https://flutterninjas.dev)に参加してきました！
素晴らしいセッションが盛りだくさんで非常に勉強になりました。
個人的に気になったセッションについて実際に実装してみようということで，何個か記事を書いてみようと思います！

# RIVEとは
[RIVE（リーヴ）](https://rive.app/)は，インタラクティブでリアルタイムなアニメーションを作成するためのデザインツールです。UIやゲームのアニメーションを作成・デプロイでき，ベクターグラフィックスを使用するため高品質でスケーラブル。
FlutterやReact Nativeなどにも統合可能になっています。

例えばこんなアニメーションが作れるようです。
![画面収録 2024-06-15 20.59.34.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/36912ddf-c5e1-6886-3d56-50cd561f46b9.gif)

[参照元](https://rive.app/community/files/11097-21255-first-attempt/)

# Flutter上でアニメーションを動かしてみる
アニメーションの詳しい作成方法については本記事では割愛します。
この2種類のアニメーションをアプリに実装していきたいと思います。
アニメーションの違いとしては走行中はスクーターが跳ねているというところだけです。

|走行中|停止中|
|-|-|
|![画面収録 2024-06-16 0.35.11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/16172418-b1b5-7042-c0b7-8150498292b9.gif)|![画面収録 2024-06-16 0.35.22.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/b2d31fd6-92d0-41f5-1912-8e856b8313df.gif)
|

## contollerの初期化
StateMachineControllerを設定を行います。今回はRIVEファイルのState Machine名は"State Machine 1"で設定しました。


```dart
  void _onInit(Artboard artboard) {
    final controller =
        StateMachineController.fromArtboard(artboard, 'State Machine 1');

    if (controller == null) {
      return;
    }

    artboard.addController(controller);
    _inputId = controller.getNumberInput('id');
  }

```
RIVEの設定画面
![スクリーンショット 2024-06-16 0.43.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/ec635c1f-bb56-7978-b7e4-38068f7930a9.png)

## RiveAnimation Widget
RIVEからエクスポートしたファイルを任意のディレクトに保存してください。今回は`assets/animations`配下に保存しています。
今回はローカルファイルで実装を行ったので`.asset`を使用しています。
```dart
        SizedBox(
          height: 450,
          width: 500,
          child: RiveAnimation.asset(
               'assets/animations/mylogo.riv',
               // idによってアニメーションを切り替える
               animations: [_inputId?.value == 1 ? 'Running' : 'Idling'],
               // 初期化時にコントローラーを取得
               onInit: _onInit,
             ),
           ),
```

## コード全容

:::note info
アニメーションの状態をわかりやすくするためにアニメーション切り替えボタンとテキストを追加しています。
:::

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

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  SMIInput<double>? _inputId;

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

  void _onInit(Artboard artboard) {
    final controller =
        StateMachineController.fromArtboard(artboard, 'State Machine 1');

    if (controller == null) {
      return;
    }

    artboard.addController(controller);
    _inputId = controller.getNumberInput('id');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.black87,
        title: const Center(
          child: Text(
            'Test App',
          ),
        ),
      ),
      backgroundColor: const Color.fromARGB(255, 2, 39, 102),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              color: Colors.amber,
              width: double.infinity,
              child: const Padding(
                padding: EdgeInsets.all(8.0),
                child: Text('SCOOTER IS',
                    textAlign: TextAlign.center,
                    style: TextStyle(
                      fontSize: 30,
                      fontWeight: FontWeight.bold,
                    )),
              ),
            ),
            Container(
              color: _inputId?.value == 1
                  ? const Color.fromARGB(255, 56, 131, 58)
                  : const Color.fromARGB(255, 220, 73, 63),
              width: double.infinity,
              child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(_inputId?.value == 1 ? 'RUNNING' : 'STOP',
                    textAlign: TextAlign.center,
                    style: const TextStyle(
                      fontSize: 30,
                      fontWeight: FontWeight.bold,
                    )),
              ),
            ),
            SizedBox(
              height: 450,
              width: 500,
              child: RiveAnimation.asset(
                'assets/animations/mylogo.riv',
                // idによってアニメーションを切り替える
                animations: [_inputId?.value == 1 ? 'Running' : 'Idling'],
                // 初期化時にコントローラーを取得
                onInit: _onInit,
              ),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _inputId?.value = 1;
                    });
                  },
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all(
                        const Color.fromARGB(255, 56, 131, 58)),
                  ),
                  child: const Text('Start'),
                ),
                ElevatedButton(
                  onPressed: () {
                    setState(() {
                      _inputId?.value = 2;
                    });
                  },
                  style: ButtonStyle(
                    backgroundColor: MaterialStateProperty.all(
                        const Color.fromARGB(255, 220, 73, 63)),
                  ),
                  child: const Text('Stop!'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

```

# 実際の動作のようす

![画面収録 2024-06-16 0.51.59.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1550392/1470aff1-1135-4f75-91ad-23a45e0188c2.gif)

# さいごに
RIVEを使ってアニメーションの実装ができました！
アニメーション内部にListnerを持たせることもできる様なのでその辺の機能はまたの機会にトライしてみようと思います。