feat(flutter): add shared texture through rive panel (#10451) 7359e8b824

* feat(flutter): add shared texture through rive panel

* feat: updates to rive panel

* chore: revert pub path change

* docs: update api level docs

* feat: expose drawOrder

feat: Add fallback AtlasTypes that don't need float color buffers (#10475) 5e6f683b9e
Floating point color buffers are only supported via extensions in GL.
Previously, the feather atlas would just break when this functionality
wasn't present.

This PR adds support for multiple different AtlasTypes that make use of
various GL extensions to render the atlas. As a final resort, if none of
the other extensions are available, it can split coverage up into rgba8
compoments. This mode works on unextended GL at the cost of quality.

Co-authored-by: Gordon <pggordonhayes@gmail.com>
This commit is contained in:
HayesGordon
2025-09-03 17:20:33 +00:00
parent 38f40c8309
commit ce00421de5
22 changed files with 1169 additions and 3 deletions

View File

@ -1 +1 @@
f7f795e53015b92c38abe175259caa7b5032cb76
7359e8b824801f268a0506ae948ab5845f610d8c

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
export 'centaur_example/game_widget.dart';

View File

@ -0,0 +1,368 @@
import 'dart:math';
import 'package:flutter/rendering.dart';
import 'package:rive/rive.dart' as rive;
class Arrow {
static const double appleRadius = 50;
static const appleRadiusSquared = appleRadius * appleRadius;
final rive.Artboard artboard;
final rive.Vec2D heading;
rive.Vec2D translation;
double time = 0;
Arrow({
required this.artboard,
required this.translation,
required this.heading,
});
void dispose() => artboard.dispose();
bool get draws => time >= 0.1;
bool get isDead => time > 2;
void advance(double elapsedSeconds, Set<Apple> apples) {
time += elapsedSeconds;
if (!draws) {
// Arrow is still leaving the bow (fire animation is playing on
// centaur).
return;
} else if (isDead) {
return;
}
// You'd likely use a scene tree to ray cast against here, but this is a
// simple example.
for (var apple in apples) {
// const { explodeTrigger, x, y } = appleInstance;
if ((apple.translation - translation).squaredLength() <
appleRadiusSquared) {
apple.damage();
}
}
translation += heading * elapsedSeconds * 3000;
heading.y += elapsedSeconds;
// Normalize heading.
heading.norm();
}
void draw(rive.Renderer renderer) {
if (!draws) {
return;
}
renderer.save();
var arrowTransform = rive.Mat2D.fromTranslation(
translation,
).mul(rive.Mat2D.fromRotation(rive.Mat2D(), atan2(heading.y, heading.x)));
renderer.transform(arrowTransform);
artboard.draw(renderer);
renderer.restore();
}
}
class Apple {
final rive.Artboard artboard;
final rive.StateMachine machine;
final rive.TriggerInput explode;
final rive.AABB bounds;
rive.Vec2D translation;
bool _isDead = false;
bool get isDead => _deadTime > 1;
double _deadTime = 0;
Apple({
required this.artboard,
required this.machine,
required this.explode,
required this.translation,
}) : bounds = artboard.bounds {
var center = bounds.center();
artboard.renderTransform = rive.Mat2D.fromTranslation(translation - center);
}
void damage() {
if (_isDead) {
return;
}
explode.fire();
_isDead = true;
}
void advance(double elapsedSeconds) {
// We don't advance the state machine here as we do all the apples in a
// single batch call.
if (_isDead) {
_deadTime += elapsedSeconds;
}
}
void draw(rive.Renderer renderer) {
renderer.save();
var center = bounds.center();
renderer.translate(translation.x - center.x, translation.y - center.y);
artboard.draw(renderer);
renderer.restore();
}
void dispose() {
artboard.dispose();
machine.dispose();
}
}
class GhostApple {
final Apple apple;
rive.Vec2D translation;
GhostApple(this.apple, this.translation);
void draw(rive.Renderer renderer) {
renderer.save();
var center = apple.bounds.center();
renderer.translate(translation.x - center.x, translation.y - center.y);
apple.artboard.draw(renderer);
renderer.restore();
}
}
base class CentaurGame extends rive.RenderTexturePainter {
final rive.File riveFile;
final rive.Artboard character;
late rive.StateMachine characterMachine;
final rive.Artboard backgroundTile;
final Set<Arrow> _arrows = {};
final Set<Apple> _apples = {};
late rive.Component target;
rive.Component? _characterRoot;
rive.Component? _arrowLocation;
rive.NumberInput? _moveInput;
rive.TriggerInput? _fireInput;
double _characterX = 0;
double _characterDirection = 1;
double move = 0;
double _currentMoveSpeed = 0;
CentaurGame(this.riveFile)
: character = riveFile.artboard('Character')!,
backgroundTile = riveFile.artboard('Background_tile')! {
characterMachine = character.defaultStateMachine()!;
character.frameOrigin = false;
backgroundTile.frameOrigin = false;
target = character.component('Look')!;
_characterRoot = character.component('Character');
_arrowLocation = character.component('ArrowLocation');
_moveInput = characterMachine.number('Move');
_fireInput = characterMachine.trigger('Fire');
}
@override
void dispose() {
character.dispose();
backgroundTile.dispose();
riveFile.dispose();
super.dispose();
}
rive.Vec2D localCursor = rive.Vec2D();
void aimAt(Offset localPosition) {
localCursor = rive.Vec2D.fromOffset(localPosition);
}
void pointerDown(PointerDownEvent event) {
_fireInput?.fire();
var transform = _arrowLocation?.worldTransform ?? rive.Mat2D();
var artboard = riveFile.artboard('Arrow');
if (artboard == null) {
return;
}
artboard.frameOrigin = false;
var arrowInstance = Arrow(
artboard: artboard,
translation:
transform.translation + rive.Vec2D.fromValues(_characterX, 0.0),
heading: transform.xDirection,
);
_arrows.add(arrowInstance);
}
static const int minApples = 30;
static const int maxApples = 100;
final Stopwatch _appleCooloff = Stopwatch()..start();
void spawnApples(int maxSpawnIterations) {
if (_appleCooloff.elapsedMilliseconds < 10) {
return;
}
_appleCooloff.reset();
_appleCooloff.start();
var rand = Random();
var count = rand.nextInt(maxApples - minApples) + minApples;
var bounds = spawnAppleBounds;
var range = bounds.maximum - bounds.minimum;
int spawnCount = 0;
while (_apples.length < count) {
var artboard = riveFile.artboard('Apple');
if (artboard == null) {
return;
}
var stateMachine = artboard.defaultStateMachine();
if (stateMachine == null) {
artboard.dispose();
return;
}
var explode = stateMachine.trigger('Explode');
if (explode == null) {
artboard.dispose();
stateMachine.dispose();
return;
}
var apple = Apple(
artboard: artboard,
machine: stateMachine,
explode: explode,
translation: rive.Vec2D.fromValues(
bounds.minimum.x + range.x * rand.nextDouble(),
bounds.minimum.y + range.y * rand.nextDouble(),
),
// translation: bounds.minimum,
);
_apples.add(apple);
spawnCount++;
if (spawnCount > maxSpawnIterations) {
return;
}
}
}
rive.AABB get sceneBounds {
final bounds = character.bounds;
final characterWidth = bounds.width;
return bounds.inset(-characterWidth * 5, 0);
}
rive.AABB get spawnAppleBounds {
return rive.AABB.fromMinMax(
sceneBounds.minimum - rive.Vec2D.fromValues(0, 3000),
sceneBounds.maximum - rive.Vec2D.fromValues(0, 600),
);
}
@override
bool paint(
rive.RenderTexture texture,
double devicePixelRatio,
Size size,
double elapsedSeconds,
) {
var renderer = texture.renderer;
var viewTransform = rive.Renderer.computeAlignment(
rive.Fit.contain,
Alignment.bottomCenter,
rive.AABB.fromValues(0, 0, size.width, size.height),
sceneBounds,
devicePixelRatio,
);
// Compute cursor in world space.
final inverseViewTransform = rive.Mat2D();
var worldCursor = rive.Vec2D();
if (rive.Mat2D.invert(inverseViewTransform, viewTransform)) {
worldCursor = inverseViewTransform * localCursor;
// Check if we should invert the character's direction by comparing
// the world location of the cursor to the world location of the
// character (need to compensate by character movement, characterX).
_characterDirection = _characterX < worldCursor.x ? 1 : -1;
_characterRoot?.scaleX = _characterDirection;
}
target.worldTransform = rive.Mat2D.fromTranslation(
worldCursor - rive.Vec2D.fromValues(_characterX, 0),
);
const moveSpeed = 100;
var targetMoveSpeed = move * moveSpeed;
_moveInput?.value = move * _characterDirection.sign;
_currentMoveSpeed +=
(targetMoveSpeed - _currentMoveSpeed) * min(1, elapsedSeconds * 10);
_characterX += elapsedSeconds * _currentMoveSpeed;
characterMachine.advanceAndApply(elapsedSeconds);
renderer.save();
renderer.transform(viewTransform);
double backgroundScale = 3;
for (int b = -2; b <= 2; b++) {
renderer.save();
var xform = rive.Mat2D.fromScale(backgroundScale, backgroundScale);
xform[4] = backgroundScale * b * backgroundTile.bounds.width;
renderer.transform(xform);
backgroundTile.draw(renderer);
renderer.restore();
}
renderer.save();
renderer.translate(_characterX, 0);
character.draw(renderer);
renderer.restore();
var deadArrows = <Arrow>{};
for (final arrow in _arrows) {
if (arrow.isDead) {
deadArrows.add(arrow);
arrow.dispose();
continue;
}
arrow.draw(renderer);
arrow.advance(elapsedSeconds, _apples);
}
_arrows.removeAll(deadArrows);
bool batchRender = true;
// Advance apple state machines in one multi-threaded batch.
if (batchRender) {
rive.Rive.batchAdvanceAndRender(
_apples.map((apple) => apple.machine),
elapsedSeconds,
renderer,
);
// ignore: dead_code
} else {
rive.Rive.batchAdvance(
_apples.map((apple) => apple.machine),
elapsedSeconds,
);
}
var deadApples = <Apple>{};
for (final apple in _apples) {
if (apple.isDead) {
deadApples.add(apple);
apple.dispose();
continue;
}
apple.advance(elapsedSeconds);
// ignore: dead_code
if (!batchRender) {
apple.draw(renderer);
}
}
_apples.removeAll(deadApples);
spawnApples(5);
renderer.restore();
return true;
}
@override
Color get background => const Color(0xFF6F8C9B);
}

View File

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rive/rive.dart' as rive;
import 'package:rive_example/advanced/centaur_example/centaur_game.dart';
class CentaurGameWidget extends StatefulWidget {
const CentaurGameWidget({super.key});
@override
State<CentaurGameWidget> createState() => _CentaurGameWidgetState();
}
class _CentaurGameWidgetState extends State<CentaurGameWidget> {
final rive.RenderTexture _renderTexture =
rive.RiveNative.instance.makeRenderTexture();
CentaurGame? _centaurPainter;
@override
void initState() {
super.initState();
load();
}
Future<void> load() async {
var data = await rootBundle.load('assets/centaur_v2.riv');
var bytes = data.buffer.asUint8List();
var file = await rive.File.decode(bytes, riveFactory: rive.Factory.rive);
if (file != null) {
setState(() {
_centaurPainter = CentaurGame(file);
});
}
}
@override
void dispose() {
super.dispose();
_centaurPainter?.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Centaur Game')),
body: ColoredBox(
color: const Color(0xFF507FBA),
child: Center(
child: _centaurPainter == null
? const SizedBox()
: Focus(
focusNode: FocusNode(
canRequestFocus: true,
onKeyEvent: (node, event) {
if (event is KeyRepeatEvent) {
return KeyEventResult.handled;
}
double speed = 0;
if (event is KeyDownEvent) {
speed = 1;
} else if (event is KeyUpEvent) {
speed = -1;
}
if (event.logicalKey == LogicalKeyboardKey.keyA) {
_centaurPainter!.move -= speed;
} else if (event.logicalKey ==
LogicalKeyboardKey.keyD) {
_centaurPainter!.move += speed;
}
return KeyEventResult.handled;
},
)..requestFocus(),
child: MouseRegion(
onHover: (event) => _centaurPainter!.aimAt(
event.localPosition * View.of(context).devicePixelRatio,
),
child: Listener(
behavior: HitTestBehavior.opaque,
onPointerDown: _centaurPainter!.pointerDown,
onPointerMove: (event) => _centaurPainter!.aimAt(
event.localPosition *
View.of(context).devicePixelRatio,
),
child: _renderTexture.widget(
painter: _centaurPainter!,
),
),
),
),
),
),
),
);
}
}

