diff --git a/.rive_head b/.rive_head index 7847a06..87bb42b 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -f7f795e53015b92c38abe175259caa7b5032cb76 +7359e8b824801f268a0506ae948ab5845f610d8c diff --git a/example/assets/centaur_v2.riv b/example/assets/centaur_v2.riv new file mode 100644 index 0000000..df5fdd6 Binary files /dev/null and b/example/assets/centaur_v2.riv differ diff --git a/example/assets/perf/rivs/Tom_Morello_2.riv b/example/assets/perf/rivs/Tom_Morello_2.riv new file mode 100644 index 0000000..47134a0 Binary files /dev/null and b/example/assets/perf/rivs/Tom_Morello_2.riv differ diff --git a/example/assets/perf/rivs/Zombie_Character.riv b/example/assets/perf/rivs/Zombie_Character.riv new file mode 100644 index 0000000..41c2656 Binary files /dev/null and b/example/assets/perf/rivs/Zombie_Character.riv differ diff --git a/example/assets/perf/rivs/adventuretime_marceline_pb.riv b/example/assets/perf/rivs/adventuretime_marceline_pb.riv new file mode 100644 index 0000000..3db0769 Binary files /dev/null and b/example/assets/perf/rivs/adventuretime_marceline_pb.riv differ diff --git a/example/assets/perf/rivs/skull_404.riv b/example/assets/perf/rivs/skull_404.riv new file mode 100644 index 0000000..2b72524 Binary files /dev/null and b/example/assets/perf/rivs/skull_404.riv differ diff --git a/example/assets/perf/rivs/towersDemo.riv b/example/assets/perf/rivs/towersDemo.riv new file mode 100644 index 0000000..5720aa8 Binary files /dev/null and b/example/assets/perf/rivs/towersDemo.riv differ diff --git a/example/assets/perf/rivs/travel_icons.riv b/example/assets/perf/rivs/travel_icons.riv new file mode 100644 index 0000000..67b26b9 Binary files /dev/null and b/example/assets/perf/rivs/travel_icons.riv differ diff --git a/example/assets/perf/rivs/walking.riv b/example/assets/perf/rivs/walking.riv new file mode 100644 index 0000000..7a63725 Binary files /dev/null and b/example/assets/perf/rivs/walking.riv differ diff --git a/example/lib/advanced/advanced.dart b/example/lib/advanced/advanced.dart new file mode 100644 index 0000000..8d7aa10 --- /dev/null +++ b/example/lib/advanced/advanced.dart @@ -0,0 +1 @@ +export 'centaur_example/game_widget.dart'; diff --git a/example/lib/advanced/centaur_example/centaur_game.dart b/example/lib/advanced/centaur_example/centaur_game.dart new file mode 100644 index 0000000..ec062a9 --- /dev/null +++ b/example/lib/advanced/centaur_example/centaur_game.dart @@ -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 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 _arrows = {}; + final Set _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 = {}; + 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 = {}; + 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); +} diff --git a/example/lib/advanced/centaur_example/game_widget.dart b/example/lib/advanced/centaur_example/game_widget.dart new file mode 100644 index 0000000..47fd2bd --- /dev/null +++ b/example/lib/advanced/centaur_example/game_widget.dart @@ -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 createState() => _CentaurGameWidgetState(); +} + +class _CentaurGameWidgetState extends State { + final rive.RenderTexture _renderTexture = + rive.RiveNative.instance.makeRenderTexture(); + CentaurGame? _centaurPainter; + + @override + void initState() { + super.initState(); + + load(); + } + + Future 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!, + ), + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/example/lib/examples/examples.dart b/example/lib/examples/examples.dart index adfc764..8aeb1c0 100644 --- a/example/lib/examples/examples.dart +++ b/example/lib/examples/examples.dart @@ -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'; diff --git a/example/lib/examples/rive_panel.dart b/example/lib/examples/rive_panel.dart new file mode 100644 index 0000000..f19797d --- /dev/null +++ b/example/lib/examples/rive_panel.dart @@ -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 createState() => _RowExampleState(); +} + +class _RowExampleState extends State { + // Only useful when using `Factory.rive`. `Factory.flutter` draws to a single + // render target already. + final factory = Factory.rive; + + late List 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 createState() => _ListViewExampleState(); +} + +class _ListViewExampleState extends State { + 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, + ) + }, + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 9d31210..e4433ac 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -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 { '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 { '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( diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 04bd4e4..5b0cecf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -32,5 +32,6 @@ flutter: assets: - assets/ - assets/fonts/ + - assets/perf/rivs/ - assets/audio/ - assets/images/ diff --git a/lib/rive.dart b/lib/rive.dart index 92c9c5e..d4509f0 100644 --- a/lib/rive.dart +++ b/lib/rive.dart @@ -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'; diff --git a/lib/src/widgets/inherited_widgets.dart b/lib/src/widgets/inherited_widgets.dart new file mode 100644 index 0000000..58f982f --- /dev/null +++ b/lib/src/widgets/inherited_widgets.dart @@ -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 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()?.texture; + + static SharedRenderTexture? find(BuildContext context) => + context.findAncestorWidgetOfExactType()?.texture; + + @override + bool updateShouldNotify(RiveSharedTexture old) => texture != old.texture; +} diff --git a/lib/src/widgets/rive_panel.dart b/lib/src/widgets/rive_panel.dart new file mode 100644 index 0000000..693405d --- /dev/null +++ b/lib/src/widgets/rive_panel.dart @@ -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 createState() => _ListViewExampleState(); +///} +/// +///class _ListViewExampleState extends State { +/// 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 createState() => _RivePanelState(); +} + +class _RivePanelState extends State { + 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, + ), + ], + ); + } +} diff --git a/lib/src/widgets/rive_widget.dart b/lib/src/widgets/rive_widget.dart index 6f310bf..f2c1e25 100644 --- a/lib/src/widgets/rive_widget.dart +++ b/lib/src/widgets/rive_widget.dart @@ -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 createState() => _RiveWidgetState(); } @@ -102,11 +123,41 @@ class _RiveWidgetState extends State { 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), + ); + } } diff --git a/lib/src/widgets/shared_texture_view.dart b/lib/src/widgets/shared_texture_view.dart new file mode 100644 index 0000000..6d19b33 --- /dev/null +++ b/lib/src/widgets/shared_texture_view.dart @@ -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 createState() => _SharedTextureViewState(); +} + +class _SharedTextureViewState extends State { + @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 + 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 { + SharedTextureArtboardWidgetPainter(ArtboardPainter super.painter); + + void artboardChanged(Artboard artboard) => painter?.artboardChanged(artboard); +} diff --git a/pubspec.yaml b/pubspec.yaml index 4dcb0db..2b5d8b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: