mirror of
https://github.com/rive-app/rive-flutter.git
synced 2025-05-17 21:36:06 +08:00
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:
@ -1 +1 @@
|
||||
db984dfd3dd7d0ebf6b4e9cfc9fed28269d8c8ed
|
||||
c0411df0a74b2aafa18526375cda19f6779a2354
|
||||
|
@ -1,6 +1,7 @@
|
||||
## Upcoming
|
||||
## 0.12.0
|
||||
|
||||
- Increase HTTP dependency range for Rive and Rive Common
|
||||
- Update api for loading rive files, simplifying loading external images & fonts.
|
||||
|
||||
## 0.11.17
|
||||
|
||||
|
BIN
example/assets/acqua_text_out_of_band.riv
Normal file
BIN
example/assets/acqua_text_out_of_band.riv
Normal file
Binary file not shown.
Binary file not shown.
BIN
example/assets/image_out_of_band.riv
Normal file
BIN
example/assets/image_out_of_band.riv
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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();
|
||||
}
|
||||
}
|
@ -14,6 +14,8 @@ import 'package:http/http.dart' as http;
|
||||
/// and provide them instantly.
|
||||
///
|
||||
/// See `custom_cached_asset_loading.dart` for an example of this.
|
||||
///
|
||||
/// See: https://help.rive.app/runtimes/loading-assets
|
||||
class CustomAssetLoading extends StatefulWidget {
|
||||
const CustomAssetLoading({Key? key}) : super(key: key);
|
||||
|
||||
@ -79,14 +81,18 @@ class _RiveRandomImageState extends State<_RiveRandomImage> {
|
||||
|
||||
Future<void> _loadFiles() async {
|
||||
final imageFile = await RiveFile.asset(
|
||||
'assets/asset.riv',
|
||||
loadEmbeddedAssets: false,
|
||||
'assets/image_out_of_band.riv',
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
final res =
|
||||
await http.get(Uri.parse('https://picsum.photos/1000/1000'));
|
||||
await asset.decode(Uint8List.view(res.bodyBytes.buffer));
|
||||
return true;
|
||||
(asset, bytes) async {
|
||||
// Replace image assets that are not embedded in the rive file
|
||||
if (asset is ImageAsset && bytes == null) {
|
||||
final res =
|
||||
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 RiveAnimation.direct(
|
||||
_riveImageSampleFile!,
|
||||
fit: BoxFit.cover,
|
||||
return Stack(
|
||||
children: [
|
||||
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 {
|
||||
final fontFile = await RiveFile.asset(
|
||||
'assets/sampletext.riv',
|
||||
loadEmbeddedAssets: false, // disable loading embedded assets.
|
||||
'assets/acqua_text_out_of_band.riv',
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) 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',
|
||||
];
|
||||
(asset, bytes) async {
|
||||
// Replace font assets that are not embedded in the rive file
|
||||
if (asset is FontAsset && bytes == null) {
|
||||
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 asset.decode(Uint8List.view(res.bodyBytes.buffer));
|
||||
return true;
|
||||
final res = await http.get(
|
||||
// pick a random url from the list of fonts
|
||||
Uri.parse(urls[Random().nextInt(urls.length)]),
|
||||
);
|
||||
await asset.decode(Uint8List.view(res.bodyBytes.buffer));
|
||||
return true;
|
||||
} else {
|
||||
return false; // use default asset loading
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -162,9 +186,23 @@ class _RiveRandomFontState extends State<_RiveRandomFont> {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return RiveAnimation.direct(
|
||||
_riveFontSampleFile!,
|
||||
fit: BoxFit.cover,
|
||||
return Stack(
|
||||
children: [
|
||||
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),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ import 'package:rive/rive.dart';
|
||||
///
|
||||
/// The example also shows how to swap out the assets multiple times by
|
||||
/// keeping a reference to the asset and swapping it out.
|
||||
///
|
||||
/// See: https://help.rive.app/runtimes/loading-assets
|
||||
class CustomCachedAssetLoading extends StatefulWidget {
|
||||
const CustomCachedAssetLoading({Key? key}) : super(key: key);
|
||||
|
||||
@ -38,7 +40,7 @@ class _CustomCachedAssetLoadingState extends State<CustomCachedAssetLoading> {
|
||||
Future<void> _warmUpCache() async {
|
||||
final futures = <Future>[];
|
||||
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 image = await ImageAsset.parseBytes(body);
|
||||
if (image != null) {
|
||||
@ -160,10 +162,9 @@ class __RiveRandomCachedImageState extends State<_RiveRandomCachedImage> {
|
||||
|
||||
Future<void> _loadRiveFile() async {
|
||||
final imageFile = await RiveFile.asset(
|
||||
'assets/asset.riv',
|
||||
loadEmbeddedAssets: false,
|
||||
'assets/image_out_of_band.riv',
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
(asset, bytes) async {
|
||||
if (asset is ImageAsset) {
|
||||
asset.image = _imageCache[Random().nextInt(_imageCache.length)];
|
||||
// Maintain a reference to the image asset
|
||||
@ -188,9 +189,23 @@ class __RiveRandomCachedImageState extends State<_RiveRandomCachedImage> {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RiveAnimation.direct(
|
||||
_riveImageSampleFile!,
|
||||
fit: BoxFit.cover,
|
||||
child: Stack(
|
||||
children: [
|
||||
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(
|
||||
@ -200,7 +215,7 @@ class __RiveRandomCachedImageState extends State<_RiveRandomCachedImage> {
|
||||
_imageAsset?.image =
|
||||
_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 {
|
||||
final fontFile = await RiveFile.asset(
|
||||
'assets/sampletext.riv',
|
||||
loadEmbeddedAssets: false,
|
||||
'assets/acqua_text_out_of_band.riv',
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
(asset, bytes) async {
|
||||
if (asset is FontAsset) {
|
||||
asset.font = _fontCache[Random().nextInt(_fontCache.length)];
|
||||
_fontAssets.add(asset);
|
||||
@ -262,9 +276,23 @@ class __RiveRandomCachedFontState extends State<_RiveRandomCachedFont> {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: RiveAnimation.direct(
|
||||
_riveFontSampleFile!,
|
||||
fit: BoxFit.cover,
|
||||
child: Stack(
|
||||
children: [
|
||||
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(
|
||||
@ -275,7 +303,7 @@ class __RiveRandomCachedFontState extends State<_RiveRandomCachedFont> {
|
||||
element?.font = _fontCache[Random().nextInt(_fontCache.length)];
|
||||
}
|
||||
},
|
||||
child: const Text('Random font asset'),
|
||||
child: const Text('Random font'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -1,5 +1,4 @@
|
||||
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_cached_asset_loading.dart';
|
||||
@ -46,7 +45,6 @@ class RiveExampleApp extends StatefulWidget {
|
||||
class _RiveExampleAppState extends State<RiveExampleApp> {
|
||||
// Example animations
|
||||
final _pages = [
|
||||
const _Page('Basic Text Customized', BasicTextCustomized()),
|
||||
const _Page('Simple Animation - Asset', SimpleAssetAnimation()),
|
||||
const _Page('Simple Animation - Network', SimpleNetworkAnimation()),
|
||||
const _Page('Play/Pause Animation', PlayPauseAnimation()),
|
||||
@ -61,8 +59,8 @@ class _RiveExampleAppState extends State<RiveExampleApp> {
|
||||
const _Page('Skinning Demo', SkinningDemo()),
|
||||
const _Page('Animation Carousel', AnimationCarousel()),
|
||||
const _Page('Basic Text', BasicText()),
|
||||
const _Page('Custom Asset Loading', CustomAssetLoading()),
|
||||
const _Page('Custom Cached Asset Loading', CustomCachedAssetLoading()),
|
||||
const _Page('Asset Loading', CustomAssetLoading()),
|
||||
const _Page('Cached Asset Loading', CustomCachedAssetLoading()),
|
||||
const _Page('Event Open URL Button', EventOpenUrlButton()),
|
||||
const _Page('Event Sounds', EventSounds()),
|
||||
const _Page('Event Star Rating', EventStarRating()),
|
||||
|
@ -9,9 +9,9 @@ import 'package:rive/src/utilities/utilities.dart';
|
||||
///
|
||||
/// See [CallbackAssetLoader] and [LocalAssetLoader] for an example of how to
|
||||
/// use this.
|
||||
// ignore: one_member_abstracts
|
||||
abstract class FileAssetLoader {
|
||||
Future<bool> load(FileAsset asset);
|
||||
bool isCompatible(FileAsset asset) => true;
|
||||
Future<bool> load(FileAsset asset, Uint8List? embeddedBytes);
|
||||
}
|
||||
|
||||
/// Loads assets from Rive's CDN.
|
||||
@ -23,10 +23,12 @@ class CDNAssetLoader extends FileAssetLoader {
|
||||
CDNAssetLoader();
|
||||
|
||||
@override
|
||||
bool isCompatible(FileAsset asset) => asset.cdnUuid.isNotEmpty;
|
||||
|
||||
@override
|
||||
Future<bool> load(FileAsset asset) async {
|
||||
Future<bool> load(FileAsset asset, Uint8List? embeddedBytes) async {
|
||||
// if the asset is embedded, or does not have a cdn uuid, do not attempt
|
||||
// to load it
|
||||
if (embeddedBytes != null || asset.cdnUuid.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
// TODO (Max): Do we have a URL builder?
|
||||
// TODO (Max): We should aim to get loading errors exposed where
|
||||
// possible, this includes failed network requests but also
|
||||
@ -81,7 +83,11 @@ class LocalAssetLoader extends FileAssetLoader {
|
||||
}) : _assetBundle = assetBundle ?? rootBundle;
|
||||
|
||||
@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;
|
||||
switch (asset.type) {
|
||||
case Type.unknown:
|
||||
@ -109,7 +115,6 @@ class LocalAssetLoader extends FileAssetLoader {
|
||||
///
|
||||
/// 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
|
||||
/// assets from the Rive CDN.
|
||||
@ -120,10 +125,9 @@ class LocalAssetLoader extends FileAssetLoader {
|
||||
/// ```dart
|
||||
/// final riveFile = await RiveFile.asset(
|
||||
/// 'assets/asset.riv',
|
||||
/// loadEmbeddedAssets: true,
|
||||
/// loadCdnAssets: true,
|
||||
/// assetLoader: CallbackAssetLoader(
|
||||
/// (asset) async {
|
||||
/// (asset, bytes) async {
|
||||
/// final res =
|
||||
/// await http.get(Uri.parse('https://picsum.photos/1000/1000'));
|
||||
/// await asset.decode(Uint8List.view(res.bodyBytes.buffer));
|
||||
@ -133,13 +137,13 @@ class LocalAssetLoader extends FileAssetLoader {
|
||||
/// );
|
||||
/// ```
|
||||
class CallbackAssetLoader extends FileAssetLoader {
|
||||
Future<bool> Function(FileAsset) callback;
|
||||
Future<bool> Function(FileAsset asset, Uint8List? embeddedBytes) callback;
|
||||
|
||||
CallbackAssetLoader(this.callback);
|
||||
|
||||
@override
|
||||
Future<bool> load(FileAsset asset) async {
|
||||
return callback(asset);
|
||||
Future<bool> load(FileAsset asset, Uint8List? embeddedBytes) async {
|
||||
return callback(asset, embeddedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,13 +161,10 @@ class FallbackAssetLoader extends FileAssetLoader {
|
||||
FallbackAssetLoader(this.fileAssetLoaders);
|
||||
|
||||
@override
|
||||
Future<bool> load(FileAsset asset) async {
|
||||
Future<bool> load(FileAsset asset, Uint8List? embeddedBytes) async {
|
||||
for (var i = 0; i < fileAssetLoaders.length; i++) {
|
||||
final resolver = fileAssetLoaders[i];
|
||||
if (!resolver.isCompatible(asset)) {
|
||||
continue;
|
||||
}
|
||||
final success = await resolver.load(asset);
|
||||
final success = await resolver.load(asset, embeddedBytes);
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
|
@ -13,38 +13,32 @@ abstract class FileAssetResolver {
|
||||
class FileAssetImporter extends ImportStackObject {
|
||||
final FileAssetLoader? assetLoader;
|
||||
final FileAsset fileAsset;
|
||||
final bool loadEmbeddedAssets;
|
||||
Uint8List? embeddedBytes;
|
||||
|
||||
FileAssetImporter(
|
||||
this.fileAsset,
|
||||
this.assetLoader, {
|
||||
this.loadEmbeddedAssets = true,
|
||||
});
|
||||
|
||||
bool _contentsResolved = false;
|
||||
this.assetLoader,
|
||||
);
|
||||
|
||||
void resolveContents(FileAssetContents contents) {
|
||||
if (loadEmbeddedAssets) {
|
||||
_contentsResolved = true;
|
||||
fileAsset.decode(contents.bytes);
|
||||
}
|
||||
embeddedBytes = contents.bytes;
|
||||
}
|
||||
|
||||
@override
|
||||
bool resolve() {
|
||||
if (!_contentsResolved) {
|
||||
// try to get them out of band
|
||||
assetLoader?.load(fileAsset).then((loaded) {
|
||||
// allow our loader to load the file asset.
|
||||
assetLoader?.load(fileAsset, embeddedBytes).then((loaded) {
|
||||
if (!loaded && embeddedBytes != null) {
|
||||
fileAsset.decode(embeddedBytes!);
|
||||
} else if (!loaded) {
|
||||
// TODO: improve error logging
|
||||
if (!loaded) {
|
||||
printDebugMessage(
|
||||
'''Rive asset (${fileAsset.name}) was not able to load:
|
||||
printDebugMessage(
|
||||
'''Rive asset (${fileAsset.name}) was not able to load:
|
||||
- Unique file name: ${fileAsset.uniqueFilename}
|
||||
- Asset id: ${fileAsset.id}''',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
return super.resolve();
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,6 @@ class StateMachineBool extends StateMachineBoolBase {
|
||||
@override
|
||||
void valueChanged(bool from, bool to) {}
|
||||
|
||||
@override
|
||||
void publicChanged(bool from, bool to) {}
|
||||
|
||||
@override
|
||||
bool isValidType<T>() => T == bool;
|
||||
}
|
||||
|
@ -18,7 +18,4 @@ abstract class StateMachineInput extends StateMachineInputBase {
|
||||
bool isValidType<T>() => false;
|
||||
}
|
||||
|
||||
class _StateMachineUnknownInput extends StateMachineInput {
|
||||
@override
|
||||
void publicChanged(bool from, bool to) {}
|
||||
}
|
||||
class _StateMachineUnknownInput extends StateMachineInput {}
|
||||
|
@ -4,7 +4,4 @@ export 'package:rive/src/generated/animation/state_machine_number_base.dart';
|
||||
class StateMachineNumber extends StateMachineNumberBase {
|
||||
@override
|
||||
void valueChanged(double from, double to) {}
|
||||
|
||||
@override
|
||||
void publicChanged(bool from, bool to) {}
|
||||
}
|
||||
|
@ -4,9 +4,6 @@ export 'package:rive/src/generated/animation/state_machine_trigger_base.dart';
|
||||
class StateMachineTrigger extends StateMachineTriggerBase {
|
||||
void fire() {}
|
||||
|
||||
@override
|
||||
void publicChanged(bool from, bool to) {}
|
||||
|
||||
@override
|
||||
bool isValidType<T>() => T == bool;
|
||||
}
|
||||
|
@ -113,6 +113,11 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Find all components of a specific type.
|
||||
Iterable<T> components<T>() {
|
||||
return _components.whereType<T>();
|
||||
}
|
||||
|
||||
@override
|
||||
Artboard get artboard => this;
|
||||
|
||||
|
@ -117,7 +117,7 @@ class NestedArtboard extends NestedArtboardBase {
|
||||
}
|
||||
|
||||
bool advance(double elapsedSeconds) {
|
||||
if (mountedArtboard == null) {
|
||||
if (mountedArtboard == null || isCollapsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -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/artboard.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/backboard.dart';
|
||||
import 'package:rive/src/rive_core/component.dart';
|
||||
@ -154,9 +153,8 @@ class RiveFile {
|
||||
RiveFile._(
|
||||
BinaryReader reader,
|
||||
this.header,
|
||||
this._assetLoader, {
|
||||
bool loadEmbeddedAssets = true,
|
||||
}) {
|
||||
this._assetLoader,
|
||||
) {
|
||||
/// Property fields table of contents
|
||||
final propertyToField = _propertyToFieldLookup(header);
|
||||
|
||||
@ -175,12 +173,7 @@ class RiveFile {
|
||||
}
|
||||
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;
|
||||
var stackType = object.coreType;
|
||||
switch (object.coreType) {
|
||||
@ -247,7 +240,6 @@ class RiveFile {
|
||||
stackObject = FileAssetImporter(
|
||||
object as FileAsset,
|
||||
_assetLoader,
|
||||
loadEmbeddedAssets: loadEmbeddedAssets,
|
||||
);
|
||||
stackType = FileAssetBase.typeKey;
|
||||
break;
|
||||
@ -322,10 +314,10 @@ class RiveFile {
|
||||
///
|
||||
/// 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
|
||||
/// file - as set in the editor.
|
||||
///
|
||||
/// Loading assets documentation: https://help.rive.app/runtimes/loading-assets
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// Will throw [RiveFormatErrorException] if data is malformed. Will throw
|
||||
@ -335,7 +327,6 @@ class RiveFile {
|
||||
@Deprecated('Use `assetLoader` instead.') FileAssetResolver? assetResolver,
|
||||
FileAssetLoader? assetLoader,
|
||||
bool loadCdnAssets = true,
|
||||
bool loadEmbeddedAssets = true,
|
||||
}) {
|
||||
var reader = BinaryReader(bytes);
|
||||
return RiveFile._(
|
||||
@ -347,7 +338,6 @@ class RiveFile {
|
||||
if (loadCdnAssets) CDNAssetLoader(),
|
||||
],
|
||||
),
|
||||
loadEmbeddedAssets: loadEmbeddedAssets,
|
||||
);
|
||||
}
|
||||
|
||||
@ -365,7 +355,6 @@ class RiveFile {
|
||||
ByteData bytes, {
|
||||
FileAssetLoader? assetLoader,
|
||||
bool loadCdnAssets = true,
|
||||
bool loadEmbeddedAssets = true,
|
||||
}) async {
|
||||
if (!_initializedText) {
|
||||
/// If the file looks like needs the text runtime, let's load it.
|
||||
@ -378,7 +367,6 @@ class RiveFile {
|
||||
bytes,
|
||||
assetLoader: assetLoader,
|
||||
loadCdnAssets: loadCdnAssets,
|
||||
loadEmbeddedAssets: loadEmbeddedAssets,
|
||||
);
|
||||
}
|
||||
|
||||
@ -396,7 +384,6 @@ class RiveFile {
|
||||
AssetBundle? bundle,
|
||||
FileAssetLoader? assetLoader,
|
||||
bool loadCdnAssets = true,
|
||||
bool loadEmbeddedAssets = true,
|
||||
}) async {
|
||||
final bytes = await (bundle ?? rootBundle).load(
|
||||
bundleKey,
|
||||
@ -406,7 +393,6 @@ class RiveFile {
|
||||
bytes,
|
||||
assetLoader: assetLoader,
|
||||
loadCdnAssets: loadCdnAssets,
|
||||
loadEmbeddedAssets: loadEmbeddedAssets,
|
||||
);
|
||||
}
|
||||
|
||||
@ -424,7 +410,6 @@ class RiveFile {
|
||||
@Deprecated('Use `assetLoader` instead.') FileAssetResolver? assetResolver,
|
||||
FileAssetLoader? assetLoader,
|
||||
bool loadCdnAssets = true,
|
||||
bool loadEmbeddedAssets = true,
|
||||
}) async {
|
||||
final res = await http.get(Uri.parse(url), headers: headers);
|
||||
final bytes = ByteData.view(res.bodyBytes.buffer);
|
||||
@ -432,7 +417,6 @@ class RiveFile {
|
||||
bytes,
|
||||
assetLoader: assetLoader,
|
||||
loadCdnAssets: loadCdnAssets,
|
||||
loadEmbeddedAssets: loadEmbeddedAssets,
|
||||
);
|
||||
}
|
||||
|
||||
@ -446,13 +430,11 @@ class RiveFile {
|
||||
String path, {
|
||||
FileAssetLoader? assetLoader,
|
||||
bool loadCdnAssets = true,
|
||||
bool loadEmbeddedAssets = true,
|
||||
}) async {
|
||||
final bytes = await localFileBytes(path);
|
||||
return _initTextAndImport(
|
||||
ByteData.view(bytes!.buffer),
|
||||
assetLoader: assetLoader,
|
||||
loadEmbeddedAssets: loadEmbeddedAssets,
|
||||
loadCdnAssets: loadCdnAssets,
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: rive
|
||||
version: 0.11.17
|
||||
version: 0.12.0
|
||||
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.
|
||||
repository: https://github.com/rive-app/rive-flutter
|
||||
|
@ -46,25 +46,26 @@ void main() {
|
||||
);
|
||||
|
||||
// each artboard adds file asset referencer.
|
||||
var count = 20;
|
||||
var count = 19;
|
||||
while (count-- > 0) {
|
||||
riveFile.artboards.first.instance();
|
||||
}
|
||||
|
||||
final image = riveFile.artboards.first.component<Image>(assetName);
|
||||
final asset = image!.asset!;
|
||||
expect(asset.fileAssetReferencers.length, 21);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
expect(asset.fileAssetReferencers.length, 20);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
// ok, kinda lame, but the above allows garbage collection to kick in
|
||||
// which will remove referencers, its not really deterministic though
|
||||
|
||||
expect(
|
||||
asset.fileAssetReferencers.length < 5,
|
||||
true,
|
||||
reason: "Expected ${asset.fileAssetReferencers.length} < 5",
|
||||
);
|
||||
},
|
||||
// skipping because it only works when we run this test directly,
|
||||
// not when running it as part of other tests.
|
||||
// skipping because it does not work, you can see things get
|
||||
// finalized but this does not consistently happen in tests.
|
||||
skip: true,
|
||||
);
|
||||
});
|
||||
|
@ -35,22 +35,6 @@ void main() {
|
||||
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 {
|
||||
final mockHttpClient = getMockHttpClient();
|
||||
|
||||
@ -59,7 +43,6 @@ void main() {
|
||||
runZonedGuarded(() {
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
loadEmbeddedAssets: false,
|
||||
loadCdnAssets: false,
|
||||
);
|
||||
}, (error, stack) {
|
||||
@ -76,16 +59,18 @@ void main() {
|
||||
verifyNever(() => mockHttpClient.openUrl(any(), any()));
|
||||
// by default we try to check for assets
|
||||
});
|
||||
|
||||
test('test importing rive file, make sure we get a good callback',
|
||||
() async {
|
||||
// lets just return an image
|
||||
final riveBytes = loadFile('assets/sample_image.riv');
|
||||
final imageBytes = loadFile('assets/file.png');
|
||||
final parameters = [];
|
||||
RiveFile.import(riveBytes, loadEmbeddedAssets: false,
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
parameters.add(asset);
|
||||
final assets = [];
|
||||
final byteList = [];
|
||||
RiveFile.import(riveBytes, assetLoader: CallbackAssetLoader(
|
||||
(asset, bytes) async {
|
||||
assets.add(asset);
|
||||
byteList.add(bytes);
|
||||
await asset.decode(Uint8List.sublistView(
|
||||
imageBytes,
|
||||
));
|
||||
@ -93,7 +78,7 @@ void main() {
|
||||
},
|
||||
));
|
||||
|
||||
final asset = parameters.first;
|
||||
final asset = assets.first;
|
||||
|
||||
expect(asset is ImageAsset, true);
|
||||
final fileAsset = asset as ImageAsset;
|
||||
@ -101,7 +86,38 @@ void main() {
|
||||
expect(fileAsset.type, Type.image);
|
||||
expect(fileAsset.name, assetName);
|
||||
expect(fileAsset.assetId, 42981);
|
||||
|
||||
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',
|
||||
@ -113,9 +129,8 @@ void main() {
|
||||
|
||||
final file = RiveFile.import(
|
||||
riveBytes,
|
||||
loadEmbeddedAssets: false,
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
(asset, bytes) async {
|
||||
await asset.decode(Uint8List.sublistView(
|
||||
imageBytes,
|
||||
));
|
||||
@ -159,30 +174,12 @@ void main() {
|
||||
)).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 {
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
loadEmbeddedAssets: false,
|
||||
loadCdnAssets: false,
|
||||
);
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
@ -199,10 +196,8 @@ void main() {
|
||||
final parameters = [];
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
|
||||
RiveFile.import(riveBytes, loadEmbeddedAssets: false,
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
RiveFile.import(riveBytes, assetLoader: CallbackAssetLoader(
|
||||
(asset, bytes) async {
|
||||
parameters.add(asset);
|
||||
await asset.decode(Uint8List.sublistView(
|
||||
imageBytes,
|
||||
@ -226,9 +221,8 @@ void main() {
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
|
||||
RiveFile.import(riveBytes, loadEmbeddedAssets: false,
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
RiveFile.import(riveBytes, assetLoader: CallbackAssetLoader(
|
||||
(asset, bytes) async {
|
||||
parameters.add(asset);
|
||||
return false;
|
||||
},
|
||||
|
Reference in New Issue
Block a user