View File

@ -14,6 +14,7 @@ export 'responsive_layouts.dart';
export 'rive_audio.dart';
export 'rive_widget.dart';
export 'rive_widget_builder.dart';
export 'rive_panel.dart';
export 'state_machine_painter.dart';
export 'text_runs.dart';
export 'ticker_mode.dart';

View File

@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
class ExampleRivePanel extends StatelessWidget {
const ExampleRivePanel({super.key});
@override
Widget build(BuildContext context) {
return const RivePanel(
child: ListViewExample(),
);
}
}
class RowExample extends StatefulWidget {
const RowExample({super.key});
@override
State<RowExample> createState() => _RowExampleState();
}
class _RowExampleState extends State<RowExample> {
// Only useful when using `Factory.rive`. `Factory.flutter` draws to a single
// render target already.
final factory = Factory.rive;
late List<FileLoader> listOfFileLoaders = [
FileLoader.fromAsset(
'assets/rating.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/vehicles.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/perf/rivs/travel_icons.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/coyote.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/perf/rivs/Tom_Morello_2.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/perf/rivs/towersDemo.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/perf/rivs/walking.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/perf/rivs/skull_404.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/perf/rivs/adventuretime_marceline_pb.riv',
riveFactory: factory,
),
FileLoader.fromAsset(
'assets/perf/rivs/Zombie_Character.riv',
riveFactory: factory,
),
];
@override
void dispose() {
for (var fileLoader in listOfFileLoaders) {
fileLoader.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 50.0),
child: SingleChildScrollView(
child: Wrap(
children: [
...listOfFileLoaders
.map((fileLoader) => SizedSample(fileLoader: fileLoader)),
],
),
),
);
}
}
class SizedSample extends StatelessWidget {
const SizedSample({super.key, required this.fileLoader});
final FileLoader fileLoader;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 200,
height: 200,
child: MyRiveWidget(fileLoader: fileLoader),
);
}
}
class ListViewExample extends StatefulWidget {
const ListViewExample({super.key});
@override
State<ListViewExample> createState() => _ListViewExampleState();
}
class _ListViewExampleState extends State<ListViewExample> {
late final fileLoader = FileLoader.fromAsset(
'assets/rating.riv',
riveFactory: Factory.rive,
);
@override
void dispose() {
fileLoader.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return SizedBox(
width: 500,
height: 100,
child: MyRiveWidget(fileLoader: fileLoader),
);
},
);
}
}
class MyRiveWidget extends StatelessWidget {
const MyRiveWidget({super.key, required this.fileLoader});
final FileLoader fileLoader;
@override
Widget build(BuildContext context) {
return RiveWidgetBuilder(
fileLoader: fileLoader,
builder: (context, state) => switch (state) {
RiveLoading() => const Center(
child: Center(child: CircularProgressIndicator()),
),
RiveFailed() => ErrorWidget.withDetails(
message: state.error.toString(),
error: FlutterError(state.error.toString()),
),
RiveLoaded() => RiveWidget(
controller: state.controller,
fit: Fit.contain,
/// Set this to true to draw to the nearest `RivePanel`
useSharedTexture: true,
)
},
);
}
}

