inSmartBank

ワンバンクを運営する株式会社スマートバンクのメンバーによるブログです

Rive で叶えるコードレスなインタラクティブアニメーション

スマートバンクでモバイルエンジニアをしている yokomii です。

先日リリースされたワンバンクアプリの新機能「AI埋蔵金チェッカー」の実装を担当しました。

smartbank.co.jp

本機能では、ユーザーアクションに応じたインタラクティブなアニメーションを実現するため、新たに「Rive」というアニメーションツールを導入しています。

rive.app


🐶Rive導入の背景と目的🐶

AI埋蔵金チェッカーでは、以下のようなアニメーション要件がありました。

  1. コードベースでは実装負荷が大きい、リッチなアニメーションの実現
  2. ノーコードアニメーションとコードベースアニメーションの連動
  3. iOS / Android で一貫したアニメーション体験

これらを満たす手段として、 Rive の導入に至りました。

1. コードベースでは実装負荷が大きい、リッチなアニメーションの実現

↓実際のアニメーションの動画です

細かく連動するパーツやタイミングの精度が求められるアニメーションを、すべてコードで制御しようとすると、設計・実装・調整の各工程で非常に大きな負担が発生します。

Rive を用いることで、デザイナーが細部にまでアニメーション意図を反映できるようになり、アニメーション実装のスピードと柔軟性が飛躍的に向上します。

2. ノーコードアニメーションとコードベースアニメーションの連動

ノーコードアニメーションの途中でコードベースの処理(アニメーション・UI操作など)を差し込んだり、その後にまたノーコードアニメーションを再開するといった連携も求められました。

Rive には Events というシグナルを送信⇄監視する仕組みがあり、これを用いることで本要件を容易に満たすことができました。

3. Android と iOS で一貫したアニメーション体験

Rive は、Android / iOS / Flutter / Web などの主要プラットフォームに対応したランタイムを提供しています。

1つの .riv ファイルを共通で使い回すことで、プラットフォーム間で表現がブレないのは大きなメリットです。

実際に今回も、アセットの再利用性を活かして、アニメーションに関する調整やフィードバックの反映を高速に回すことができました。

🐶実装のステップ🐶

Rive の導入は、ざっくり以下のステップで進めました。

  1. Rive エディタでアニメーションの作成
  2. StateMachine(Input/Event)の設計と設定
  3. アプリ側の実装(.riv ファイルの組み込み)

1. Rive エディタでアニメーションの作成

Rive では、アニメーションの作成から書き出しまでを一貫して行える公式エディタが提供されており、After Effects などの外部ツールを併用する必要がありません。

今回の実装では、アニメーションの制作をプロダクトデザイナーが担当し、エンジニアは .riv ファイルをそのままアプリに組み込むというスムーズな分業体制を実現できました。

また、 Rive エディタはアニメーションのタイムライン構成を視覚的に確認できるため、仕様のすれ違いが減り、エンジニアもアニメーションの挙動を直接理解しやすいというメリットもあります。

2. State Machine(Inputs/Events)の設計と設定

Rive は、アニメーションをただ再生するだけでなく、「入力値の状態」によってアニメーションを変動させることができます。これを実現しているのが State Machine という仕組みです。

AI埋蔵金チェッカーでは、アニメーションを初期状態では静止させて、ユーザーアクション(開始ボタンの押下)によって再生するために Trigger Input(一度だけ値が true になる入力値)を定義しています。

// startAnimation (Trigger Input) をアプリから有効化するコード
RiveAnimationView.setBooleanState(
    stateMachineName = "stateMachine",
    inputName = "startAnimation",
    value = true,
)

逆に Rive 側から任意のタイミングでアプリコードを実行したい場合は、先ほど軽く説明した Events を用います。

AI埋蔵金チェッカーでは、Event をアニメーションの Timeline に設定することで、オブジェクトの動きに連動した Haptic Feedback を実現しています。

// Event を監視して、Haptic Feedback を実行するコード
val listener = object : RiveFileController.RiveEventListener {
    override fun notifyEvent(event: RiveEvent) {
        when(event.name) {
            "hapticMine" -> {
                Vibrator.vibrate(VibrationEffect.createOneShot(200, VibrationEffect.DEFAULT_AMPLITUDE))
            }
        }
    }
}

