You can manage advancing and drawing an artboard yourself by using a CustomPainter. This will give you more control at a painting level, allowing you to:
Draw multiple Rive artboards to the same Flutter Canvas.
Advance an artboard manually and control the elapsed time.
Reuse the same artboard instance and redraw it multiple times.
More complex clipping, transformation, or other painting/rendering can be applied to the canvas.
The Flame game engine makes use of the techniques discussed below to render Rive animations. Some of the code in this example is taken from the flame_rive package.
Note that this is a low-level API, and under most circumstances, it is preferable to make use of theRiveAnimation or Rive widgets instead.
Example Code
The following is a complete example demonstrating how to manually advance a single artboard and draw it multiple times in a grid to the same Flutter canvas.
import'dart:math';import'package:flutter/material.dart';import'package:rive/math.dart';import'package:rive/rive.dart';voidmain() {runApp(constMyApp());}classMyAppextendsStatelessWidget {constMyApp({super.key});@overrideWidgetbuild(BuildContext context) {returnconstMaterialApp( debugShowCheckedModeBanner:false, home:MyRiveWidget(), ); }}classMyRiveWidgetextendsStatefulWidget {constMyRiveWidget({Key? key}) : super(key: key);@overrideState<MyRiveWidget> createState() =>_MyRiveWidgetState();}class_MyRiveWidgetStateextendsState<MyRiveWidget>withSingleTickerProviderStateMixin {latefinalAnimationController _animationController =AnimationController(vsync: this, duration:constDuration(seconds:10));RiveArtboardRenderer? _artboardRenderer;Future<void> _load() async {// You need to manage adding the controller to the artboard yourself,// unlike with the RiveAnimation widget that handles a lot of this logic// for you by simply providing the state machine (or animation) name.final file =awaitRiveFile.asset('assets/little_machine.riv');final artboard = file.mainArtboard.instance();final controller =StateMachineController.fromArtboard( artboard,'State Machine 1', ); artboard.addController(controller!);setState( () => _artboardRenderer =RiveArtboardRenderer( antialiasing:true, fit:BoxFit.cover, alignment:Alignment.center, artboard: artboard, ), ); }@overridevoidinitState() { super.initState(); _animationController.repeat();_load(); }@overridevoiddispose() { _animationController.dispose(); super.dispose(); }@overrideWidgetbuild(BuildContext context) {returnScaffold( body:Center( child: _artboardRenderer ==null?constSizedBox():CustomPaint( painter:RiveCustomPainter( _artboardRenderer!, repaint: _animationController, ), child:constSizedBox.expand(), // use all the size available ), ), ); }}classRiveCustomPainterextendsCustomPainter {finalRiveArtboardRenderer artboardRenderer;RiveCustomPainter(this.artboardRenderer, {super.repaint}) { _lastTickTime =DateTime.now(); _elapsedTime =Duration.zero; }lateDateTime _lastTickTime;lateDuration _elapsedTime;void_calculateElapsedTime() {final currentTime =DateTime.now(); _elapsedTime = currentTime.difference(_lastTickTime); _lastTickTime = currentTime; }@overridevoidpaint(Canvas canvas, Size size) {_calculateElapsedTime(); // Calculate elapsed time since last tick.// Advance the artboard by the elapsed time. artboardRenderer.advance(_elapsedTime.inMicroseconds /1000000);final width = size.width /3;final height = size.height /2;final artboardSize =Size(width, height);// First row canvas.save(); artboardRenderer.render(canvas, artboardSize); canvas.restore(); canvas.save(); canvas.translate(width, 0); artboardRenderer.render(canvas, artboardSize); canvas.restore(); canvas.save(); canvas.translate(width *2, 0); artboardRenderer.render(canvas, artboardSize); canvas.restore();// Second row canvas.save(); canvas.translate(0, height); artboardRenderer.render(canvas, artboardSize); canvas.restore(); canvas.save(); canvas.translate(width, height); artboardRenderer.render(canvas, artboardSize); canvas.restore(); canvas.save(); canvas.translate(width *2, height); artboardRenderer.render(canvas, artboardSize); canvas.restore();// Paint the full canvas size// artboardRenderer.render(canvas, size); }@overrideboolshouldRepaint(covariantCustomPainter oldDelegate) {returntrue; }}/// Keeps an `Artboard` instance and renders it to a `Canvas`.////// This is a simplified version of the `RiveAnimation` widget and its/// RenderObject////// This accounts for the `fit` and `alignment` properties, similar to how/// `RiveAnimation` works.classRiveArtboardRenderer {finalArtboard artboard;finalbool antialiasing;finalBoxFit fit;finalAlignment alignment;RiveArtboardRenderer({required this.antialiasing,required this.fit,required this.alignment,required this.artboard, }) { artboard.antialiasing = antialiasing; }voidadvance(double dt) { artboard.advance(dt, nested:true); }latefinal aabb =AABB.fromValues(0, 0, artboard.width, artboard.height);voidrender(Canvas canvas, Size size) {_paint(canvas, aabb, size); }final _transform =Mat2D();final _center =Mat2D();void_paint(Canvas canvas, AABB bounds, Size size) {const position =Offset.zero;final contentWidth = bounds[2] - bounds[0];final contentHeight = bounds[3] - bounds[1];if (contentWidth ==0|| contentHeight ==0) {return; }final x =-1* bounds[0] - contentWidth /2.0- (alignment.x * contentWidth /2.0);final y =-1* bounds[1] - contentHeight /2.0- (alignment.y * contentHeight /2.0);var scaleX =1.0;var scaleY =1.0; canvas.save(); canvas.clipRect(position & size);switch (fit) {caseBoxFit.fill: scaleX = size.width / contentWidth; scaleY = size.height / contentHeight;break;caseBoxFit.contain:final minScale =min(size.width / contentWidth, size.height / contentHeight); scaleX = scaleY = minScale;break;caseBoxFit.cover:final maxScale =max(size.width / contentWidth, size.height / contentHeight); scaleX = scaleY = maxScale;break;caseBoxFit.fitHeight:final minScale = size.height / contentHeight; scaleX = scaleY = minScale;break;caseBoxFit.fitWidth:final minScale = size.width / contentWidth; scaleX = scaleY = minScale;break;caseBoxFit.none: scaleX = scaleY =1.0;break;caseBoxFit.scaleDown:final minScale =min(size.width / contentWidth, size.height / contentHeight); scaleX = scaleY = minScale <1.0? minScale :1.0;break; }Mat2D.setIdentity(_transform); _transform[4] = size.width /2.0+ (alignment.x * size.width /2.0); _transform[5] = size.height /2.0+ (alignment.y * size.height /2.0);Mat2D.scale(_transform, _transform, Vec2D.fromValues(scaleX, scaleY));Mat2D.setIdentity(_center); _center[4] = x; _center[5] = y;Mat2D.multiply(_transform, _transform, _center); canvas.translate( size.width /2.0+ (alignment.x * size.width /2.0), size.height /2.0+ (alignment.y * size.height /2.0), ); canvas.scale(scaleX, scaleY); canvas.translate(x, y); artboard.draw(canvas); canvas.restore(); }}
The RiveArtboardRenderer class is taken from the Flame Rive package, and can be used as a starting point to understand how Rive uses Alignment and BoxFit to layout an artboard to a canvas.
The important steps are:
Access an artboard from a RiveFile and attach any Rive animation controller (StateMachineController). The animation can be controlled as it normally would with the controller.
Create a Flutter CustomPaint widget to get access to a Flutter canvas.
Make use of an AnimationController (or Ticker/Listener) to force the RiveCustomPainter to repaint each frame.
Calculate the elapsed time between animation ticks.
Advance the artboard with artboard.advance(dt, nested: true); where dt is the elapsed time (delta time).
Draw the artboard to the canvas with artboard.draw(canvas);
The rest of the code is responsible for layout and sizing.
Other Examples
In this example a single artboard is used, however, it's possible to draw multiple artboard instances (from the same or different Rive file) to the same canvas.
See this editable example that showcases how to draw two different artboards (a unique artboard instance is created for each zombie) to the same canvas. Each artboard has a number input to switch between different skins:
Note that .instance() is called on the artboard to create a unique instance that can be advanced on it's own.
For additional information/inspiration take a look at the Rive GameKit for Flutter documentation. Similar techniques can be used to create more complex scenes and layouts using the Rive Flutter runtime.