View File

@ -1,6 +1,7 @@
// ignore_for_file: deprecated_member_use
import 'package:flutter/material.dart';
import 'package:rive_example/advanced/advanced.dart';
import 'package:rive_example/colors.dart';
import 'package:rive_example/examples/examples.dart';
import 'package:rive/rive.dart' as rive;
@ -67,6 +68,8 @@ class _RiveExampleAppState extends State<RiveExampleApp> {
'Simple example usage of the Rive widget with common parameters.'),
_Page('Rive Widget Builder', ExampleRiveWidgetBuilder(),
'Example usage of the Rive builder widget with common parameters.'),
_Page('Rive Panel [Shared Texture]', ExampleRivePanel(),
'Example usage of the Shared Texture View widget.'),
],
),
const _Section(
@ -107,6 +110,7 @@ class _RiveExampleAppState extends State<RiveExampleApp> {
'Advanced: Custom painter for state machines.'),
_Page('Single Animation Painter', ExampleSingleAnimationPainter(),
'Advanced: Custom painter for single animation playback.'),
_Page('Centaur Game', CentaurGameWidget(), 'Advanced: Centaur Game.'),
],
),
const _Section(

View File

@ -32,5 +32,6 @@ flutter:
assets:
- assets/
- assets/fonts/
- assets/perf/rivs/
- assets/audio/
- assets/images/

View File

@ -22,5 +22,8 @@ export 'src/models/data_bind.dart';
export 'src/models/state_machine_selector.dart';
export 'src/painters/widget_controller.dart';
export 'src/rive_extensions.dart';
export 'src/widgets/inherited_widgets.dart';
export 'src/widgets/rive_builder.dart';
export 'src/widgets/rive_panel.dart';
export 'src/widgets/rive_widget.dart';
export 'src/widgets/shared_texture_view.dart';

View File

@ -0,0 +1,96 @@
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:rive_native/rive_native.dart' as rive;
import 'package:meta/meta.dart';
/// **EXPERIMENTAL**: This API may change or be removed in a future release.
@experimental
abstract class SharedTexturePainter {
int get sharedDrawOrder;
void paintIntoSharedTexture(rive.RenderTexture texture);
}
/// **EXPERIMENTAL**: This API may change or be removed in a future release.
@experimental
class SharedRenderTexture {
final rive.RenderTexture texture;
final double devicePixelRatio;
final Color backgroundColor;
final List<SharedTexturePainter> painters = [];
final GlobalKey panelKey;
SharedRenderTexture({
required this.texture,
required this.devicePixelRatio,
required this.backgroundColor,
required this.panelKey,
});
/// Paint the shared render texture.
void _paintShared(_) {
texture.clear(backgroundColor);
for (final painter in painters) {
painter.paintIntoSharedTexture(texture);
}
texture.flush(devicePixelRatio);
_scheduled = false;
}
bool _scheduled = false;
/// Schedule a paint of the shared render texture.
void schedulePaint() {
if (_scheduled) {
return;
}
_scheduled = true;
SchedulerBinding.instance.addPostFrameCallback(_paintShared);
}
/// Add a painter to the shared render texture.
void addPainter(SharedTexturePainter painter) {
painters.add(painter);
painters.sort((a, b) => a.sharedDrawOrder.compareTo(b.sharedDrawOrder));
}
/// Remove a painter from the shared render texture.
void removePainter(SharedTexturePainter painter) {
painters.remove(painter);
}
}
/// Inherited widget that will pass the background render texture down the tree
///
/// **EXPERIMENTAL**: This API may change or be removed in a future release.
@experimental
class RiveSharedTexture extends InheritedWidget {
late final SharedRenderTexture? texture;
RiveSharedTexture({
required super.child,
required rive.RenderTexture? texture,
required double devicePixelRatio,
required Color backgroundColor,
required GlobalKey panelKey,
super.key,
}) {
this.texture = texture != null
? SharedRenderTexture(
texture: texture,
devicePixelRatio: devicePixelRatio,
backgroundColor: backgroundColor,
panelKey: panelKey,
)
: null;
}
static SharedRenderTexture? of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<RiveSharedTexture>()?.texture;
static SharedRenderTexture? find(BuildContext context) =>
context.findAncestorWidgetOfExactType<RiveSharedTexture>()?.texture;
@override
bool updateShouldNotify(RiveSharedTexture old) => texture != old.texture;
}

View File

@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:meta/meta.dart';
/// A widget that creates a shared texture to paint multiple [RiveWidget]s to.
///
/// Useful when using [Factory.rive]. This won't have an effect when using
/// [Factory.flutter], and will unnecessarily create a new render texture that
/// won't be used.
///
/// Painting multiple [RiveWidget]s to the same texture can drastically
/// improve performance under certain conditions. Drawing multiple [RiveWidget]s
/// each to their own texture has a performance cost. The more textures you draw
/// to, the more performance you lose. Additionally, on the web, you are
/// limited to the number of WebGL contexts the browser allows. Drawing to a
/// single texture avoids this limitation.
///
/// Wrap your [RiveWidget]s with a [RivePanel] to enable this behavior, and
/// set `useSharedTexture` to `true` in [RiveWidget]
///
/// Note:
/// - There is a memory cost in allocating a larger texture. However, under some
/// conditions, the memory cost might be better, or the same. Benchmarking
/// is recommended.
/// - Drawing to the same surface will mean that you cannot interleave drawing
/// commands that Rive performs with that of Flutter. If you need to interleave
/// content, you will need to draw to a separate surface - [RivePanel]. Or use
/// [Factory.flutter] - which uses the Flutter rendering pipeline to perform
/// all rendering as a single pass. Benchmarking is recommended - what works
/// for one use case may not work for another.
///
/// ### Example:
/// ```dart
/// class ExampleRivePanel extends StatelessWidget {
/// const ExampleRivePanel({super.key});
///
/// @override
/// Widget build(BuildContext context) {
/// return const RivePanel(
/// backgroundColor: Colors.red,
/// child: ListViewExample(),
/// );
/// }
///}
///class ListViewExample extends StatefulWidget {
/// const ListViewExample({super.key});
///
/// @override
/// State<ListViewExample> createState() => _ListViewExampleState();
///}
///
///class _ListViewExampleState extends State<ListViewExample> {
/// late final fileLoader = FileLoader.fromAsset(
/// 'assets/rating.riv',
/// riveFactory: Factory.rive,
/// );
///
/// @override
/// void dispose() {
/// fileLoader.dispose();
/// super.dispose();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return ListView.builder(
/// itemCount: 10,
/// itemBuilder: (context, index) {
/// return MyRiveWidget(fileLoader: fileLoader);
/// },
/// );
/// }
///}
///class MyRiveWidget extends StatelessWidget {
/// const MyRiveWidget({super.key, required this.fileLoader});
/// final FileLoader fileLoader;
///
/// @override
/// Widget build(BuildContext context) {
/// return RiveWidgetBuilder(
/// fileLoader: fileLoader,
/// builder: (context, state) => switch (state) {
/// RiveLoading() => const Center(
/// child: Center(child: CircularProgressIndicator()),
/// ),
/// RiveFailed() => ErrorWidget.withDetails(
/// message: state.error.toString(),
/// error: FlutterError(state.error.toString()),
/// ),
/// RiveLoaded() => RiveWidget(
/// controller: state.controller,
/// fit: Fit.contain,
///
/// /// Set this to true to draw to the nearest `RivePanel`
/// useSharedTexture: true,
/// )
/// },
/// );
/// }
///}
/// ```
///
/// **EXPERIMENTAL**: This API may change or be removed in a future release.
@experimental
class RivePanel extends StatefulWidget {
const RivePanel({
super.key,
this.backgroundColor = Colors.transparent,
required this.child,
});
final Color backgroundColor;
final Widget child;
@override
State<RivePanel> createState() => _RivePanelState();
}
class _RivePanelState extends State<RivePanel> {
RenderTexture? _renderTexture;
final GlobalKey _panelKey = GlobalKey();
@override
void initState() {
super.initState();
_renderTexture = RiveNative.instance.makeRenderTexture();
}
@override
void dispose() {
_renderTexture?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(
child: _renderTexture!.widget(key: _panelKey),
),
RiveSharedTexture(
panelKey: _panelKey,
backgroundColor: widget.backgroundColor,
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
texture: _renderTexture,
child: widget.child,
),
],
);
}
}

