Some observations and requests
Thank you for making this amazing tool. I’ve had a chance to use it a few times since late last year, and thought I’d explain how I’ve used it and make some feature requests.

First I created Christmas lights, then Christmas presents and then Easter eggs using v6. For each of these I needed to adjust various colors, set rotations, and turn components on and off. I needed to import from the src dir to get to these, so thank you for exposing the components so we no longer have to. However it still involves a fair bit of searching and experimentation to figure out how to get the right components and then how to achieve the desired results.
I create many icons/images white and set their color in Flutter. I want to use Rive in a similar way, and every time I’ve used Rive I’ve needed to change colors. I’d love to have a simple way to find an element by name, and set it’s fill or stroke color (internally searching it’s children for fills or strokes). Something like:
Artboard.getComponentByName(‘Ball’).setFillColor(Colors.red);
Artboard.getComponentByName(‘Ball’).y = 40.5;
Here is what I am doing for the lights:
rive.artboard.forEachComponent((child){
if (child.name == 'Lid') {
riveNode.Node node = child;
node.rotation = vMath.radians((num * 60).toDouble());
}
else if (child.name == 'Fill') {
Fill fill = child;
fill.paint.color = fillColor;
}
else if (child.name == 'Stroke') {
Stroke stroke = child;
stroke.paint.color = strokeColor;
}
else if (child.name == 'Glow') {
if (!inTimes) {
try {
riveNode.Node node = child;
node.opacity = 0;
} catch (e) {}
}
}
});
For each RiveFile I stored the bytes so I could create unique instances. Another request is for easy instances, which I see you’ve been working on.
I’ve used Rive recently to create a metronome and animated play button for a midi player.
https://user-images.githubusercontent.com/22208505/125040374-b937d800-e0d6-11eb-9e16-32cb9f46dfa2.mp4
The metronome took some figuring out. I first created a simple looping animation with a custom controller to adjust the speed, but it wasn’t totally accurate compared to the playback. Then I tried ping-pong, but that had the same problem. I needed a way to sync the metronome for each tick. I ended up with 2 oneShot animations, but I needed them to stay in their final position instead of resetting, so I removed the auto-reset. Most of my other oneShot uses require holding the final frame, but resetting before playing again. So another request is to include options to reset before playing or reset after playing.
I created an animation for the metronome weight from slowest to fastest. I wanted to set the time for this animation based on the speed of the music. I couldn’t find a good way to do this so I ended up setting the Y position.
I think it would be really useful for a variety of use cases (sliders, scroll bars and more) to be able to set an animation position, and a time to get there from the current position. For example, to move from the current time/position to 0.25 position over half a second:
AnimController.setTime(0.25, 0.5);
Since then I’ve been able to set the hand animation position which would probably work for the weight as well, but relies on the speed variable:
metronomeTick.speed = 0;
metronomeTick.play();
metronomeTick.time = 0.25;
For the play button, play and pause are oneShot animations with resets at the beginning instead of the end. For the loading I created an animation that included the entry, a looping section, and an exit. Now, I could split that into 3 separate animations but that would make it difficult to edit because I’d need to split it every time I made a change. Instead I created overrides for the start time, end time and loop type, and import the same animation 3 times with different values. To do this I had to create a custom LinearAnimation and controller, but it works really well, and I think it would be a useful feature to include as a default. To play the loading animations, I created an onLoop callback which is triggered when _didLoop = true; to chain them together, but gosh it was tricky. There are so many moving parts, trying to figure out how to get them to behave, and what part was going wrong was a nightmare. Added to that was a real problem that sometimes animations don’t quite get to 100% (which you can see at the end of the video) and so the callbacks are never called. I think there should be a way for this to work, but I haven’t been able to get it to be rock solid. For now what works every time is waiting on a Future.delayed between each part of the animation.
The play buttons on the first screen need color changes, but in this case changing the fill color didn’t work because it un-hid all my hidden layers (I guess you’re using color alpha to fade layers). I had to use this instead which retained my animated layer alphas:
if (c.coreType == 18) {
SolidColorBase solidColorBase = c;
solidColorBase.colorValue = color.value;
}
To recap, I’d really to easily set the color of a component without affecting the layer alpha, and a standard animation controller which can:
- Set the speed
- An OnLoop callback guaranteed to be triggered
- Option to reset before and/or after playing
- Custom start time, end time and loop type to manipulate different sections of an animation independently
The reason I ask instead of just doing it myself, is because I figure it will be much easier for you to do it since you know how it all works, and it makes sense to do it once rather than every user experimenting and figuring it out for themselves, and I think it will generally make working with Rive animations much more flexible and user friendly.
Here is the code for my controller and animation in case it’s useful:
class AnimController extends rive.RiveAnimationController<rive.RuntimeArtboard> {
AnimInstance _instance;
double _speedMultiplier;
/// Animation name
final String animationName;
/// Pauses the animation when it's created
final bool autoplay;
/// Mix value for the animation, value between 0 and 1
double _mix;
/// Fires when the animation stops being active
final VoidCallback onStop;
/// Fires when the animation starts being active
final VoidCallback onStart;
/// Fires when a looping animation loops
final VoidCallback onLoop;
/// Overrides for animation start time, end time and loop type
double startOverride = -1;
double endOverride = -1;
Loop loopOverride;
/// reset after playing OneShot?
final bool oneShotReset;
// Controls the level of mix for the animation, clamped between 0 and 1
AnimController(this.animationName, {
double mix = 1,
double speed = 1,
double start = -1,
double end = -1,
Loop loop,
this.autoplay = true,
this.oneShotReset = true,
this.onStop,
this.onStart,
this.onLoop,
}) {
_mix = mix.clamp(0, 1).toDouble();
_speedMultiplier = speed;
startOverride = start;
endOverride = end;
loopOverride = loop;
isActive = autoplay;
isActiveChanged.addListener(onActiveChanged);
}
AnimInstance get instance => _instance;
double get mix => _mix;
set mix(double value) => _mix = value.clamp(0, 1).toDouble();
set speed(double value) => _speedMultiplier = value;
set time(double value) => instance.time = value;
bool get didLoop => instance.didLoop;
@override
bool init(rive.RuntimeArtboard artboard) {
//_instance = artboard.animationByName(animationName);
var animation = artboard.animations.firstWhere(
(animation) => animation is LinearAnimation && animation.name == animationName,
orElse: () => null,
);
if (animation != null) {
_instance = AnimInstance(animation as LinearAnimation);
if (startOverride > -1)
_instance.setStartOverride(startOverride);
if (endOverride > -1)
_instance.setEndOverride(endOverride);
_instance.loopOverride = loopOverride;
if (onLoop != null)
_instance.onLoop = onLoop;
}
isActive = autoplay;
return _instance != null;
}
/// Dispose of any callback listeners
@override
void dispose() {
super.dispose();
isActiveChanged.removeListener(onActiveChanged);
}
@override
void apply(rive.RuntimeArtboard artboard, double elapsedSeconds) {
if (_instance == null) {
isActive = false;
}
if (!_instance.keepGoing && oneShotReset) {
isActive = false;
}
_instance
..animation.apply(_instance.time, coreContext: artboard, mix: mix)
..advance(elapsedSeconds * _speedMultiplier);
}
/// Resets the animation back to it's starting time position
void reset() => _instance?.reset();
/// Perform tasks when the animation's active state changes
void onActiveChanged() {
// If the animation stops and it is at the end of the one-shot, reset the
// animation back to the starting time
if (!isActive && oneShotReset) {
reset();
}
// Fire any callbacks
isActive
? onStart?.call()
// onStop can fire while widgets are still drawing
: WidgetsBinding.instance?.addPostFrameCallback((_) => onStop?.call());
}
void play()
{
isActive = true;
}
void stop()
{
isActive = false;
}
void reverse()
{
instance.direction = -instance.direction;
reset();
}
}
class AnimInstance {
final LinearAnimation animation;
double _time = 0;
double _totalTime = 0;
double _lastTotalTime = 0;
int _direction = 1;
bool _didLoop = false;
bool get didLoop => _didLoop;
double _spilledTime = 0;
double get spilledTime => _spilledTime;
double get totalTime => _totalTime;
double get lastTotalTime => _lastTotalTime;
/// Overrides
Loop loopOverride;
int startOverride = -1;
int endOverride = -1;
/// Fires when a looping animation loops
VoidCallback onLoop;
AnimInstance(this.animation)
: _time =
(animation.enableWorkArea ? animation.workStart : 0).toDouble() /
animation.fps;
/// Note that when time is set, the direction will be changed to 1
set time(double value) {
if (_time == value) {
return;
}
// Make sure to keep last and total in relative lockstep so state machines
// can track change even when setting time.
var diff = _totalTime - _lastTotalTime;
_time = _totalTime = value;
_lastTotalTime = _totalTime - diff;
_direction = 1;
}
/// Returns the current time position of the animation in seconds
double get time => _time;
/// Direction should only be +1 or -1
set direction(int value) => _direction = value == -1 ? -1 : 1;
/// Returns the animation's play direction: 1 for forwards, -1 for backwards
int get direction => _direction;
/// Returns the end time of the animation in seconds
double get endTime {
if (endOverride > -1)
return endOverride / animation.fps;
return (animation.enableWorkArea ? animation.workEnd : animation.duration).toDouble() / animation.fps;
}
/// Returns the start time of the animation in seconds
double get startTime {
if (startOverride > -1)
return startOverride / animation.fps;
return (animation.enableWorkArea ? animation.workStart : 0).toDouble() / animation.fps;
}
/// Set start and end time overrides
void setStartOverride(double seconds) {
startOverride = (seconds * animation.fps).toInt();
}
void setEndOverride(double seconds) {
endOverride = (seconds * animation.fps).toInt();
}
double get progress => (_time - startTime) / (endTime - startTime);
/// Resets the animation to the starting frame
void reset() {
_time = startTime;
//_didLoop = false;
}
/// Whether the controller driving this animation should keep requesting
/// frames be drawn.
bool get keepGoing => animation.loop != Loop.oneShot || !_didLoop;
bool advance(double elapsedSeconds) {
var deltaSeconds = elapsedSeconds * animation.speed * _direction;
_lastTotalTime = _totalTime;
_totalTime += deltaSeconds;
_time += deltaSeconds;
double frames = _time * animation.fps;
var start = animation.enableWorkArea ? animation.workStart : 0;
var end = animation.enableWorkArea ? animation.workEnd : animation.duration;
Loop loop = animation.loop;
/// Check overrides
if (startOverride > -1)
start = startOverride;
if (endOverride > -1)
end = endOverride;
if (loopOverride != null)
loop = loopOverride;
var range = end - start;
bool keepGoing = true;
_didLoop = false;
_spilledTime = 0;
switch (loop) {
case Loop.oneShot:
if (frames > end) {
keepGoing = false;
_spilledTime = (frames - end) / animation.fps;
frames = end.toDouble();
_time = frames / animation.fps;
_didLoop = true;
onLoop?.call();
}
break;
case Loop.loop:
if (frames >= end) {
_spilledTime = (frames - end) / animation.fps;
frames = _time * animation.fps;
frames = start + (frames - start) % range;
_time = frames / animation.fps;
_didLoop = true;
onLoop?.call();
}
break;
case Loop.pingPong:
// ignore: literal_only_boolean_expressions
while (true) {
if (_direction == 1 && frames >= end) {
_spilledTime = (frames - end) / animation.fps;
_direction = -1;
frames = end + (end - frames);
_time = frames / animation.fps;
_didLoop = true;
onLoop?.call();
} else if (_direction == -1 && frames < start) {
_spilledTime = (start - frames) / animation.fps;
_direction = 1;
frames = start + (start - frames);
_time = frames / animation.fps;
_didLoop = true;
onLoop?.call();
} else {
// we're within the range, we can stop fixing. We do this in a
// loop to fix conditions when time has advanced so far that we've
// ping-ponged back and forth a few times in a single frame. We
// want to accomodate for this in cases where animations are not
// advanced on regular intervals.
break;
}
}
break;
}
return keepGoing;
}
}
Thanks for the thoughtful feedback. The app looks great!
Instancing
Artboard instancing is available, there's a new method Artboard.instance: https://github.com/rive-app/rive-flutter/blob/61e83f31848aeea7abdc484a095b3fbe746f5b55/lib/src/runtime_artboard.dart#L122
This will clone the artboard efficiently keeping animations, state machines, etc shared.
Three part animation
This is exactly what our StateMachines are built for! Yes, you would currently need to split the animation into three parts. We could add a feature that gives you the ability to segment/splice the start/end of the animation in the StateMachine @neurowave. So you could re-use the same animation for different states, but effectively have a different work area (start/end) per state. @JcToon would this be valuable? @alxgibsn there might be a cool way to do show this custom start/end in the inspector for the state or potentially add multiple work areas to each animation when viewing their timeline (state inspector could let you choose which work area to use). But maybe this is all overkill?
Changing Colors
We want to provide an easier API here, but there are a few different considerations we need to make with regards to Gradients and Solid Colors and how this impacts our other APIs. Right now what you're doing is fine, even though tedious.
You should also be able to do SolidColorBase.typeKey to not hardcode "18" for checking the concrete type: https://github.com/rive-app/rive-flutter/blob/61e83f31848aeea7abdc484a095b3fbe746f5b55/lib/src/generated/shapes/paint/solid_color_base.dart#L9
You can also just cast to SolidColor (all SolidColorBase types will be SolidColor) and then do solidColor.color = color;. Layer alpha comes from the parent alpha values, changing the color shouldn't affect those. I'm not sure where things are going wrong there. Any chance you can show what you're doing in a small isolated code example?
Metronome Controller
Interpolating the time value is cool (animate to position/time value), but you're the first to request it so I'm not sure if many others would find value in it if it were a core part of Rive. It's definitely easy to write into a custom controller which we could provide an example/gist of. Another way I'd consider doing a metronome is by simply syncing the time value of the animation each frame to the detected beat/time in measure/beat of the song. As long as your beat/time calculator is frame coherent, it should look smooth (you could always apply a tiny bit of interpolation if necessary). Or sync it to a midi clock if you have that available, that'd be the easiest! I would do this all in a custom controller.
Thanks for your reply Luigi.
Artboard.instance() works great! Thanks
Yes, I would have loved to use a state machine for the play button, but couldn't without splitting it. Having a slicer (custom in/out) as a part of each animation state would be great.
I'll put something together for you to show the fill problem. I have 5 objects in the Rive file: play button, each pause line, rotating drop, and a droplet. Object layer alpha is different for each animation.
I tried finding fill layers by name (Fill 1) before realizing that the name is null unless explicitly set. Then I tried:
if (c.coreType == 20) { // FillBase
Fill fill = c;
fill.paint.color = color;
}
This turns on all the layers all the time. Perhaps FillBase is too broad?
Syncing the animation frames to the midi clock was going to be my next attempt, in which case I could do away with animation and just set the rotation of the component, but I'm trying to minimize per frame actions until I can move the timing/sequencer to native code. Currently I only need to synchronize once per tick.
Oh, another reason I think speed should be a default variable:
When I created the play/pause transitions I wasn't sure how fast I wanted them. I needed to see them in context, in the app. So I created them at 30f/0.5sec and then tried various speeds in the app until I was happy - quick and easy.
Without speed control I would have had to go back and forth between the Rive editor, export, import, test which would have been much more difficult and time consuming.
Going to close this owing to the age of the Github issue. A lot more features and API have been introduced in both the editor and runtime.
Please feel free to reopen, or create a separate issue