bring flutter asset loading inline with cpp

basically does what it says, i'm also removing an example i used to check on things being garbage collected

Diffs=
c0411df0a bring flutter asset loading inline with cpp (#6135)

Co-authored-by: Gordon Hayes <pggordonhayes@gmail.com>
Co-authored-by: Maxwell Talbot <talbot.maxwell@gmail.com>
This commit is contained in:
mjtalbot
2023-10-24 14:37:30 +00:00
parent 50f9624195
commit 312a8b0577
23 changed files with 212 additions and 478 deletions

View File

@ -1 +1 @@
db984dfd3dd7d0ebf6b4e9cfc9fed28269d8c8ed c0411df0a74b2aafa18526375cda19f6779a2354

View File

@ -1,6 +1,7 @@
## Upcoming ## 0.12.0
- Increase HTTP dependency range for Rive and Rive Common - Increase HTTP dependency range for Rive and Rive Common
- Update api for loading rive files, simplifying loading external images & fonts.
## 0.11.17 ## 0.11.17

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,296 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:rive/math.dart';
import 'package:rive/rive.dart';
class BasicTextCustomized extends StatefulWidget {
const BasicTextCustomized({Key? key}) : super(key: key);
@override
State<BasicTextCustomized> createState() => _BasicTextCustomizedState();
}
/// Basic example playing a Rive animation from a packaged asset.
class _BasicTextCustomizedState extends State<BasicTextCustomized>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 10));
RiveArtboardRenderer? _artboardRenderer;
List<Artboard> artboards = [];
late RiveFile file;
FontAsset? fontAsset;
randomFont() async {
final urls = [
'https://cdn.rive.app/runtime/flutter/IndieFlower-Regular.ttf',
'https://cdn.rive.app/runtime/flutter/comic-neue.ttf',
'https://cdn.rive.app/runtime/flutter/inter.ttf',
'https://cdn.rive.app/runtime/flutter/inter-tight.ttf',
'https://cdn.rive.app/runtime/flutter/josefin-sans.ttf',
'https://cdn.rive.app/runtime/flutter/send-flowers.ttf',
];
final res = await http.get(
// pick a random url from the list of fonts
Uri.parse(urls[Random().nextInt(urls.length)]),
);
await fontAsset?.decode(Uint8List.view(res.bodyBytes.buffer));
}
addArtboard() async {
unawaited(randomFont());
setState(() {
final artboard = file.mainArtboard.instance();
final controller = StateMachineController.fromArtboard(
artboard,
'State Machine 1',
)!;
artboard.addController(controller);
artboards.add(artboard);
while (artboards.length > 5) {
artboards.removeAt(0);
}
_artboardRenderer = RiveArtboardRenderer(
fit: BoxFit.cover,
alignment: Alignment.center,
artboards: artboards,
);
});
}
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.
file = await RiveFile.asset(
'assets/trans_text.riv',
loadEmbeddedAssets: false,
assetLoader: CallbackAssetLoader(
(asset) async {
if (asset is FontAsset) {
setState(() {
fontAsset = asset;
});
return true;
}
return false;
},
),
);
final artboard = file.mainArtboard.instance();
final controller = StateMachineController.fromArtboard(
artboard,
'State Machine 1',
)!;
artboard.addController(controller);
artboards.add(artboard);
unawaited(randomFont());
setState(
() => _artboardRenderer = RiveArtboardRenderer(
fit: BoxFit.cover,
alignment: Alignment.center,
artboards: [artboard],
),
);
}
@override
void initState() {
super.initState();
_animationController.repeat();
_load();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
addArtboard();
},
child: Center(
child: _artboardRenderer == null
? const SizedBox()
: CustomPaint(
painter: RiveCustomPainter(
_artboardRenderer!,
repaint: _animationController,
),
child: const SizedBox.expand(), // use all the size available
),
),
),
);
}
}
class RiveCustomPainter extends CustomPainter {
final RiveArtboardRenderer artboardRenderer;
RiveCustomPainter(this.artboardRenderer, {super.repaint}) {
_lastTickTime = DateTime.now();
_elapsedTime = Duration.zero;
}
late DateTime _lastTickTime;
late Duration _elapsedTime;
void _calculateElapsedTime() {
final currentTime = DateTime.now();
_elapsedTime = currentTime.difference(_lastTickTime);
_lastTickTime = currentTime;
}
@override
void paint(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();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
/// 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.
class RiveArtboardRenderer {
final List<Artboard> artboards;
final BoxFit fit;
final Alignment alignment;
RiveArtboardRenderer({
required this.fit,
required this.alignment,
required this.artboards,
});
void advance(double dt) {
for (var artboard in artboards) {
artboard.advance(dt, nested: true);
}
}
late final aabb =
AABB.fromValues(0, 0, artboards.first.width, artboards.first.height);
void render(Canvas canvas, Size size) {
_paint(canvas, aabb, size);
}
final _transform = Mat2D();
final _center = Mat2D();
void _paint(Canvas canvas, AABB bounds, Size size) {
for (var artboard in artboards) {
_paintArtboard(artboard, canvas, bounds, size);
bounds = AABB.fromValues(bounds.left - 100, bounds.top - 100,
bounds.right - 100, bounds.bottom - 100);
}
}
void _paintArtboard(
Artboard artboard, Canvas canvas, AABB bounds, Size size) {
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();
switch (fit) {
case BoxFit.fill:
scaleX = size.width / contentWidth;
scaleY = size.height / contentHeight;
break;
case BoxFit.contain:
final minScale =
min(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = minScale;
break;
case BoxFit.cover:
final maxScale =
max(size.width / contentWidth, size.height / contentHeight);
scaleX = scaleY = maxScale;
break;
case BoxFit.fitHeight:
final minScale = size.height / contentHeight;
scaleX = scaleY = minScale;
break;
case BoxFit.fitWidth:
final minScale = size.width / contentWidth;
scaleX = scaleY = minScale;
break;
case BoxFit.none:
scaleX = scaleY = 1.0;
break;
case BoxFit.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();
}
}

View File

@ -14,6 +14,8 @@ import 'package:http/http.dart' as http;
/// and provide them instantly. /// and provide them instantly.
/// ///
/// See `custom_cached_asset_loading.dart` for an example of this. /// See `custom_cached_asset_loading.dart` for an example of this.
///
/// See: https://help.rive.app/runtimes/loading-assets
class CustomAssetLoading extends StatefulWidget { class CustomAssetLoading extends StatefulWidget {
const CustomAssetLoading({Key? key}) : super(key: key); const CustomAssetLoading({Key? key}) : super(key: key);
@ -79,14 +81,18 @@ class _RiveRandomImageState extends State<_RiveRandomImage> {
Future<void> _loadFiles() async { Future<void> _loadFiles() async {
final imageFile = await RiveFile.asset( final imageFile = await RiveFile.asset(
'assets/asset.riv', 'assets/image_out_of_band.riv',
loadEmbeddedAssets: false,
assetLoader: CallbackAssetLoader( assetLoader: CallbackAssetLoader(
(asset) async { (asset, bytes) async {
final res = // Replace image assets that are not embedded in the rive file
await http.get(Uri.parse('https://picsum.photos/1000/1000')); if (asset is ImageAsset && bytes == null) {
await asset.decode(Uint8List.view(res.bodyBytes.buffer)); final res =
return true; await http.get(Uri.parse('https://picsum.photos/500/500'));
await asset.decode(Uint8List.view(res.bodyBytes.buffer));
return true;
} else {
return false; // use default asset loading
}
}, },
), ),
); );
@ -102,9 +108,23 @@ class _RiveRandomImageState extends State<_RiveRandomImage> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return RiveAnimation.direct( return Stack(
_riveImageSampleFile!, children: [
fit: BoxFit.cover, RiveAnimation.direct(
_riveImageSampleFile!,
stateMachines: const ['State Machine 1'],
fit: BoxFit.cover,
),
const Positioned(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'This example loads a random image dynamically and asynchronously.\n\nHover to zoom.',
style: TextStyle(color: Colors.black),
),
),
)
],
); );
} }
} }
@ -128,25 +148,29 @@ class _RiveRandomFontState extends State<_RiveRandomFont> {
Future<void> _loadFiles() async { Future<void> _loadFiles() async {
final fontFile = await RiveFile.asset( final fontFile = await RiveFile.asset(
'assets/sampletext.riv', 'assets/acqua_text_out_of_band.riv',
loadEmbeddedAssets: false, // disable loading embedded assets.
assetLoader: CallbackAssetLoader( assetLoader: CallbackAssetLoader(
(asset) async { (asset, bytes) async {
final urls = [ // Replace font assets that are not embedded in the rive file
'https://cdn.rive.app/runtime/flutter/IndieFlower-Regular.ttf', if (asset is FontAsset && bytes == null) {
'https://cdn.rive.app/runtime/flutter/comic-neue.ttf', final urls = [
'https://cdn.rive.app/runtime/flutter/inter.ttf', 'https://cdn.rive.app/runtime/flutter/IndieFlower-Regular.ttf',
'https://cdn.rive.app/runtime/flutter/inter-tight.ttf', 'https://cdn.rive.app/runtime/flutter/comic-neue.ttf',
'https://cdn.rive.app/runtime/flutter/josefin-sans.ttf', 'https://cdn.rive.app/runtime/flutter/inter.ttf',
'https://cdn.rive.app/runtime/flutter/send-flowers.ttf', 'https://cdn.rive.app/runtime/flutter/inter-tight.ttf',
]; 'https://cdn.rive.app/runtime/flutter/josefin-sans.ttf',
'https://cdn.rive.app/runtime/flutter/send-flowers.ttf',
];
final res = await http.get( final res = await http.get(
// pick a random url from the list of fonts // pick a random url from the list of fonts
Uri.parse(urls[Random().nextInt(urls.length)]), Uri.parse(urls[Random().nextInt(urls.length)]),
); );
await asset.decode(Uint8List.view(res.bodyBytes.buffer)); await asset.decode(Uint8List.view(res.bodyBytes.buffer));
return true; return true;
} else {
return false; // use default asset loading
}
}, },
), ),
); );
@ -162,9 +186,23 @@ class _RiveRandomFontState extends State<_RiveRandomFont> {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return RiveAnimation.direct( return Stack(
_riveFontSampleFile!, children: [
fit: BoxFit.cover, RiveAnimation.direct(
_riveFontSampleFile!,
stateMachines: const ['State Machine 1'],
fit: BoxFit.cover,
),
const Positioned(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'This example loads a random font dynamically and asynchronously.\n\nClick to change drink.',
style: TextStyle(color: Colors.black),
),
),
)
],
); );
} }
} }

View File

@ -14,6 +14,8 @@ import 'package:rive/rive.dart';
/// ///
/// The example also shows how to swap out the assets multiple times by /// The example also shows how to swap out the assets multiple times by
/// keeping a reference to the asset and swapping it out. /// keeping a reference to the asset and swapping it out.
///
/// See: https://help.rive.app/runtimes/loading-assets
class CustomCachedAssetLoading extends StatefulWidget { class CustomCachedAssetLoading extends StatefulWidget {
const CustomCachedAssetLoading({Key? key}) : super(key: key); const CustomCachedAssetLoading({Key? key}) : super(key: key);
@ -38,7 +40,7 @@ class _CustomCachedAssetLoadingState extends State<CustomCachedAssetLoading> {
Future<void> _warmUpCache() async { Future<void> _warmUpCache() async {
final futures = <Future>[]; final futures = <Future>[];
loadImage() async { loadImage() async {
final res = await http.get(Uri.parse('https://picsum.photos/1000/1000')); final res = await http.get(Uri.parse('https://picsum.photos/500/500'));
final body = Uint8List.view(res.bodyBytes.buffer); final body = Uint8List.view(res.bodyBytes.buffer);
final image = await ImageAsset.parseBytes(body); final image = await ImageAsset.parseBytes(body);
if (image != null) { if (image != null) {
@ -160,10 +162,9 @@ class __RiveRandomCachedImageState extends State<_RiveRandomCachedImage> {
Future<void> _loadRiveFile() async { Future<void> _loadRiveFile() async {
final imageFile = await RiveFile.asset( final imageFile = await RiveFile.asset(
'assets/asset.riv', 'assets/image_out_of_band.riv',
loadEmbeddedAssets: false,
assetLoader: CallbackAssetLoader( assetLoader: CallbackAssetLoader(
(asset) async { (asset, bytes) async {
if (asset is ImageAsset) { if (asset is ImageAsset) {
asset.image = _imageCache[Random().nextInt(_imageCache.length)]; asset.image = _imageCache[Random().nextInt(_imageCache.length)];
// Maintain a reference to the image asset // Maintain a reference to the image asset
@ -188,9 +189,23 @@ class __RiveRandomCachedImageState extends State<_RiveRandomCachedImage> {
return Column( return Column(
children: [ children: [
Expanded( Expanded(
child: RiveAnimation.direct( child: Stack(
_riveImageSampleFile!, children: [
fit: BoxFit.cover, RiveAnimation.direct(
_riveImageSampleFile!,
stateMachines: const ['State Machine 1'],
fit: BoxFit.cover,
),
const Positioned(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'This example caches images and swaps them out instantly.\n\nHover to zoom.',
style: TextStyle(color: Colors.black),
),
),
),
],
), ),
), ),
Padding( Padding(
@ -200,7 +215,7 @@ class __RiveRandomCachedImageState extends State<_RiveRandomCachedImage> {
_imageAsset?.image = _imageAsset?.image =
_imageCache[Random().nextInt(_imageCache.length)]; _imageCache[Random().nextInt(_imageCache.length)];
}, },
child: const Text('Random image asset'), child: const Text('Random image'),
), ),
), ),
], ],
@ -234,10 +249,9 @@ class __RiveRandomCachedFontState extends State<_RiveRandomCachedFont> {
Future<void> _loadRiveFile() async { Future<void> _loadRiveFile() async {
final fontFile = await RiveFile.asset( final fontFile = await RiveFile.asset(
'assets/sampletext.riv', 'assets/acqua_text_out_of_band.riv',
loadEmbeddedAssets: false,
assetLoader: CallbackAssetLoader( assetLoader: CallbackAssetLoader(
(asset) async { (asset, bytes) async {
if (asset is FontAsset) { if (asset is FontAsset) {
asset.font = _fontCache[Random().nextInt(_fontCache.length)]; asset.font = _fontCache[Random().nextInt(_fontCache.length)];
_fontAssets.add(asset); _fontAssets.add(asset);
@ -262,9 +276,23 @@ class __RiveRandomCachedFontState extends State<_RiveRandomCachedFont> {
return Column( return Column(
children: [ children: [
Expanded( Expanded(
child: RiveAnimation.direct( child: Stack(
_riveFontSampleFile!, children: [
fit: BoxFit.cover, RiveAnimation.direct(
_riveFontSampleFile!,
stateMachines: const ['State Machine 1'],
fit: BoxFit.cover,
),
const Positioned(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'This example caches fonts and swaps them out instantly.\n\nClick to change drink.',
style: TextStyle(color: Colors.black),
),
),
),
],
), ),
), ),
Padding( Padding(
@ -275,7 +303,7 @@ class __RiveRandomCachedFontState extends State<_RiveRandomCachedFont> {
element?.font = _fontCache[Random().nextInt(_fontCache.length)]; element?.font = _fontCache[Random().nextInt(_fontCache.length)];
} }
}, },
child: const Text('Random font asset'), child: const Text('Random font'),
), ),
), ),
], ],

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rive_example/basic_text_customized.dart';
import 'package:rive_example/custom_asset_loading.dart'; import 'package:rive_example/custom_asset_loading.dart';
import 'package:rive_example/custom_cached_asset_loading.dart'; import 'package:rive_example/custom_cached_asset_loading.dart';
@ -46,7 +45,6 @@ class RiveExampleApp extends StatefulWidget {
class _RiveExampleAppState extends State<RiveExampleApp> { class _RiveExampleAppState extends State<RiveExampleApp> {
// Example animations // Example animations
final _pages = [ final _pages = [
const _Page('Basic Text Customized', BasicTextCustomized()),
const _Page('Simple Animation - Asset', SimpleAssetAnimation()), const _Page('Simple Animation - Asset', SimpleAssetAnimation()),
const _Page('Simple Animation - Network', SimpleNetworkAnimation()), const _Page('Simple Animation - Network', SimpleNetworkAnimation()),
const _Page('Play/Pause Animation', PlayPauseAnimation()), const _Page('Play/Pause Animation', PlayPauseAnimation()),
@ -61,8 +59,8 @@ class _RiveExampleAppState extends State<RiveExampleApp> {
const _Page('Skinning Demo', SkinningDemo()), const _Page('Skinning Demo', SkinningDemo()),
const _Page('Animation Carousel', AnimationCarousel()), const _Page('Animation Carousel', AnimationCarousel()),
const _Page('Basic Text', BasicText()), const _Page('Basic Text', BasicText()),
const _Page('Custom Asset Loading', CustomAssetLoading()), const _Page('Asset Loading', CustomAssetLoading()),
const _Page('Custom Cached Asset Loading', CustomCachedAssetLoading()), const _Page('Cached Asset Loading', CustomCachedAssetLoading()),
const _Page('Event Open URL Button', EventOpenUrlButton()), const _Page('Event Open URL Button', EventOpenUrlButton()),
const _Page('Event Sounds', EventSounds()), const _Page('Event Sounds', EventSounds()),
const _Page('Event Star Rating', EventStarRating()), const _Page('Event Star Rating', EventStarRating()),

View File

@ -9,9 +9,9 @@ import 'package:rive/src/utilities/utilities.dart';
/// ///
/// See [CallbackAssetLoader] and [LocalAssetLoader] for an example of how to /// See [CallbackAssetLoader] and [LocalAssetLoader] for an example of how to
/// use this. /// use this.
// ignore: one_member_abstracts
abstract class FileAssetLoader { abstract class FileAssetLoader {
Future<bool> load(FileAsset asset); Future<bool> load(FileAsset asset, Uint8List? embeddedBytes);
bool isCompatible(FileAsset asset) => true;
} }
/// Loads assets from Rive's CDN. /// Loads assets from Rive's CDN.
@ -23,10 +23,12 @@ class CDNAssetLoader extends FileAssetLoader {
CDNAssetLoader(); CDNAssetLoader();
@override @override
bool isCompatible(FileAsset asset) => asset.cdnUuid.isNotEmpty; Future<bool> load(FileAsset asset, Uint8List? embeddedBytes) async {
// if the asset is embedded, or does not have a cdn uuid, do not attempt
@override // to load it
Future<bool> load(FileAsset asset) async { if (embeddedBytes != null || asset.cdnUuid.isEmpty) {
return false;
}
// TODO (Max): Do we have a URL builder? // TODO (Max): Do we have a URL builder?
// TODO (Max): We should aim to get loading errors exposed where // TODO (Max): We should aim to get loading errors exposed where
// possible, this includes failed network requests but also // possible, this includes failed network requests but also
@ -81,7 +83,11 @@ class LocalAssetLoader extends FileAssetLoader {
}) : _assetBundle = assetBundle ?? rootBundle; }) : _assetBundle = assetBundle ?? rootBundle;
@override @override
Future<bool> load(FileAsset asset) async { Future<bool> load(FileAsset asset, Uint8List? embeddedBytes) async {
// do not load embedded assets.
if (embeddedBytes != null) {
return false;
}
String? assetPath; String? assetPath;
switch (asset.type) { switch (asset.type) {
case Type.unknown: case Type.unknown:
@ -109,7 +115,6 @@ class LocalAssetLoader extends FileAssetLoader {
/// ///
/// This callback will be triggered for any **referenced** assets. /// This callback will be triggered for any **referenced** assets.
/// ///
/// Set [loadEmbeddedAssets] to false to disable loading embedded assets
/// ///
/// Set [loadCdnAssets] to false to disable loading /// Set [loadCdnAssets] to false to disable loading
/// assets from the Rive CDN. /// assets from the Rive CDN.
@ -120,10 +125,9 @@ class LocalAssetLoader extends FileAssetLoader {
/// ```dart /// ```dart
/// final riveFile = await RiveFile.asset( /// final riveFile = await RiveFile.asset(
/// 'assets/asset.riv', /// 'assets/asset.riv',
/// loadEmbeddedAssets: true,
/// loadCdnAssets: true, /// loadCdnAssets: true,
/// assetLoader: CallbackAssetLoader( /// assetLoader: CallbackAssetLoader(
/// (asset) async { /// (asset, bytes) async {
/// final res = /// final res =
/// await http.get(Uri.parse('https://picsum.photos/1000/1000')); /// await http.get(Uri.parse('https://picsum.photos/1000/1000'));
/// await asset.decode(Uint8List.view(res.bodyBytes.buffer)); /// await asset.decode(Uint8List.view(res.bodyBytes.buffer));
@ -133,13 +137,13 @@ class LocalAssetLoader extends FileAssetLoader {
/// ); /// );
/// ``` /// ```
class CallbackAssetLoader extends FileAssetLoader { class CallbackAssetLoader extends FileAssetLoader {
Future<bool> Function(FileAsset) callback; Future<bool> Function(FileAsset asset, Uint8List? embeddedBytes) callback;
CallbackAssetLoader(this.callback); CallbackAssetLoader(this.callback);
@override @override
Future<bool> load(FileAsset asset) async { Future<bool> load(FileAsset asset, Uint8List? embeddedBytes) async {
return callback(asset); return callback(asset, embeddedBytes);
} }
} }
@ -157,13 +161,10 @@ class FallbackAssetLoader extends FileAssetLoader {
FallbackAssetLoader(this.fileAssetLoaders); FallbackAssetLoader(this.fileAssetLoaders);
@override @override
Future<bool> load(FileAsset asset) async { Future<bool> load(FileAsset asset, Uint8List? embeddedBytes) async {
for (var i = 0; i < fileAssetLoaders.length; i++) { for (var i = 0; i < fileAssetLoaders.length; i++) {
final resolver = fileAssetLoaders[i]; final resolver = fileAssetLoaders[i];
if (!resolver.isCompatible(asset)) { final success = await resolver.load(asset, embeddedBytes);
continue;
}
final success = await resolver.load(asset);
if (success) { if (success) {
return true; return true;
} }

View File

@ -13,38 +13,32 @@ abstract class FileAssetResolver {
class FileAssetImporter extends ImportStackObject { class FileAssetImporter extends ImportStackObject {
final FileAssetLoader? assetLoader; final FileAssetLoader? assetLoader;
final FileAsset fileAsset; final FileAsset fileAsset;
final bool loadEmbeddedAssets; Uint8List? embeddedBytes;
FileAssetImporter( FileAssetImporter(
this.fileAsset, this.fileAsset,
this.assetLoader, { this.assetLoader,
this.loadEmbeddedAssets = true, );
});
bool _contentsResolved = false;
void resolveContents(FileAssetContents contents) { void resolveContents(FileAssetContents contents) {
if (loadEmbeddedAssets) { embeddedBytes = contents.bytes;
_contentsResolved = true;
fileAsset.decode(contents.bytes);
}
} }
@override @override
bool resolve() { bool resolve() {
if (!_contentsResolved) { // allow our loader to load the file asset.
// try to get them out of band assetLoader?.load(fileAsset, embeddedBytes).then((loaded) {
assetLoader?.load(fileAsset).then((loaded) { if (!loaded && embeddedBytes != null) {
fileAsset.decode(embeddedBytes!);
} else if (!loaded) {
// TODO: improve error logging // TODO: improve error logging
if (!loaded) { printDebugMessage(
printDebugMessage( '''Rive asset (${fileAsset.name}) was not able to load:
'''Rive asset (${fileAsset.name}) was not able to load:
- Unique file name: ${fileAsset.uniqueFilename} - Unique file name: ${fileAsset.uniqueFilename}
- Asset id: ${fileAsset.id}''', - Asset id: ${fileAsset.id}''',
); );
} }
}); });
}
return super.resolve(); return super.resolve();
} }
} }

View File

@ -5,9 +5,6 @@ class StateMachineBool extends StateMachineBoolBase {
@override @override
void valueChanged(bool from, bool to) {} void valueChanged(bool from, bool to) {}
@override
void publicChanged(bool from, bool to) {}
@override @override
bool isValidType<T>() => T == bool; bool isValidType<T>() => T == bool;
} }

View File

@ -18,7 +18,4 @@ abstract class StateMachineInput extends StateMachineInputBase {
bool isValidType<T>() => false; bool isValidType<T>() => false;
} }
class _StateMachineUnknownInput extends StateMachineInput { class _StateMachineUnknownInput extends StateMachineInput {}
@override
void publicChanged(bool from, bool to) {}
}

View File

@ -4,7 +4,4 @@ export 'package:rive/src/generated/animation/state_machine_number_base.dart';
class StateMachineNumber extends StateMachineNumberBase { class StateMachineNumber extends StateMachineNumberBase {
@override @override
void valueChanged(double from, double to) {} void valueChanged(double from, double to) {}
@override
void publicChanged(bool from, bool to) {}
} }

View File

@ -4,9 +4,6 @@ export 'package:rive/src/generated/animation/state_machine_trigger_base.dart';
class StateMachineTrigger extends StateMachineTriggerBase { class StateMachineTrigger extends StateMachineTriggerBase {
void fire() {} void fire() {}
@override
void publicChanged(bool from, bool to) {}
@override @override
bool isValidType<T>() => T == bool; bool isValidType<T>() => T == bool;
} }

View File

@ -113,6 +113,11 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
return null; return null;
} }
/// Find all components of a specific type.
Iterable<T> components<T>() {
return _components.whereType<T>();
}
@override @override
Artboard get artboard => this; Artboard get artboard => this;

View File

@ -117,7 +117,7 @@ class NestedArtboard extends NestedArtboardBase {
} }
bool advance(double elapsedSeconds) { bool advance(double elapsedSeconds) {
if (mountedArtboard == null) { if (mountedArtboard == null || isCollapsed) {
return false; return false;
} }

View File

@ -30,7 +30,6 @@ import 'package:rive/src/rive_core/animation/state_machine_listener.dart';
import 'package:rive/src/rive_core/animation/state_transition.dart'; import 'package:rive/src/rive_core/animation/state_transition.dart';
import 'package:rive/src/rive_core/artboard.dart'; import 'package:rive/src/rive_core/artboard.dart';
import 'package:rive/src/rive_core/assets/file_asset.dart'; import 'package:rive/src/rive_core/assets/file_asset.dart';
import 'package:rive/src/rive_core/assets/file_asset_contents.dart';
import 'package:rive/src/rive_core/assets/image_asset.dart'; import 'package:rive/src/rive_core/assets/image_asset.dart';
import 'package:rive/src/rive_core/backboard.dart'; import 'package:rive/src/rive_core/backboard.dart';
import 'package:rive/src/rive_core/component.dart'; import 'package:rive/src/rive_core/component.dart';
@ -154,9 +153,8 @@ class RiveFile {
RiveFile._( RiveFile._(
BinaryReader reader, BinaryReader reader,
this.header, this.header,
this._assetLoader, { this._assetLoader,
bool loadEmbeddedAssets = true, ) {
}) {
/// Property fields table of contents /// Property fields table of contents
final propertyToField = _propertyToFieldLookup(header); final propertyToField = _propertyToFieldLookup(header);
@ -175,12 +173,7 @@ class RiveFile {
} }
continue; continue;
} }
// TODO: Question (Max): two options, either tell the fileAssetImporter,
// or simply skip the object. I think we should skip the object.
if (!loadEmbeddedAssets && object is FileAssetContentsBase) {
// suppress importing embedded assets
continue;
}
ImportStackObject? stackObject; ImportStackObject? stackObject;
var stackType = object.coreType; var stackType = object.coreType;
switch (object.coreType) { switch (object.coreType) {
@ -247,7 +240,6 @@ class RiveFile {
stackObject = FileAssetImporter( stackObject = FileAssetImporter(
object as FileAsset, object as FileAsset,
_assetLoader, _assetLoader,
loadEmbeddedAssets: loadEmbeddedAssets,
); );
stackType = FileAssetBase.typeKey; stackType = FileAssetBase.typeKey;
break; break;
@ -322,10 +314,10 @@ class RiveFile {
/// ///
/// Set [loadCdnAssets] to `false` to disable loading assets from the CDN. /// Set [loadCdnAssets] to `false` to disable loading assets from the CDN.
/// ///
/// Set [loadEmbeddedAssets] to `false` to disable loading embedded assets.
///
/// Whether an assets is embedded/cdn/referenced is determined by the Rive /// Whether an assets is embedded/cdn/referenced is determined by the Rive
/// file - as set in the editor. /// file - as set in the editor.
///
/// Loading assets documentation: https://help.rive.app/runtimes/loading-assets
/// {@endtemplate} /// {@endtemplate}
/// ///
/// Will throw [RiveFormatErrorException] if data is malformed. Will throw /// Will throw [RiveFormatErrorException] if data is malformed. Will throw
@ -335,7 +327,6 @@ class RiveFile {
@Deprecated('Use `assetLoader` instead.') FileAssetResolver? assetResolver, @Deprecated('Use `assetLoader` instead.') FileAssetResolver? assetResolver,
FileAssetLoader? assetLoader, FileAssetLoader? assetLoader,
bool loadCdnAssets = true, bool loadCdnAssets = true,
bool loadEmbeddedAssets = true,
}) { }) {
var reader = BinaryReader(bytes); var reader = BinaryReader(bytes);
return RiveFile._( return RiveFile._(
@ -347,7 +338,6 @@ class RiveFile {
if (loadCdnAssets) CDNAssetLoader(), if (loadCdnAssets) CDNAssetLoader(),
], ],
), ),
loadEmbeddedAssets: loadEmbeddedAssets,
); );
} }
@ -365,7 +355,6 @@ class RiveFile {
ByteData bytes, { ByteData bytes, {
FileAssetLoader? assetLoader, FileAssetLoader? assetLoader,
bool loadCdnAssets = true, bool loadCdnAssets = true,
bool loadEmbeddedAssets = true,
}) async { }) async {
if (!_initializedText) { if (!_initializedText) {
/// If the file looks like needs the text runtime, let's load it. /// If the file looks like needs the text runtime, let's load it.
@ -378,7 +367,6 @@ class RiveFile {
bytes, bytes,
assetLoader: assetLoader, assetLoader: assetLoader,
loadCdnAssets: loadCdnAssets, loadCdnAssets: loadCdnAssets,
loadEmbeddedAssets: loadEmbeddedAssets,
); );
} }
@ -396,7 +384,6 @@ class RiveFile {
AssetBundle? bundle, AssetBundle? bundle,
FileAssetLoader? assetLoader, FileAssetLoader? assetLoader,
bool loadCdnAssets = true, bool loadCdnAssets = true,
bool loadEmbeddedAssets = true,
}) async { }) async {
final bytes = await (bundle ?? rootBundle).load( final bytes = await (bundle ?? rootBundle).load(
bundleKey, bundleKey,
@ -406,7 +393,6 @@ class RiveFile {
bytes, bytes,
assetLoader: assetLoader, assetLoader: assetLoader,
loadCdnAssets: loadCdnAssets, loadCdnAssets: loadCdnAssets,
loadEmbeddedAssets: loadEmbeddedAssets,
); );
} }
@ -424,7 +410,6 @@ class RiveFile {
@Deprecated('Use `assetLoader` instead.') FileAssetResolver? assetResolver, @Deprecated('Use `assetLoader` instead.') FileAssetResolver? assetResolver,
FileAssetLoader? assetLoader, FileAssetLoader? assetLoader,
bool loadCdnAssets = true, bool loadCdnAssets = true,
bool loadEmbeddedAssets = true,
}) async { }) async {
final res = await http.get(Uri.parse(url), headers: headers); final res = await http.get(Uri.parse(url), headers: headers);
final bytes = ByteData.view(res.bodyBytes.buffer); final bytes = ByteData.view(res.bodyBytes.buffer);
@ -432,7 +417,6 @@ class RiveFile {
bytes, bytes,
assetLoader: assetLoader, assetLoader: assetLoader,
loadCdnAssets: loadCdnAssets, loadCdnAssets: loadCdnAssets,
loadEmbeddedAssets: loadEmbeddedAssets,
); );
} }
@ -446,13 +430,11 @@ class RiveFile {
String path, { String path, {
FileAssetLoader? assetLoader, FileAssetLoader? assetLoader,
bool loadCdnAssets = true, bool loadCdnAssets = true,
bool loadEmbeddedAssets = true,
}) async { }) async {
final bytes = await localFileBytes(path); final bytes = await localFileBytes(path);
return _initTextAndImport( return _initTextAndImport(
ByteData.view(bytes!.buffer), ByteData.view(bytes!.buffer),
assetLoader: assetLoader, assetLoader: assetLoader,
loadEmbeddedAssets: loadEmbeddedAssets,
loadCdnAssets: loadCdnAssets, loadCdnAssets: loadCdnAssets,
); );
} }

View File

@ -1,5 +1,5 @@
name: rive name: rive
version: 0.11.17 version: 0.12.0
homepage: https://rive.app homepage: https://rive.app
description: Rive 2 Flutter Runtime. This package provides runtime functionality for playing back and interacting with animations built with the Rive editor available at https://rive.app. description: Rive 2 Flutter Runtime. This package provides runtime functionality for playing back and interacting with animations built with the Rive editor available at https://rive.app.
repository: https://github.com/rive-app/rive-flutter repository: https://github.com/rive-app/rive-flutter

View File

@ -46,25 +46,26 @@ void main() {
); );
// each artboard adds file asset referencer. // each artboard adds file asset referencer.
var count = 20; var count = 19;
while (count-- > 0) { while (count-- > 0) {
riveFile.artboards.first.instance(); riveFile.artboards.first.instance();
} }
final image = riveFile.artboards.first.component<Image>(assetName); final image = riveFile.artboards.first.component<Image>(assetName);
final asset = image!.asset!; final asset = image!.asset!;
expect(asset.fileAssetReferencers.length, 21); expect(asset.fileAssetReferencers.length, 20);
await Future<void>.delayed(const Duration(milliseconds: 10)); await Future<void>.delayed(const Duration(milliseconds: 100));
// ok, kinda lame, but the above allows garbage collection to kick in // ok, kinda lame, but the above allows garbage collection to kick in
// which will remove referencers, its not really deterministic though // which will remove referencers, its not really deterministic though
expect( expect(
asset.fileAssetReferencers.length < 5, asset.fileAssetReferencers.length < 5,
true, true,
reason: "Expected ${asset.fileAssetReferencers.length} < 5", reason: "Expected ${asset.fileAssetReferencers.length} < 5",
); );
}, },
// skipping because it only works when we run this test directly, // skipping because it does not work, you can see things get
// not when running it as part of other tests. // finalized but this does not consistently happen in tests.
skip: true, skip: true,
); );
}); });

View File

@ -35,22 +35,6 @@ void main() {
verifyNever(() => mockHttpClient.openUrl(any(), any())); verifyNever(() => mockHttpClient.openUrl(any(), any()));
}); });
test('Disabling embedded assets also does not hit a url', () async {
final mockHttpClient = getMockHttpClient();
await HttpOverrides.runZoned(() async {
final riveBytes = loadFile('assets/sample_image.riv');
RiveFile.import(
riveBytes,
loadEmbeddedAssets: false,
);
}, createHttpClient: (_) => mockHttpClient);
// by default we try to make a network request
verifyNever(() => mockHttpClient.openUrl(any(), any()));
});
test('Disabling cdn also does not hit a url', () async { test('Disabling cdn also does not hit a url', () async {
final mockHttpClient = getMockHttpClient(); final mockHttpClient = getMockHttpClient();
@ -59,7 +43,6 @@ void main() {
runZonedGuarded(() { runZonedGuarded(() {
RiveFile.import( RiveFile.import(
riveBytes, riveBytes,
loadEmbeddedAssets: false,
loadCdnAssets: false, loadCdnAssets: false,
); );
}, (error, stack) { }, (error, stack) {
@ -76,16 +59,18 @@ void main() {
verifyNever(() => mockHttpClient.openUrl(any(), any())); verifyNever(() => mockHttpClient.openUrl(any(), any()));
// by default we try to check for assets // by default we try to check for assets
}); });
test('test importing rive file, make sure we get a good callback', test('test importing rive file, make sure we get a good callback',
() async { () async {
// lets just return an image // lets just return an image
final riveBytes = loadFile('assets/sample_image.riv'); final riveBytes = loadFile('assets/sample_image.riv');
final imageBytes = loadFile('assets/file.png'); final imageBytes = loadFile('assets/file.png');
final parameters = []; final assets = [];
RiveFile.import(riveBytes, loadEmbeddedAssets: false, final byteList = [];
assetLoader: CallbackAssetLoader( RiveFile.import(riveBytes, assetLoader: CallbackAssetLoader(
(asset) async { (asset, bytes) async {
parameters.add(asset); assets.add(asset);
byteList.add(bytes);
await asset.decode(Uint8List.sublistView( await asset.decode(Uint8List.sublistView(
imageBytes, imageBytes,
)); ));
@ -93,7 +78,7 @@ void main() {
}, },
)); ));
final asset = parameters.first; final asset = assets.first;
expect(asset is ImageAsset, true); expect(asset is ImageAsset, true);
final fileAsset = asset as ImageAsset; final fileAsset = asset as ImageAsset;
@ -101,7 +86,38 @@ void main() {
expect(fileAsset.type, Type.image); expect(fileAsset.type, Type.image);
expect(fileAsset.name, assetName); expect(fileAsset.name, assetName);
expect(fileAsset.assetId, 42981); expect(fileAsset.assetId, 42981);
expect(fileAsset.id, -1); expect(fileAsset.id, -1);
expect(byteList.first.length, 202385);
});
test('test we load embedded assets if loaders are not provided', () async {
// lets just return an image
final riveBytes = loadFile('assets/sample_image.riv');
final assets = [];
RiveFile.import(riveBytes, assetLoader: CallbackAssetLoader(
(asset, bytes) async {
assets.add(asset);
return false;
},
));
final asset = assets.first;
expect(asset is ImageAsset, true);
final fileAsset = asset as ImageAsset;
expect(fileAsset.extension, Extension.png);
expect(fileAsset.type, Type.image);
expect(fileAsset.name, assetName);
expect(fileAsset.assetId, 42981);
// file asset will not be loaded
expect(fileAsset.image, null);
expect(fileAsset.id, -1);
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(fileAsset.image != null, true);
}); });
test('Make sure the image gets the dimensions once the image is loaded', test('Make sure the image gets the dimensions once the image is loaded',
@ -113,9 +129,8 @@ void main() {
final file = RiveFile.import( final file = RiveFile.import(
riveBytes, riveBytes,
loadEmbeddedAssets: false,
assetLoader: CallbackAssetLoader( assetLoader: CallbackAssetLoader(
(asset) async { (asset, bytes) async {
await asset.decode(Uint8List.sublistView( await asset.decode(Uint8List.sublistView(
imageBytes, imageBytes,
)); ));
@ -159,30 +174,12 @@ void main() {
)).called(1); )).called(1);
}); });
test('Disabling embedded assets also hits a url', () async {
await HttpOverrides.runZoned(() async {
final riveBytes = loadFile('assets/cdn_image.riv');
runZonedGuarded(() {
RiveFile.import(
riveBytes,
loadEmbeddedAssets: false,
);
}, (error, stack) {
print('what?');
});
}, createHttpClient: (_) => mockHttpClient);
// by default we try to make a network request
verify(() => mockHttpClient.openUrl(any(), any())).called(1);
});
test('Disabling cdn will mean no url hit', () async { test('Disabling cdn will mean no url hit', () async {
await HttpOverrides.runZoned(() async { await HttpOverrides.runZoned(() async {
final riveBytes = loadFile('assets/cdn_image.riv'); final riveBytes = loadFile('assets/cdn_image.riv');
RiveFile.import( RiveFile.import(
riveBytes, riveBytes,
loadEmbeddedAssets: false,
loadCdnAssets: false, loadCdnAssets: false,
); );
}, createHttpClient: (_) => mockHttpClient); }, createHttpClient: (_) => mockHttpClient);
@ -199,10 +196,8 @@ void main() {
final parameters = []; final parameters = [];
await HttpOverrides.runZoned(() async { await HttpOverrides.runZoned(() async {
final riveBytes = loadFile('assets/cdn_image.riv'); final riveBytes = loadFile('assets/cdn_image.riv');
RiveFile.import(riveBytes, assetLoader: CallbackAssetLoader(
RiveFile.import(riveBytes, loadEmbeddedAssets: false, (asset, bytes) async {
assetLoader: CallbackAssetLoader(
(asset) async {
parameters.add(asset); parameters.add(asset);
await asset.decode(Uint8List.sublistView( await asset.decode(Uint8List.sublistView(
imageBytes, imageBytes,
@ -226,9 +221,8 @@ void main() {
await HttpOverrides.runZoned(() async { await HttpOverrides.runZoned(() async {
final riveBytes = loadFile('assets/cdn_image.riv'); final riveBytes = loadFile('assets/cdn_image.riv');
RiveFile.import(riveBytes, loadEmbeddedAssets: false, RiveFile.import(riveBytes, assetLoader: CallbackAssetLoader(
assetLoader: CallbackAssetLoader( (asset, bytes) async {
(asset) async {
parameters.add(asset); parameters.add(asset);
return false; return false;
}, },