View File

@ -1,15 +1,19 @@
import 'package:flutter/material.dart';
import 'package:rive/rive.dart';
import 'package:meta/meta.dart';
/// A widget that displays a Rive artboard.
///
/// - The [controller] parameter is the [RiveControlelr] that controls the
/// - The [controller] parameter is the [RiveWidgetController] that controls the
/// artboard and state machine. This controller builds on top of the concept
/// of a Rive painter, but provides a more convenient API for building
/// Rive widgets.
/// - The [fit] parameter is the fit of the artboard.
/// - The [alignment] parameter is the alignment of the artboard.
/// - The [hitTestBehavior] parameter is the hit test behavior of the artboard.
/// - The [cursor] parameter is the platform/Flutter cursor when interacting with an area that has a `hitTest` of `true`.
/// - The [layoutScaleFactor] parameter is the layout scale factor of the artboard when using `Fit.layout`.
/// - The [useSharedTexture] parameter is whether to use a shared texture ([RivePanel]) to draw the artboard to.
class RiveWidget extends StatefulWidget {
const RiveWidget({
super.key,
@ -19,6 +23,8 @@ class RiveWidget extends StatefulWidget {
this.hitTestBehavior = RiveDefaults.hitTestBehaviour,
this.cursor = RiveDefaults.mouseCursor,
this.layoutScaleFactor = RiveDefaults.layoutScaleFactor,
this.useSharedTexture = false,
this.drawOrder = 1,
});
final RiveWidgetController controller;
@ -47,6 +53,21 @@ class RiveWidget extends StatefulWidget {
/// Defaults to [RiveDefaults.layoutScaleFactor].
final double layoutScaleFactor;
/// Whether to use a shared texture [(RivePanel]) to draw the artboard to.
///
/// Defaults to false. When set to true, it draws to nearest inherited widget
/// of type [RivePanel].
///
/// **EXPERIMENTAL**: This API may change or be removed in a future release.
@experimental
final bool useSharedTexture;
/// The draw order of the artboard. This is only used when [useSharedTexture]
/// is true when drawing to a [RivePanel], and using [Factory.rive].
///
/// Defaults to 1.
final int drawOrder;
@override
State<RiveWidget> createState() => _RiveWidgetState();
}
@ -102,11 +123,41 @@ class _RiveWidgetState extends State<RiveWidget> {
controller.scheduleRepaint();
}
late final SharedTextureArtboardWidgetPainter _painter =
SharedTextureArtboardWidgetPainter(widget.controller);
@override
Widget build(BuildContext context) {
if (widget.useSharedTexture) {
if (widget.controller.artboard.riveFactory == Factory.flutter) {
return errorWidget(
'useSharedTexture is only supported when using Factory.rive');
}
final sharedTexture = RiveSharedTexture.of(context);
if (sharedTexture == null) {
return errorWidget(
'RiveWidget requires a shared texture when useSharedTexture is true.\n'
'Make sure to wrap this widget with a RiveSharedTexture widget in the widget tree.');
} else {
return SharedTextureView(
artboard: widget.controller.artboard,
painter: _painter,
sharedTexture: sharedTexture,
drawOrder: widget.drawOrder,
);
}
}
return RiveArtboardWidget(
artboard: widget.controller.artboard,
painter: widget.controller,
);
}
ErrorWidget errorWidget(String message) {
return ErrorWidget.withDetails(
message: message,
error: FlutterError(message),
);
}
}

View File

@ -0,0 +1,223 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/src/widgets/inherited_widgets.dart';
import 'package:rive_native/rive_native.dart';
import 'package:meta/meta.dart';
/// Renderers the [artboard] to a [sharedTexture].
///
/// See [RivePanel]. Only useful when using `Factory.rive`.
///
/// **EXPERIMENTAL**: This API may change or be removed in a future release.
@experimental
class SharedTextureView extends StatefulWidget {
final Artboard artboard;
final SharedTextureArtboardWidgetPainter painter;
final SharedRenderTexture sharedTexture;
final int drawOrder;
const SharedTextureView({
required this.artboard,
required this.painter,
required this.sharedTexture,
required this.drawOrder,
super.key,
});
@override
State<SharedTextureView> createState() => _SharedTextureViewState();
}
class _SharedTextureViewState extends State<SharedTextureView> {
@override
void initState() {
super.initState();
widget.painter.artboardChanged(widget.artboard);
}
@override
void didUpdateWidget(covariant SharedTextureView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.artboard != widget.artboard) {
widget.painter.artboardChanged(widget.artboard);
}
}
@override
Widget build(BuildContext context) {
return SharedTextureViewRenderer(
renderTexturePainter: widget.painter,
sharedTexture: widget.sharedTexture,
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
drawOrder: widget.drawOrder,
);
}
}
class SharedTextureViewRenderer extends LeafRenderObjectWidget {
final RenderTexturePainter renderTexturePainter;
final SharedRenderTexture sharedTexture;
final double devicePixelRatio;
final int drawOrder;
const SharedTextureViewRenderer({
super.key,
required this.renderTexturePainter,
required this.sharedTexture,
required this.devicePixelRatio,
required this.drawOrder,
});
@override
RenderObject createRenderObject(BuildContext context) {
return SharedTextureViewRenderObject(sharedTexture)
..painter = renderTexturePainter
..scrollPosition = Scrollable.maybeOf(context)?.position
..devicePixelRatio = devicePixelRatio
..drawOrder = drawOrder;
}
@override
void updateRenderObject(
BuildContext context,
covariant SharedTextureViewRenderObject renderObject,
) {
renderObject
..shared = sharedTexture
..painter = renderTexturePainter
..scrollPosition = Scrollable.maybeOf(context)?.position
..devicePixelRatio = devicePixelRatio
..drawOrder = drawOrder;
}
@override
void didUnmountRenderObject(
covariant SharedTextureViewRenderObject renderObject,
) {}
}
class SharedTextureViewRenderObject
extends RiveNativeRenderBox<RenderTexturePainter>
implements SharedTexturePainter {
SharedRenderTexture _shared;
SharedTextureViewRenderObject(this._shared) {
_shared.texture.onTextureChanged = _onRiveTextureChanged;
}
int drawOrder = 1;
SharedRenderTexture get shared => _shared;
set shared(SharedRenderTexture value) {
if (_shared == value) {
return;
}
_shared.texture.onTextureChanged = null;
_shared.removePainter(this);
_shared = value;
_shared.texture.onTextureChanged = _onRiveTextureChanged;
_shared.addPainter(this);
markNeedsPaint();
}
bool _shouldAdvance = true;
@override
bool get shouldAdvance => _shouldAdvance;
// Repaint when the texture is created/changed. This reduces the flicker when
// resizing the widget. This flicker is caused by recreating the underlying
// texture Rive draws to.
void _onRiveTextureChanged() => markNeedsLayout();
@override
bool get sizedByParent => true;
@override
Size computeDryLayout(BoxConstraints constraints) => constraints.smallest;
@override
void paint(PaintingContext context, Offset offset) => _shared.schedulePaint();
ScrollPosition? _scrollPosition;
set scrollPosition(ScrollPosition? v) {
if (identical(v, _scrollPosition)) return;
_unsubscribe();
_scrollPosition = v;
_subscribe();
_scheduleCheck();
}
void _subscribe() => _scrollPosition?.addListener(_scheduleCheck);
void _unsubscribe() => _scrollPosition?.removeListener(_scheduleCheck);
void _scheduleCheck() {
if (!attached) return;
markNeedsPaint();
}
@override
void dispose() {
_shared.removePainter(this);
_shared.texture.onTextureChanged = null;
super.dispose();
}
@override
void paintIntoSharedTexture(RenderTexture texture) {
// TODO (Gordon): could move out this logic to calculate the position only under certain conditions.
final panelKeyContext = shared.panelKey.currentContext;
if (panelKeyContext == null) {
return;
}
RenderBox renderBox = panelKeyContext.findRenderObject() as RenderBox;
Offset panelPosition = renderBox.localToGlobal(Offset.zero);
Offset globalPosition = localToGlobal(Offset.zero) - panelPosition;
final renderer = texture.renderer;
renderer.save();
renderer.translate(
globalPosition.dx * devicePixelRatio,
globalPosition.dy * devicePixelRatio,
);
final scaledSize = size * devicePixelRatio;
final needsAdvance = rivePainter?.paint(
texture, devicePixelRatio, scaledSize, elapsedSeconds) ??
false;
_shouldAdvance = elapsedSeconds == 0 ? true : needsAdvance;
renderer.restore();
}
@override
int get sharedDrawOrder => drawOrder;
@override
void attach(PipelineOwner owner) {
super.attach(owner);
markNeedsLayout();
_shared.addPainter(this);
}
@override
void frameCallback(Duration duration) {
super.frameCallback(duration);
_shared.schedulePaint();
}
@override
void detach() {
_unsubscribe();
_scrollPosition = null;
_shared.removePainter(this);
super.detach();
}
}
base class SharedTextureArtboardWidgetPainter
extends ArtboardWidgetPainter<ArtboardPainter> {
SharedTextureArtboardWidgetPainter(ArtboardPainter super.painter);
void artboardChanged(Artboard artboard) => painter?.artboardChanged(artboard);
}

View File

@ -19,7 +19,9 @@ dependencies:
sdk: flutter
flutter_web_plugins:
sdk: flutter
rive_native: 0.0.9
meta: ^1.9.0
rive_native:
path: ../rive_native
dev_dependencies:
flutter_test: