状态机与输入驱动的运动
将 Rive 状态机输入连接到应用数据,实现响应式的分支动画。
状态机与输入驱动的运动 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why State Machines Beat Linear Animations
Traditional Rive animations play a single timeline from start to finish. A state machine is a graph of animation states connected by transitions, and those transitions fire based on inputs you control from Dart.
- Reactive: the animation reflects live app data instead of a fixed sequence.
- Branching: the same artboard can idle, hover, succeed, or fail depending on input values.
- Blended: Rive interpolates between states, so you get smooth motion for free.
In this lesson you will wire a Rive state machine's inputs to your widget tree so motion follows user actions and model state.
The Three Input Types
A Rive state machine exposes exactly three input kinds, and choosing the right one is the core design decision.
- Boolean (
SMIBool): a held on/off value, e.g.isOpenorisLoading. - Number (
SMINumber): a continuous value that can drive blend states, e.g. a 0–100 progress or a scroll offset. - Trigger (
SMITrigger): a one-shot pulse that fires a transition once, e.g.tapSuccess. It has no persistent value.
Rule of thumb: use a Trigger for momentary events, a Boolean for state you hold, and a Number for anything continuous you want to blend.
Loading the Artboard and State Machine
To drive inputs you need a StateMachineController. The onInit callback of RiveAnimation.asset hands you the loaded Artboard. You build a controller from a named state machine and attach it.
Always grab references to the inputs you need right here, while you still have the artboard, and store them on your widget's state.
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
StateMachineController? _controller;
SMIBool? _isLiked;
void _onRiveInit(Artboard artboard) {
final controller = StateMachineController.fromArtboard(
artboard,
'LikeMachine',
);
if (controller != null) {
artboard.addController(controller);
_controller = controller;
_isLiked = controller.findInput<bool>('isLiked') as SMIBool?;
}
}
@override
Widget build(BuildContext context) {
return RiveAnimation.asset(
'assets/like.riv',
stateMachines: const ['LikeMachine'],
onInit: _onRiveInit,
);
}
}Setting a Boolean Input from a Tap
Once you hold an SMIBool reference, driving it is just assigning to its value. Rive immediately evaluates transitions whose condition depends on that input.
Wrap the Rive widget in a GestureDetector and toggle the boolean. There is no need to call setState for the animation itself — Rive repaints internally — but you may want it to keep your own model in sync.
GestureDetector(
onTap: () {
final input = _isLiked;
if (input != null) {
input.value = !input.value; // flips the held state
}
},
child: RiveAnimation.asset(
'assets/like.riv',
stateMachines: const ['LikeMachine'],
onInit: _onRiveInit,
),
)Firing a Trigger for One-Shot Motion
A SMITrigger has no value. You call fire() to pulse it exactly once, which is perfect for confetti bursts, success checkmarks, or shake-on-error feedback.
Because triggers auto-reset, you never have to clear them. Firing again replays the transition.
class _SubmitButtonState extends State<SubmitButton> {
SMITrigger? _success;
SMITrigger? _error;
void _onInit(Artboard artboard) {
final c = StateMachineController.fromArtboard(artboard, 'SubmitMachine');
if (c != null) {
artboard.addController(c);
_success = c.findSMI('success') as SMITrigger?;
_error = c.findSMI('error') as SMITrigger?;
}
}
Future<void> _onSubmit() async {
final ok = await _saveForm();
if (ok) {
_success?.fire();
} else {
_error?.fire();
}
}
}Driving a Number Input for Blended States
Number inputs shine when motion must follow a continuous value. A blend state in Rive can morph an artboard across a 0–100 range, so feeding it live data produces fluid, data-driven motion.
Common sources: download progress, a scroll position, a slider, or a sensor reading. Clamp the value so it never exceeds the range the artist designed for.
class _ProgressGaugeState extends State<ProgressGauge> {
SMINumber? _progress;
void _onInit(Artboard artboard) {
final c = StateMachineController.fromArtboard(artboard, 'GaugeMachine');
if (c != null) {
artboard.addController(c);
_progress = c.findSMI('progress') as SMINumber?;
}
}
void updateProgress(double fraction) {
// fraction is 0.0..1.0 from a download stream
_progress?.value = (fraction * 100).clamp(0, 100);
}
}Connecting Inputs to a Stream of App Data
Input-driven motion becomes powerful when a data stream feeds it. Subscribe in initState, push each event into the matching input, and cancel the subscription in dispose to avoid leaks.
Notice you guard against a null input: the stream may emit before onInit has run, so a null-aware call keeps you safe.
late final StreamSubscription<double> _sub;
@override
void initState() {
super.initState();
_sub = downloadProgress$.listen((fraction) {
_progress?.value = (fraction * 100).clamp(0, 100);
});
}
@override
void dispose() {
_sub.cancel();
_controller?.dispose();
super.dispose();
}Modeling Branching Logic in Pure Dart
Before touching Rive, it helps to model the decision that selects a branch as plain Dart. Here is a self-contained mapper that turns an upload result into the kind of input event you would fire. Testing this logic in isolation keeps your widget thin.
This snippet has no Flutter or Rive dependency, so it runs anywhere.
enum MotionEvent { idle, loading, success, error }
MotionEvent selectMotion({
required bool inFlight,
required bool? succeeded,
}) {
if (inFlight) return MotionEvent.loading;
if (succeeded == null) return MotionEvent.idle;
return succeeded ? MotionEvent.success : MotionEvent.error;
}
void main() {
print(selectMotion(inFlight: true, succeeded: null)); // loading
print(selectMotion(inFlight: false, succeeded: true)); // success
print(selectMotion(inFlight: false, succeeded: false)); // error
print(selectMotion(inFlight: false, succeeded: null)); // idle
}Applying a Branch Decision to Inputs
Now bind that pure decision to the state machine. A single method translates a MotionEvent into the correct combination of boolean holds and trigger pulses.
- Booleans hold state, so set both the new value and clear the opposite where needed.
- Triggers fire once for transient feedback.
Centralizing this in one method keeps your input wiring consistent and easy to audit.
void applyMotion(MotionEvent event) {
switch (event) {
case MotionEvent.loading:
_isLoading?.value = true;
break;
case MotionEvent.idle:
_isLoading?.value = false;
break;
case MotionEvent.success:
_isLoading?.value = false;
_success?.fire();
break;
case MotionEvent.error:
_isLoading?.value = false;
_error?.fire();
break;
}
}Reading Input State Back via Listeners
Sometimes motion is driven by Rive itself (e.g. a draggable knob the user moves on the artboard) and you need the current value back in Dart. Inputs expose their value, and you can poll or react to it inside the state machine's change callback.
The StateMachineController also accepts an onStateChange callback so you can run Dart whenever the active state transitions, useful for analytics or chaining side effects.
void _onInit(Artboard artboard) {
final c = StateMachineController.fromArtboard(
artboard,
'KnobMachine',
onStateChange: (machineName, stateName) {
debugPrint('Entered $stateName in $machineName');
if (stateName == 'Snapped') {
HapticFeedback.lightImpact();
}
},
);
if (c != null) {
artboard.addController(c);
_angle = c.findSMI('angle') as SMINumber?;
}
}Lifecycle, Disposal, and Common Pitfalls
State machines are stateful resources. Treat them with the same discipline as controllers and subscriptions.
- Always dispose the
StateMachineControllerindispose()to release the artboard binding. - Null-check inputs:
findSMI/findInputreturn null if the name is misspelled or the artboard lacks that input — a silent no-op otherwise. - Match names exactly: input and state-machine names are case-sensitive and must match the
.rivfile. - Do not over-call setState: Rive repaints itself; rebuilding the whole subtree on every stream tick wastes frames.
Get these right and your input-driven motion stays smooth and leak-free.
Quick Check: Choosing the Right Input
You are wiring a Rive state machine to a form submit button. Tapping submit should play a one-time success checkmark animation that then returns to idle. Which input type and call is correct?
Recap: Reactive, Branching Motion
You now know how to make Rive animations follow your app instead of a fixed timeline.
- Three inputs: Boolean for held state, Number for continuous/blended values, Trigger for one-shot events.
- Wiring: build a
StateMachineControllerinonInit, attach it to the artboard, and cache input references. - Driving: assign to
valuefor booleans/numbers, callfire()for triggers, and feed continuous data from streams with clamping. - Architecture: model the branch decision in pure, testable Dart, then translate it to inputs in one central method.
- Hygiene: dispose the controller, cancel subscriptions, null-check inputs, and match names exactly.
With these patterns your animations become a live reflection of user input and model state.
用 AI 导师学习 Dart — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 22
- 课程
- 88
常见问题解答
「状态机与输入驱动的运动」课时是免费的吗?
是的 — 「状态机与输入驱动的运动」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。
「状态机与输入驱动的运动」这节课中我会学到什么?
将 Rive 状态机输入连接到应用数据,实现响应式的分支动画。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Flutter Mobile Development 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「状态机与输入驱动的运动」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Flutter Mobile Development 课中编写并运行代码吗?
能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。