🐅

【Flutter】Rive→Flutter StateMachineを使ったアニメーション

2024/02/29に公開

今回やること

『Rive』アニメーションツールを使って状態を管理し、アニメーションさせる実装を Flutter にて行っていきます。

ボックスの選択、バウンドの回数は Rive 内で state を更新し、DarkMode と Move(始動)は Flutter から入力します。

実行例

https://rive.app/

Rive でアニメーションを作成する

数字の書かれたボックスを選択(タップ)し、その数字の数だけバウンドするアニメーションを作成します。

バウンドアニメーションは Constraints でボールを Path に追従するようにし、Distance を増加させ動かします。
スケールなども調整すると良いでしょう。
実行例

State Machine の設定

Rive では state を設定、保持してくれるので、その state を使いインタラクティブなアニメーションを作成することができます。
今回設定した state パラメータを紹介します。

Inputs

  • isMove (bool)   →   true になると動き出す
  • isDark (bool)   →   true でダークモードになる
  • count (Number)   →   バウンドする(動く)回数
    実行例

Listeners

  • select0   →   target(0 のボックス)をタップすると count の値を 0 へ変更
  • select1   →   target(1 のボックス)をタップすると count の値を 1 へ変更
  • select2 , select3 同様
    実行例

Animations

  • dark , light   →  ボックスの色のみを定義 動かなくても特定のパラメータを定義できる

  • move1~3   →   move◯ の ◯ 回バウンドするアニメーション

  • select0~3   → ボールの初期状態と選択されたボックスに枠線を付ける

    実行例

Rive 側での動き

実行例

実行例

下記リンクより実際の動きを確認できます。
https://rive.app/community/8510-16308-state-machine-sample

全体の解説

実行例

Flutter 側で実装する

まずはパッケージをインストール
https://pub.dev/packages/rive

flutter pub add rive

まずは、作成した.riv ファイルを/asset などに入れます。画像などと同様です。

コードは下記のようになります。State Machine を使用する場合は onInit を定義する必要が
あります。今回は Inputs も使用するのでそれぞれ定義します。

import 'package:rive/rive.dart';

SMIInput<bool>? _isDarkModeInput;
SMIInput<bool>? _isMoveInput;
SMIInput<double>? _count; // InputsでNumberを選択した場合の型はdouble

/// RiveAnimationの初期化 Widget build時に呼ばれる
void _onRiveInit(Artboard artboard) {
    final controller = StateMachineController.fromArtboard(
        artboard,
        'State Machine 1', // Animations欄の StateMachine名
        onStateChange: _onStateChange, // callback関数
    );
    artboard.addController(controller!);
    // それぞれInputを定義 初期値はnullなので注意が必要
    // 'isDark'などのテキストはInputsの表示名と対応します
    _isDarkModeInput = controller.findInput<bool>('isDark') as SMIBool;
    _isMoveInput = controller.findInput<bool>('isMove') as SMIBool;
    _count = controller.findInput<double>('count') as SMINumber;
}

/// Widget
RiveAnimation.asset(
    'assets/rive/sample.riv',
    onInit: _onRiveInit,
),

後は Inputs のそれぞれの値を入れてあげれば動作します。
isMove に true を入れると count 回バウンドします。

Inputs の現在の値を取得することもできます。init 時は SMIInput<bool>? isMoveInput は null で入ってくるので処理には注意が必要です。

Inputs の count は Rive 内でボックスをタップした際に数値が変更されます。

Move の切り替え

ElevatedButton(
    onPressed: () {
    _isMoveInput!.value = !_isMoveInput!.value;
    },
    child: Text(_isMoveInput != null
        ? _isMoveInput!.value
            ? "Reset"
            : "Move"
        : "Move"),
),

Dark Mode の切り替え

Switch(
    onChanged: (value) => setState(() {
         _isDarkModeInput!.value = !_isDarkModeInput!.value;
    }),
    value: _isDarkModeInput != null ? _isDarkModeInput!.value : false,
)

実行例

callback について

_onRiveInit()の onStateChange で設定した callback で現在実行している Animations の
Animation 名をテキストで取得できます。

callback
String _currentAnimationState = '';

/// アニメーションの状態が変わった時の処理
void _onStateChange(
    String stateMachineName,
    String stateName,
) =>
    setState(
        () => _currentAnimationState = stateName,
    );

下 の "animation state" が現在実行している Animation 名です。
実行例

その他の実装例

https://x.com/_isaji134/status/1738141657156182157?s=20

終わりに

Rive ではここで紹介した機能の他に、たくさんの機能を備えています。
公式の YouTube も頻繁に更新されていてチュートリアル動画もとても分かりやすので、
是非チェックして見てください。

https://www.youtube.com/@Rive_app

コード全文

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

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

  
  State<RiveSample> createState() => _RiveSampleState();
}

class _RiveSampleState extends State<RiveSample> {
  SMIInput<bool>? _isDarkModeInput;
  SMIInput<bool>? _isMoveInput;
  SMIInput<double>? _count;

  String _currentAnimationState = '';

  /// アニメーションの状態が変わった時の処理
  void _onStateChange(
    String stateMachineName,
    String stateName,
  ) =>
      setState(
        () => _currentAnimationState = stateName,
      );

  void _onRiveInit(Artboard artboard) {
    final controller = StateMachineController.fromArtboard(
      artboard,
      'State Machine 1',
      onStateChange: _onStateChange,
    );
    artboard.addController(controller!);

    _isDarkModeInput = controller.findInput<bool>('isDark') as SMIBool;
    _isMoveInput = controller.findInput<bool>('isMove') as SMIBool;
    _count = controller.findInput<double>('count') as SMINumber;

    /// 初期値の入力
    _count?.value = 0;
    _isDarkModeInput?.value = false;
    _isMoveInput?.value = false;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      /// RiveAnimationのisDarkを監視して、BGを変更
      backgroundColor: _isDarkModeInput != null
          ? _isDarkModeInput!.value
              ? Colors.grey
              : Colors.white
          : Colors.white,
      body: Center(
        child: Column(
          children: [
            SizedBox(
              width: double.infinity,
              height: 300,
              child: RiveAnimation.asset(
                'assets/rive/sample.riv',
                onInit: _onRiveInit,
                fit: BoxFit.contain,
              ),
            ),
            Text(
              "count : ${_count != null ? _count!.value : ""}",
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            Text(
              "animation state : $_currentAnimationState",
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text("Dark Mode"),
                Switch(
                  onChanged: (value) => setState(() {
                    _isDarkModeInput!.value = !_isDarkModeInput!.value;
                  }),
                  value: _isDarkModeInput != null ? _isDarkModeInput!.value : false,
                ),
              ],
            ),
            ElevatedButton(
              onPressed: () {
                _isMoveInput!.value = !_isMoveInput!.value;
              },
              child: Text(_isMoveInput != null
                  ? _isMoveInput!.value
                      ? "Reset"
                      : "Move"
                  : "Move"),
            ),
          ],
        ),
      ),
    );
  }
}

Discussion