riveView.addEventListener(listener)

State Machine の設計には、状態遷移やイベント駆動の理解など、一定のプログラミング知識が必要です。そのため、全体の構成はエンジニアが主導しつつ、デザイナーと密に連携しながら設計・検証を進めるのがスムーズでした。

なお、現在は Inputs/Events の代わりに、Data Binding を用いて状態管理をすることができます。

3. アプリ側の実装(.riv ファイルの組み込み)

以下は .riv ファイルをアプリで再生するための実装例(Android)です。

@OptIn(ControllerStateManagement::class)
@Composable
fun RiveAnimation(
    state: RiveAnimationUiState,
    modifier: Modifier = Modifier,
    controllerState: ControllerState? = null,
    onEvent: (RiveAnimationView, RiveEvent) -> Unit = { _, _ -> },
    onUpdate: (RiveAnimationView) -> Unit = {},
    onSavedControllerState: (ControllerState) -> Unit = {},
) {
    fun RiveAnimationView.setRiveResource(state: RiveAnimationUiState) {
        setRiveResource(
            resId = state.resId,
            fit = state.fit,
            artboardName = state.artboardName,
            stateMachineName = state.stateMachineName,
        )
    }

    val context = LocalContext.current
    val riveView = remember {
        SaveControllerStateRiveAnimationView(context).apply {
            if (controllerState == null) {
                setRiveResource(state)
            } else {
                restoreControllerState(controllerState)
            }
            this.onSavedControllerState = onSavedControllerState
        }
    }

    var oldState by remember { mutableStateOf(state) }
    LaunchedEffect(state) {
        if (oldState != state) {
            riveView.setRiveResource(state)
            oldState = state
        }
    }

    val currentOnEvent by rememberUpdatedState(onEvent)
    DisposableEffect(riveView) {
        val listener = object : RiveFileController.RiveEventListener {
            override fun notifyEvent(event: RiveEvent) {
                currentOnEvent(riveView, event)
            }
        }
        riveView.addEventListener(listener)
        onDispose { riveView.removeEventListener(listener) }
    }

    AndroidView(
        factory = { riveView },
        update = { onUpdate(riveView) },
        modifier = modifier,
    )
}

data class RiveAnimationUiState(
    @RawRes val resId: Int,
    val artboardName: String,
    val stateMachineName: String,
    val fit: Fit = Fit.CONTAIN,
)

/**
 * アニメーション状態情報([ControllerState])を保存するための [RiveAnimationView] の拡張
 */
@OptIn(ControllerStateManagement::class)
private class SaveControllerStateRiveAnimationView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
) : RiveAnimationView(context, attrs) {

    var onSavedControllerState: (ControllerState) -> Unit = {}

    override fun onDetachedFromWindow() {
        val controllerState = saveControllerState()
        controllerState?.let { onSavedControllerState(it) }
        super.onDetachedFromWindow()
    }
}

実装のポイントとしては下記の通りです。

  • Compose サポートが未対応であるため、 AndroidView コンポーネントを用いる
  • ControllerState によって、構成の変更後にアニメーション状態を復元する

🐶まとめ🐶

Rive を新たに導入することで、これまでのワンバンクにはなかったインタラクティブでリッチなアニメーション体験を実現することができました。

単に見た目が華やかになるだけでなく、ユーザーのアクションに反応してアニメーションが変化することで、プロダクトとしての"楽しさ"や"気持ちよさ"を感じてもらえる体験がつくれたと思います。

もちろん、アニメーションの内製にはまだ課題もあります。たとえば、 State Machine の設計や Events の定義など、一定のプログラミング知識を前提とする工程は属人化しやすく、運用コストも無視できません。
ですが、今回のようにデザイナーとエンジニアが協力しながらワークフローを整えることで、コードとデザインの境界を越えた開発体験が可能になることも改めて実感しました。

今後は、こうした表現をさらにプロダクト全体に広げ、家計管理や節約といった行為に、ポジティブな感情を結びつける体験を作っていければと考えています。


スマートバンクの採用情報はこちらをご確認ください。

smartbank.co.jp

We create the new normal of easy budgeting, easy banking, and easy living.
In this blog, engineers, product managers, designers, business development, legal, CS, and other members will share their insights.