mirror of
https://github.com/rive-app/rive-flutter
synced 2025-06-22 01:55:59 +00:00
Font dart runtime
There are a number of questions I'd like to resolve before considering to merge this. also a few thigns to clean up no doubt # Questions before merging To customize loading out of band assets, we expect our users to implement ``` abstract class FileAssetLoader { Future<bool> load(FileAsset asset); bool isCompatible(FileAsset asset) => true; } ``` 1. is this a good interface (i've changed `loadContents`, to `load`)? (if we like this we should change this in cpp too) 2. `FileAssetLoader` is a mouth-full, would `AssetLoader` be better? 3. We are passing `FileAssets` (our core objects) to users with some slight api extensions, is that good? should we just wrap this in an Asset class (I had this before, its not a lot of work to get it back) things sorted - [x] cdn "loading" vs url loading - just sticking with cdn, users can customize for url - [x] asset class for consumers of our runtime. - i've avoided this one for now, just extending our FileAsset - [x] Importer/Resolver/Loader. I flipped some names around, mostly because I want our end users to provide an `AssetLoader`, not a resolver. things to sort out down the line, i'm declaring them out of scope for this pr, fft disagree: - Fallback font, I see we have a fallback font file hardcoded in the runtime. should investigate if we can include an asset for people like this, or if we need to have users set this if they want to use fallback fonts. - Image Placement, we should strip width/height from runtimes on ImageAssets - What do we want to do about asset loading / decoding errors - TextStyle has both assetId & fontAssetId it gets assetId from the file asset referencer, its nbd, Diffs= 06e959ad2 Font dart runtime (#5411) Co-authored-by: Maxwell Talbot <talbot.maxwell@gmail.com>
This commit is contained in:
@ -1 +1 @@
|
||||
9007b7f92943caf8ae4ba90ba198bfabca5f2b60
|
||||
06e959ad21ead2df3d477652f1c208a7ea6e959d
|
||||
|
17
.vscode/launch.json
vendored
Normal file
17
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Rive Flutter example",
|
||||
"program": "example/lib/main.dart",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
BIN
example/assets/asset.riv
Normal file
BIN
example/assets/asset.riv
Normal file
Binary file not shown.
BIN
example/assets/embedded_text.riv
Normal file
BIN
example/assets/embedded_text.riv
Normal file
Binary file not shown.
BIN
example/assets/fonts/Inter-Regular.ttf
Normal file
BIN
example/assets/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
example/assets/sampleimage.riv
Normal file
BIN
example/assets/sampleimage.riv
Normal file
Binary file not shown.
BIN
example/assets/sampletext.riv
Normal file
BIN
example/assets/sampletext.riv
Normal file
Binary file not shown.
95
example/lib/custom_asset_loading.dart
Normal file
95
example/lib/custom_asset_loading.dart
Normal file
@ -0,0 +1,95 @@
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
/// An example showing how to load image or font assets dynamically
|
||||
class CustomAssetLoading extends StatefulWidget {
|
||||
const CustomAssetLoading({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<CustomAssetLoading> createState() => _CustomAssetLoadingState();
|
||||
}
|
||||
|
||||
class _CustomAssetLoadingState extends State<CustomAssetLoading> {
|
||||
var _index = 0;
|
||||
void next() {
|
||||
setState(() {
|
||||
_index += 1;
|
||||
});
|
||||
}
|
||||
|
||||
void previous() {
|
||||
setState(() {
|
||||
_index -= 1;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Custom Asset Loading'),
|
||||
),
|
||||
body: Center(
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: previous,
|
||||
child: const Icon(Icons.arrow_back),
|
||||
),
|
||||
Expanded(
|
||||
child: (_index % 2 == 0)
|
||||
? RiveAnimation.asset(
|
||||
'assets/asset.riv',
|
||||
fit: BoxFit.cover,
|
||||
importEmbeddedAssets: false,
|
||||
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;
|
||||
},
|
||||
),
|
||||
)
|
||||
: RiveAnimation.asset(
|
||||
'assets/sampletext.riv',
|
||||
fit: BoxFit.cover,
|
||||
importEmbeddedAssets: false,
|
||||
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',
|
||||
];
|
||||
|
||||
final res = await http.get(Uri.parse(
|
||||
urls[Random().nextInt(
|
||||
urls.length,
|
||||
)],
|
||||
));
|
||||
await asset
|
||||
.decode(Uint8List.view(res.bodyBytes.buffer));
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: next,
|
||||
child: const Icon(Icons.arrow_forward),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rive_example/custom_asset_loading.dart';
|
||||
|
||||
import 'package:rive_example/carousel.dart';
|
||||
import 'package:rive_example/custom_controller.dart';
|
||||
import 'package:rive_example/example_state_machine.dart';
|
||||
@ -51,6 +53,7 @@ class _RiveExampleAppState extends State<RiveExampleApp> {
|
||||
const _Page('State Machine with Listener', StateMachineListener()),
|
||||
const _Page('Skinning Demo', SkinningDemo()),
|
||||
const _Page('Animation Carousel', AnimationCarousel()),
|
||||
const _Page('Custom Asset Loading', CustomAssetLoading()),
|
||||
];
|
||||
|
||||
@override
|
||||
|
@ -15,8 +15,8 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
rive_common: fab8476ce8352bf54152a913f393a8696d3dc98c
|
||||
rive_common: acedcab7802c0ece4b0d838b71d7deb637e1309a
|
||||
|
||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||
|
||||
COCOAPODS: 1.12.0
|
||||
COCOAPODS: 1.12.1
|
||||
|
@ -13,6 +13,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
rive:
|
||||
path: ../
|
||||
http:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -24,3 +25,4 @@ flutter:
|
||||
|
||||
assets:
|
||||
- assets/
|
||||
- assets/fonts/
|
||||
|
@ -1,5 +1,7 @@
|
||||
library rive;
|
||||
|
||||
export 'package:rive/src/asset.dart';
|
||||
export 'package:rive/src/asset_loader.dart';
|
||||
export 'package:rive/src/controllers/one_shot_controller.dart';
|
||||
export 'package:rive/src/controllers/simple_controller.dart';
|
||||
export 'package:rive/src/controllers/state_machine_controller.dart';
|
||||
|
54
lib/src/asset.dart
Normal file
54
lib/src/asset.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'package:rive/src/rive_core/assets/file_asset.dart';
|
||||
|
||||
export 'package:rive/src/generated/artboard_base.dart';
|
||||
|
||||
/// TODO: do we prefer this, or do we want to wrap our FileAssets
|
||||
/// into a custom asset class.
|
||||
extension FileAssetExtension on FileAsset {
|
||||
Extension get extension => _getExtension(fileExtension);
|
||||
Type get type => _getType(fileExtension);
|
||||
}
|
||||
|
||||
Extension _getExtension(String extension) {
|
||||
switch (extension) {
|
||||
case 'png':
|
||||
return Extension.png;
|
||||
case 'jpeg':
|
||||
return Extension.jpeg;
|
||||
case 'webp':
|
||||
return Extension.webp;
|
||||
case 'otf':
|
||||
return Extension.otf;
|
||||
case 'ttf':
|
||||
return Extension.ttf;
|
||||
}
|
||||
return Extension.unknown;
|
||||
}
|
||||
|
||||
Type _getType(String extension) {
|
||||
switch (extension) {
|
||||
case 'png':
|
||||
case 'jpeg':
|
||||
case 'webp':
|
||||
return Type.image;
|
||||
case 'otf':
|
||||
case 'ttf':
|
||||
return Type.font;
|
||||
}
|
||||
return Type.unknown;
|
||||
}
|
||||
|
||||
enum Extension {
|
||||
otf,
|
||||
ttf,
|
||||
jpeg,
|
||||
png,
|
||||
webp,
|
||||
unknown,
|
||||
}
|
||||
|
||||
enum Type {
|
||||
font,
|
||||
image,
|
||||
unknown,
|
||||
}
|
101
lib/src/asset_loader.dart
Normal file
101
lib/src/asset_loader.dart
Normal file
@ -0,0 +1,101 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:rive/src/asset.dart';
|
||||
import 'package:rive/src/rive_core/assets/file_asset.dart';
|
||||
import 'package:rive/src/utilities/utilities.dart';
|
||||
|
||||
import 'core/importers/file_asset_importer.dart';
|
||||
|
||||
class CDNAssetLoader extends FileAssetLoader {
|
||||
final String baseUrl;
|
||||
CDNAssetLoader(this.baseUrl);
|
||||
|
||||
@override
|
||||
bool isCompatible(FileAsset asset) => asset.cdnUuid.isNotEmpty;
|
||||
|
||||
@override
|
||||
Future<bool> load(FileAsset asset) async {
|
||||
// do we have a url builder, dart seems to suck a bit for this.
|
||||
var url = baseUrl;
|
||||
if (!baseUrl.endsWith('/')) {
|
||||
url += '/';
|
||||
}
|
||||
url += formatUuid(
|
||||
uuidVariant2(asset.cdnUuid),
|
||||
);
|
||||
|
||||
final res = await http.get(Uri.parse(url));
|
||||
await asset.decode(
|
||||
Uint8List.view(res.bodyBytes.buffer),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class LocalAssetLoader extends FileAssetLoader {
|
||||
final String fontPath;
|
||||
final String imagePath;
|
||||
final AssetBundle _assetBundle;
|
||||
|
||||
LocalAssetLoader({
|
||||
required this.fontPath,
|
||||
required this.imagePath,
|
||||
AssetBundle? assetBundle,
|
||||
}) : _assetBundle = assetBundle ?? rootBundle;
|
||||
|
||||
@override
|
||||
Future<bool> load(FileAsset asset) async {
|
||||
String? assetPath;
|
||||
switch (asset.type) {
|
||||
case Type.unknown:
|
||||
return false;
|
||||
case Type.image:
|
||||
assetPath = imagePath + asset.name;
|
||||
break;
|
||||
case Type.font:
|
||||
assetPath = fontPath + asset.name;
|
||||
break;
|
||||
}
|
||||
|
||||
final bytes = await _assetBundle.load(assetPath);
|
||||
await asset.decode(Uint8List.view(bytes.buffer));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class CallbackAssetLoader extends FileAssetLoader {
|
||||
Future<bool> Function(FileAsset) callback;
|
||||
|
||||
CallbackAssetLoader(this.callback);
|
||||
|
||||
@override
|
||||
Future<bool> load(FileAsset asset) async {
|
||||
return callback(asset);
|
||||
}
|
||||
}
|
||||
|
||||
// Just a thought, can load assets from a few sources
|
||||
// maybe pointless tbh, if people have a path here, they should sort it out...
|
||||
// might be helpful users have a few different setups & simple want to
|
||||
// be able to copy pasta their asset loading setup.
|
||||
class FallbackAssetLoader extends FileAssetLoader {
|
||||
// by default, unless the data was inline we check locally & then on our cdn
|
||||
final List<FileAssetLoader> fileAssetLoaders;
|
||||
|
||||
FallbackAssetLoader(this.fileAssetLoaders);
|
||||
|
||||
@override
|
||||
Future<bool> load(FileAsset asset) async {
|
||||
for (var i = 0; i < fileAssetLoaders.length; i++) {
|
||||
final resolver = fileAssetLoaders[i];
|
||||
if (!resolver.isCompatible(asset)) {
|
||||
continue;
|
||||
}
|
||||
final success = await resolver.load(asset);
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -2,31 +2,52 @@ import 'package:rive/src/core/core.dart';
|
||||
import 'package:rive/src/rive_core/assets/file_asset.dart';
|
||||
import 'package:rive/src/rive_core/assets/file_asset_contents.dart';
|
||||
|
||||
/// A helper for resolving Rive file assets (like images) that are provided out
|
||||
/// of band with regards to the .riv file itself.
|
||||
/// QUESTION: renamed this to loader & importer
|
||||
/// we use the "importer" names elsewhere
|
||||
///
|
||||
/// but IMO, the function names are pretty telling,
|
||||
/// now the loader has "loadContents" & the resolver has "resolve"
|
||||
/// before this was backwards.
|
||||
/// (
|
||||
/// also as part of our import process, we import components,
|
||||
/// some components have additional *resolvers* added to the stack, which
|
||||
/// get ... resolved... and when components mention assets, those assets get
|
||||
/// loaded
|
||||
/// )
|
||||
|
||||
// ignore: one_member_abstracts
|
||||
abstract class FileAssetResolver {
|
||||
Future<Uint8List> loadContents(FileAsset asset);
|
||||
abstract class FileAssetLoader {
|
||||
Future<bool> load(FileAsset asset);
|
||||
bool isCompatible(FileAsset asset) => true;
|
||||
}
|
||||
|
||||
// this should be the resolver.
|
||||
class FileAssetImporter extends ImportStackObject {
|
||||
final FileAssetResolver? assetResolver;
|
||||
final FileAssetLoader? assetLoader;
|
||||
final FileAsset fileAsset;
|
||||
final bool importEmbeddedAssets;
|
||||
|
||||
FileAssetImporter(this.fileAsset, this.assetResolver);
|
||||
FileAssetImporter(
|
||||
this.fileAsset,
|
||||
this.assetLoader, {
|
||||
this.importEmbeddedAssets = true,
|
||||
});
|
||||
|
||||
bool _loadedContents = false;
|
||||
bool _contentsResolved = false;
|
||||
|
||||
void loadContents(FileAssetContents contents) {
|
||||
_loadedContents = true;
|
||||
fileAsset.decode(contents.bytes);
|
||||
// awkward name
|
||||
void resolveContents(FileAssetContents contents) {
|
||||
if (importEmbeddedAssets) {
|
||||
_contentsResolved = true;
|
||||
fileAsset.decode(contents.bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool resolve() {
|
||||
if (!_loadedContents) {
|
||||
if (!_contentsResolved) {
|
||||
// try to get them out of band
|
||||
assetResolver?.loadContents(fileAsset).then(fileAsset.decode);
|
||||
assetLoader?.load(fileAsset);
|
||||
}
|
||||
return super.resolve();
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
/// Core automatically generated lib/src/generated/assets/file_asset_base.dart.
|
||||
/// Do not modify manually.
|
||||
|
||||
import 'package:rive/src/core/core.dart';
|
||||
import 'package:rive/src/rive_core/assets/asset.dart';
|
||||
|
||||
abstract class FileAssetBase extends Asset {
|
||||
@ -34,9 +35,34 @@ abstract class FileAssetBase extends Asset {
|
||||
|
||||
void assetIdChanged(int from, int to);
|
||||
|
||||
/// --------------------------------------------------------------------------
|
||||
/// CdnUuid field with key 359.
|
||||
static final Uint8List cdnUuidInitialValue = Uint8List(0);
|
||||
Uint8List _cdnUuid = cdnUuidInitialValue;
|
||||
static const int cdnUuidPropertyKey = 359;
|
||||
|
||||
/// The cdn uuid if it exists
|
||||
Uint8List get cdnUuid => _cdnUuid;
|
||||
|
||||
/// Change the [_cdnUuid] field value.
|
||||
/// [cdnUuidChanged] will be invoked only if the field's value has changed.
|
||||
set cdnUuid(Uint8List value) {
|
||||
if (listEquals(_cdnUuid, value)) {
|
||||
return;
|
||||
}
|
||||
Uint8List from = _cdnUuid;
|
||||
_cdnUuid = value;
|
||||
if (hasValidated) {
|
||||
cdnUuidChanged(from, value);
|
||||
}
|
||||
}
|
||||
|
||||
void cdnUuidChanged(Uint8List from, Uint8List to);
|
||||
|
||||
@override
|
||||
void copy(covariant FileAssetBase source) {
|
||||
super.copy(source);
|
||||
_assetId = source._assetId;
|
||||
_cdnUuid = source._cdnUuid;
|
||||
}
|
||||
}
|
||||
|
@ -1494,6 +1494,11 @@ class RiveCoreContext {
|
||||
object.assetId = value;
|
||||
}
|
||||
break;
|
||||
case FileAssetBase.cdnUuidPropertyKey:
|
||||
if (object is FileAssetBase && value is Uint8List) {
|
||||
object.cdnUuid = value;
|
||||
}
|
||||
break;
|
||||
case DrawableAssetBase.heightPropertyKey:
|
||||
if (object is DrawableAssetBase && value is double) {
|
||||
object.height = value;
|
||||
@ -1757,6 +1762,7 @@ class RiveCoreContext {
|
||||
case GradientStopBase.colorValuePropertyKey:
|
||||
return colorType;
|
||||
case MeshBase.triangleIndexBytesPropertyKey:
|
||||
case FileAssetBase.cdnUuidPropertyKey:
|
||||
case FileAssetContentsBase.bytesPropertyKey:
|
||||
return bytesType;
|
||||
default:
|
||||
@ -2260,6 +2266,8 @@ class RiveCoreContext {
|
||||
switch (propertyKey) {
|
||||
case MeshBase.triangleIndexBytesPropertyKey:
|
||||
return (object as MeshBase).triangleIndexBytes;
|
||||
case FileAssetBase.cdnUuidPropertyKey:
|
||||
return (object as FileAssetBase).cdnUuid;
|
||||
case FileAssetContentsBase.bytesPropertyKey:
|
||||
return (object as FileAssetContentsBase).bytes;
|
||||
}
|
||||
@ -3453,6 +3461,11 @@ class RiveCoreContext {
|
||||
object.triangleIndexBytes = value;
|
||||
}
|
||||
break;
|
||||
case FileAssetBase.cdnUuidPropertyKey:
|
||||
if (object is FileAssetBase) {
|
||||
object.cdnUuid = value;
|
||||
}
|
||||
break;
|
||||
case FileAssetContentsBase.bytesPropertyKey:
|
||||
if (object is FileAssetContentsBase) {
|
||||
object.bytes = value;
|
||||
|
@ -8,6 +8,9 @@ abstract class FileAsset extends FileAssetBase {
|
||||
@override
|
||||
void assetIdChanged(int from, int to) {}
|
||||
|
||||
@override
|
||||
void cdnUuidChanged(Uint8List from, Uint8List to) {}
|
||||
|
||||
Future<void> decode(Uint8List bytes);
|
||||
|
||||
@override
|
||||
|
@ -22,12 +22,11 @@ class FileAssetContents extends FileAssetContentsBase {
|
||||
|
||||
@override
|
||||
bool import(ImportStack stack) {
|
||||
var fileAssetImporter =
|
||||
stack.latest<FileAssetImporter>(FileAssetBase.typeKey);
|
||||
if (fileAssetImporter == null) {
|
||||
var resolver = stack.latest<FileAssetImporter>(FileAssetBase.typeKey);
|
||||
if (resolver == null) {
|
||||
return false;
|
||||
}
|
||||
fileAssetImporter.loadContents(this);
|
||||
resolver.resolveContents(this);
|
||||
|
||||
return super.import(stack);
|
||||
}
|
||||
|
@ -172,7 +172,6 @@ class Text extends TextBase with TextStyleContainer {
|
||||
_syncRuns();
|
||||
}
|
||||
|
||||
|
||||
final Size _size = Size.zero;
|
||||
|
||||
static const double paragraphSpacing = 20;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:rive/src/core/core.dart';
|
||||
import 'package:rive/src/generated/text/text_style_base.dart';
|
||||
import 'package:rive/src/rive_core/artboard.dart';
|
||||
import 'package:rive/src/rive_core/assets/file_asset.dart';
|
||||
@ -142,6 +143,12 @@ class TextStyle extends TextStyleBase
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void copy(covariant TextStyle source) {
|
||||
super.copy(source);
|
||||
asset = source.asset;
|
||||
}
|
||||
|
||||
@override
|
||||
void fontAssetIdChanged(int from, int to) {
|
||||
asset = context.resolve(to);
|
||||
@ -149,12 +156,6 @@ class TextStyle extends TextStyleBase
|
||||
|
||||
void _fontDecoded() => _markShapeDirty();
|
||||
|
||||
@override
|
||||
void onAddedDirty() {
|
||||
super.onAddedDirty();
|
||||
asset = context.resolve(fontAssetId);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDirty(int mask) {
|
||||
super.onDirty(mask);
|
||||
@ -296,4 +297,12 @@ class TextStyle extends TextStyleBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool import(ImportStack stack) {
|
||||
if (!registerWithImporter(stack)) {
|
||||
return false;
|
||||
}
|
||||
return super.import(stack);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'dart:collection';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:rive/src/asset_loader.dart';
|
||||
import 'package:rive/src/core/core.dart';
|
||||
import 'package:rive/src/core/field_types/core_field_type.dart';
|
||||
import 'package:rive/src/generated/animation/animation_state_base.dart';
|
||||
@ -10,6 +11,7 @@ import 'package:rive/src/generated/animation/any_state_base.dart';
|
||||
import 'package:rive/src/generated/animation/blend_state_transition_base.dart';
|
||||
import 'package:rive/src/generated/animation/entry_state_base.dart';
|
||||
import 'package:rive/src/generated/animation/exit_state_base.dart';
|
||||
import 'package:rive/src/generated/assets/font_asset_base.dart';
|
||||
import 'package:rive/src/generated/nested_artboard_base.dart';
|
||||
import 'package:rive/src/local_file_io.dart'
|
||||
if (dart.library.html) 'package:rive/src/local_file_web.dart';
|
||||
@ -26,12 +28,14 @@ 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';
|
||||
import 'package:rive/src/rive_core/runtime/exceptions/rive_format_error_exception.dart';
|
||||
import 'package:rive/src/rive_core/runtime/runtime_header.dart';
|
||||
import 'package:rive/src/runtime_nested_artboard.dart';
|
||||
|
||||
import 'package:rive_common/utilities.dart';
|
||||
|
||||
Core<CoreContext>? _readRuntimeObject(
|
||||
@ -85,13 +89,14 @@ class RiveFile {
|
||||
|
||||
Backboard _backboard = Backboard.unknown;
|
||||
final _artboards = <Artboard>[];
|
||||
final FileAssetResolver? _assetResolver;
|
||||
final FileAssetLoader? _assetLoader;
|
||||
|
||||
RiveFile._(
|
||||
BinaryReader reader,
|
||||
this.header,
|
||||
this._assetResolver,
|
||||
) {
|
||||
this._assetLoader, {
|
||||
bool importEmbeddedAssets = true,
|
||||
}) {
|
||||
/// Property fields table of contents
|
||||
final propertyToField = HashMap<int, CoreFieldType>();
|
||||
|
||||
@ -125,6 +130,12 @@ class RiveFile {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// two options, either tell the fileAssetImporter,
|
||||
// or simply skip the object. i think we should skip the object.
|
||||
if (!importEmbeddedAssets && object is FileAssetContentsBase) {
|
||||
// suppress importing embedded assets
|
||||
continue;
|
||||
}
|
||||
|
||||
ImportStackObject? stackObject;
|
||||
var stackType = object.coreType;
|
||||
@ -187,7 +198,13 @@ class RiveFile {
|
||||
break;
|
||||
}
|
||||
case ImageAssetBase.typeKey:
|
||||
stackObject = FileAssetImporter(object as FileAsset, _assetResolver);
|
||||
case FontAssetBase.typeKey:
|
||||
// all these stack objects are resolvers. they get resolved.
|
||||
stackObject = FileAssetImporter(
|
||||
object as FileAsset,
|
||||
_assetLoader,
|
||||
importEmbeddedAssets: importEmbeddedAssets,
|
||||
);
|
||||
stackType = FileAssetBase.typeKey;
|
||||
break;
|
||||
default:
|
||||
@ -250,42 +267,80 @@ class RiveFile {
|
||||
/// [RiveUnsupportedVersionException] if the version is not supported.
|
||||
factory RiveFile.import(
|
||||
ByteData bytes, {
|
||||
FileAssetResolver? assetResolver,
|
||||
FileAssetLoader? assetLoader,
|
||||
bool cdn = true,
|
||||
bool importEmbeddedAssets = true,
|
||||
}) {
|
||||
var reader = BinaryReader(bytes);
|
||||
return RiveFile._(reader, RuntimeHeader.read(reader), assetResolver);
|
||||
return RiveFile._(
|
||||
reader,
|
||||
RuntimeHeader.read(reader),
|
||||
FallbackAssetLoader(
|
||||
[
|
||||
if (assetLoader != null) assetLoader,
|
||||
if (cdn) CDNAssetLoader('https://public.rive.app/cdn/uuid'),
|
||||
],
|
||||
),
|
||||
importEmbeddedAssets: importEmbeddedAssets,
|
||||
);
|
||||
}
|
||||
|
||||
/// Imports a Rive file from an asset bundle. Provide [basePath] if any nested
|
||||
/// Rive asset isn't in the same path as the [bundleKey].
|
||||
static Future<RiveFile> asset(String bundleKey, {String? basePath}) async {
|
||||
final bytes = await rootBundle.load(bundleKey);
|
||||
if (basePath == null) {
|
||||
int index = bundleKey.lastIndexOf('/');
|
||||
if (index != -1) {
|
||||
basePath = bundleKey.substring(0, index + 1);
|
||||
} else {
|
||||
// ignore: parameter_assignments
|
||||
basePath = '';
|
||||
}
|
||||
}
|
||||
return RiveFile.import(bytes, assetResolver: _LocalAssetResolver(basePath));
|
||||
/// Imports a Rive file from an asset bundle.
|
||||
/// By default we will look for out of bound assets next to the [bundleKey] & check
|
||||
/// Rive's CDN for content.
|
||||
static Future<RiveFile> asset(
|
||||
String bundleKey, {
|
||||
FileAssetLoader? assetLoader,
|
||||
bool cdn = true,
|
||||
bool importEmbeddedAssets = true,
|
||||
AssetBundle? bundle,
|
||||
}) async {
|
||||
final bytes = await (bundle ?? rootBundle).load(
|
||||
bundleKey,
|
||||
);
|
||||
|
||||
return RiveFile.import(
|
||||
bytes,
|
||||
assetLoader: assetLoader,
|
||||
cdn: cdn,
|
||||
importEmbeddedAssets: importEmbeddedAssets,
|
||||
);
|
||||
}
|
||||
|
||||
/// Imports a Rive file from a URL over HTTP. Provide an [assetResolver] if
|
||||
/// your file contains images that needed to be loaded with separate network
|
||||
/// requests.
|
||||
static Future<RiveFile> network(String url,
|
||||
{FileAssetResolver? assetResolver, Map<String, String>? headers}) async {
|
||||
static Future<RiveFile> network(
|
||||
String url, {
|
||||
FileAssetLoader? assetLoader,
|
||||
Map<String, String>? headers,
|
||||
bool cdn = true,
|
||||
bool importEmbeddedAssets = true,
|
||||
}) async {
|
||||
final res = await http.get(Uri.parse(url), headers: headers);
|
||||
final bytes = ByteData.view(res.bodyBytes.buffer);
|
||||
return RiveFile.import(bytes, assetResolver: assetResolver);
|
||||
return RiveFile.import(
|
||||
bytes,
|
||||
assetLoader: assetLoader,
|
||||
cdn: cdn,
|
||||
importEmbeddedAssets: importEmbeddedAssets,
|
||||
);
|
||||
}
|
||||
|
||||
/// Imports a Rive file from local folder
|
||||
static Future<RiveFile> file(String path) async {
|
||||
static Future<RiveFile> file(
|
||||
String path, {
|
||||
FileAssetLoader? assetLoader,
|
||||
bool cdn = true,
|
||||
bool importEmbeddedAssets = true,
|
||||
}) async {
|
||||
final bytes = await localFileBytes(path);
|
||||
return RiveFile.import(ByteData.view(bytes!.buffer));
|
||||
return RiveFile.import(
|
||||
ByteData.view(bytes!.buffer),
|
||||
assetLoader: assetLoader,
|
||||
importEmbeddedAssets: importEmbeddedAssets,
|
||||
cdn: cdn,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns all artboards in the file
|
||||
@ -299,25 +354,3 @@ class RiveFile {
|
||||
Artboard? artboardByName(String name) =>
|
||||
_artboards.firstWhereOrNull((a) => a.name == name);
|
||||
}
|
||||
|
||||
/// Resolves a Rive asset from the network provided a [baseUrl].
|
||||
class NetworkAssetResolver extends FileAssetResolver {
|
||||
final String baseUrl;
|
||||
NetworkAssetResolver(this.baseUrl);
|
||||
|
||||
@override
|
||||
Future<Uint8List> loadContents(FileAsset asset) async {
|
||||
final res = await http.get(Uri.parse(baseUrl + asset.uniqueFilename));
|
||||
return Uint8List.view(res.bodyBytes.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class _LocalAssetResolver extends FileAssetResolver {
|
||||
String basePath;
|
||||
_LocalAssetResolver(this.basePath);
|
||||
@override
|
||||
Future<Uint8List> loadContents(FileAsset asset) async {
|
||||
final bytes = await rootBundle.load(basePath + asset.uniqueFilename);
|
||||
return Uint8List.view(bytes.buffer);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// Szudzik's function for hashing two ints together
|
||||
int szudzik(int a, int b) {
|
||||
// a and b must be >= 0
|
||||
@ -5,3 +7,58 @@ int szudzik(int a, int b) {
|
||||
int y = b.abs();
|
||||
return x >= y ? x * x + x + y : x + y * y;
|
||||
}
|
||||
|
||||
String byteToHex(int byte) {
|
||||
return byte.toRadixString(16).padLeft(2, '0');
|
||||
}
|
||||
|
||||
/// Adapted from:
|
||||
/// https://github.com/daegalus/dart-uuid/blob/main/lib/parsing.dart
|
||||
///
|
||||
/// Unparses a [buffer] of bytes and outputs a proper UUID string.
|
||||
///
|
||||
/// Throws a [RangeError] exception if the [buffer] is not large enough to
|
||||
/// hold the bytes.
|
||||
String formatUuid(Uint8List buffer) {
|
||||
if (buffer.length < 16) {
|
||||
throw RangeError('buffer too small: need 16: length=${buffer.length}');
|
||||
}
|
||||
var i = 0;
|
||||
return '${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}'
|
||||
'${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}'
|
||||
'-'
|
||||
'${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}'
|
||||
'-'
|
||||
'${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}'
|
||||
'-'
|
||||
'${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}'
|
||||
'-'
|
||||
'${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}'
|
||||
'${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}'
|
||||
'${byteToHex(buffer[i++])}${byteToHex(buffer[i++])}';
|
||||
}
|
||||
|
||||
Uint8List uuidVariant2(Uint8List uuidBuffer) {
|
||||
return Uint8List.fromList([
|
||||
uuidBuffer[3],
|
||||
uuidBuffer[2],
|
||||
uuidBuffer[1],
|
||||
uuidBuffer[0],
|
||||
// -
|
||||
uuidBuffer[5],
|
||||
uuidBuffer[4],
|
||||
// -
|
||||
uuidBuffer[7],
|
||||
uuidBuffer[6],
|
||||
// -
|
||||
uuidBuffer[9],
|
||||
uuidBuffer[8],
|
||||
// -
|
||||
uuidBuffer[15],
|
||||
uuidBuffer[14],
|
||||
uuidBuffer[13],
|
||||
uuidBuffer[12],
|
||||
uuidBuffer[11],
|
||||
uuidBuffer[10],
|
||||
]);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
import 'package:rive/src/core/importers/file_asset_importer.dart';
|
||||
import 'package:rive_common/math.dart';
|
||||
|
||||
/// Specifies whether a source is from an asset bundle or http
|
||||
@ -58,6 +59,14 @@ class RiveAnimation extends StatefulWidget {
|
||||
/// Headers for network requests
|
||||
final Map<String, String>? headers;
|
||||
|
||||
/// Setting to tell our rive file that it should load embedded assets
|
||||
/// disable this customize assets even when embedded
|
||||
final bool? importEmbeddedAssets;
|
||||
|
||||
/// Specify an assetLoader explicitley, leave this blank to let rive
|
||||
/// chose how to load an asset itself.
|
||||
final FileAssetLoader? assetLoader;
|
||||
|
||||
/// Creates a new [RiveAnimation] from an asset bundle.
|
||||
///
|
||||
/// *Example:*
|
||||
@ -75,6 +84,8 @@ class RiveAnimation extends StatefulWidget {
|
||||
this.antialiasing = true,
|
||||
this.controllers = const [],
|
||||
this.onInit,
|
||||
this.importEmbeddedAssets = true,
|
||||
this.assetLoader,
|
||||
Key? key,
|
||||
}) : name = asset,
|
||||
file = null,
|
||||
@ -100,6 +111,8 @@ class RiveAnimation extends StatefulWidget {
|
||||
this.controllers = const [],
|
||||
this.onInit,
|
||||
this.headers,
|
||||
this.importEmbeddedAssets = true,
|
||||
this.assetLoader,
|
||||
Key? key,
|
||||
}) : name = url,
|
||||
file = null,
|
||||
@ -123,7 +136,9 @@ class RiveAnimation extends StatefulWidget {
|
||||
this.antialiasing = true,
|
||||
this.controllers = const [],
|
||||
this.onInit,
|
||||
this.importEmbeddedAssets = true,
|
||||
Key? key,
|
||||
this.assetLoader,
|
||||
}) : name = path,
|
||||
file = null,
|
||||
headers = null,
|
||||
@ -152,6 +167,8 @@ class RiveAnimation extends StatefulWidget {
|
||||
Key? key,
|
||||
}) : name = null,
|
||||
headers = null,
|
||||
importEmbeddedAssets = null,
|
||||
assetLoader = null,
|
||||
src = _Source.direct,
|
||||
super(key: key);
|
||||
|
||||
@ -187,13 +204,28 @@ class RiveAnimationState extends State<RiveAnimation> {
|
||||
Future<RiveFile> _loadRiveFile() {
|
||||
switch (widget.src) {
|
||||
case _Source.asset:
|
||||
return RiveFile.asset(widget.name!);
|
||||
return RiveFile.asset(
|
||||
widget.name!,
|
||||
importEmbeddedAssets: widget.importEmbeddedAssets!,
|
||||
assetLoader: widget.assetLoader,
|
||||
);
|
||||
case _Source.network:
|
||||
return RiveFile.network(widget.name!, headers: widget.headers);
|
||||
return RiveFile.network(
|
||||
widget.name!,
|
||||
headers: widget.headers,
|
||||
importEmbeddedAssets: widget.importEmbeddedAssets!,
|
||||
assetLoader: widget.assetLoader,
|
||||
);
|
||||
case _Source.file:
|
||||
return RiveFile.file(widget.name!);
|
||||
return RiveFile.file(
|
||||
widget.name!,
|
||||
importEmbeddedAssets: widget.importEmbeddedAssets!,
|
||||
assetLoader: widget.assetLoader,
|
||||
);
|
||||
case _Source.direct:
|
||||
return Future.value(widget.file!);
|
||||
return Future.value(
|
||||
widget.file!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
220
test/asset_test.dart
Normal file
220
test/asset_test.dart
Normal file
@ -0,0 +1,220 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
import 'package:rive/src/asset_loader.dart';
|
||||
import 'package:rive/src/rive_core/assets/image_asset.dart';
|
||||
|
||||
import 'mocks/mocks.dart';
|
||||
import 'src/network.dart';
|
||||
import 'src/utils.dart';
|
||||
|
||||
const assetName = 'CleanShot 2023-06-08 at 08.51.19@2x.png';
|
||||
|
||||
class MockAssetBundle extends Mock implements AssetBundle {}
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(ArtboardFake());
|
||||
registerFallbackValue(Uri());
|
||||
registerFallbackValue(Stream.value(<int>[]));
|
||||
});
|
||||
group("Test loading rive file with embedded asset.", () {
|
||||
testWidgets('Default load does not hit any url',
|
||||
(WidgetTester tester) async {
|
||||
final mockHttpClient = getMockHttpClient();
|
||||
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/sample_image.riv');
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
);
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
verifyNever(() => mockHttpClient.openUrl(any(), any()));
|
||||
});
|
||||
|
||||
testWidgets('Disabling embedded assets also does not hit a url',
|
||||
(WidgetTester tester) async {
|
||||
final mockHttpClient = getMockHttpClient();
|
||||
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/sample_image.riv');
|
||||
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
importEmbeddedAssets: false,
|
||||
);
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
// by default we try to make a network request
|
||||
verifyNever(() => mockHttpClient.openUrl(any(), any()));
|
||||
});
|
||||
|
||||
testWidgets('Disabling cdn also does not hit a url',
|
||||
(WidgetTester tester) async {
|
||||
final mockHttpClient = getMockHttpClient();
|
||||
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/sample_image.riv');
|
||||
runZonedGuarded(() {
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
importEmbeddedAssets: false,
|
||||
cdn: false,
|
||||
);
|
||||
}, (error, stack) {
|
||||
// importing assets throws rn when we do not end up loading assets..
|
||||
// could suppress this too..
|
||||
// could ignore them & info log our load attempts.
|
||||
|
||||
// could have a future people can await to get any issues
|
||||
// could have a future people can await to get logs...
|
||||
});
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
// network disabled
|
||||
verifyNever(() => mockHttpClient.openUrl(any(), any()));
|
||||
// by default we try to check for assets
|
||||
});
|
||||
testWidgets('test importing rive file, make sure we get a good callback',
|
||||
(WidgetTester tester) async {
|
||||
// lets just return an image
|
||||
final riveBytes = loadFile('assets/sample_image.riv');
|
||||
final imageBytes = loadFile('assets/file.png');
|
||||
final parameters = [];
|
||||
RiveFile.import(riveBytes, importEmbeddedAssets: false,
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
parameters.add(asset);
|
||||
await asset.decode(Uint8List.sublistView(
|
||||
imageBytes,
|
||||
));
|
||||
return true;
|
||||
},
|
||||
));
|
||||
|
||||
final asset = parameters.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);
|
||||
expect(fileAsset.id, -1);
|
||||
});
|
||||
});
|
||||
group("Test loading rive file with cdn asset.", () {
|
||||
late MockHttpClient mockHttpClient;
|
||||
setUp(() {
|
||||
mockHttpClient = getMockHttpClient();
|
||||
final imageBytes = loadFile('assets/file.png');
|
||||
prepMockRequest(mockHttpClient, Uint8List.sublistView(imageBytes));
|
||||
});
|
||||
|
||||
testWidgets('Default load will his the cdn', (WidgetTester tester) async {
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
);
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
verify(() => mockHttpClient.openUrl(
|
||||
any(),
|
||||
// ok, hardcoded for the cdn_image.riv file.
|
||||
Uri.parse(
|
||||
'https://public.rive.app/cdn/uuid/b86dc1e6-35f7-4490-96fc-89ebdf848473'),
|
||||
)).called(1);
|
||||
});
|
||||
|
||||
testWidgets('Disabling embedded assets also hits a url',
|
||||
(WidgetTester tester) async {
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
runZonedGuarded(() {
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
importEmbeddedAssets: false,
|
||||
);
|
||||
}, (error, stack) {
|
||||
print('what?');
|
||||
});
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
// by default we try to make a network request
|
||||
verify(() => mockHttpClient.openUrl(any(), any())).called(1);
|
||||
});
|
||||
|
||||
testWidgets('Disabling cdn will mean no url hit',
|
||||
(WidgetTester tester) async {
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
|
||||
RiveFile.import(
|
||||
riveBytes,
|
||||
importEmbeddedAssets: false,
|
||||
cdn: false,
|
||||
);
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
// network disabled
|
||||
verifyNever(() => mockHttpClient.openUrl(any(), any()));
|
||||
// by default we try to check for assets
|
||||
});
|
||||
testWidgets(
|
||||
'If we provide a callback, we are hit first, and success means no cdn hit',
|
||||
(WidgetTester tester) async {
|
||||
// lets just return an image
|
||||
final imageBytes = loadFile('assets/file.png');
|
||||
final parameters = [];
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
|
||||
RiveFile.import(riveBytes, importEmbeddedAssets: false,
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
parameters.add(asset);
|
||||
await asset.decode(Uint8List.sublistView(
|
||||
imageBytes,
|
||||
));
|
||||
return true;
|
||||
},
|
||||
));
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
final asset = parameters.first;
|
||||
|
||||
expect(asset is ImageAsset, true);
|
||||
verifyNever(() => mockHttpClient.openUrl(any(), any()));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'If we provide a callback, we are hit first, a failure means we hit cdn',
|
||||
(WidgetTester tester) async {
|
||||
// lets just return an image
|
||||
final parameters = [];
|
||||
await HttpOverrides.runZoned(() async {
|
||||
final riveBytes = loadFile('assets/cdn_image.riv');
|
||||
|
||||
RiveFile.import(riveBytes, importEmbeddedAssets: false,
|
||||
assetLoader: CallbackAssetLoader(
|
||||
(asset) async {
|
||||
parameters.add(asset);
|
||||
return false;
|
||||
},
|
||||
));
|
||||
}, createHttpClient: (_) => mockHttpClient);
|
||||
|
||||
final asset = parameters.first;
|
||||
|
||||
expect(asset is ImageAsset, true);
|
||||
verify(() => mockHttpClient.openUrl(any(), any())).called(1);
|
||||
});
|
||||
});
|
||||
}
|
BIN
test/assets/cdn_image.riv
Normal file
BIN
test/assets/cdn_image.riv
Normal file
Binary file not shown.
BIN
test/assets/file.png
Normal file
BIN
test/assets/file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
BIN
test/assets/sample_image.riv
Normal file
BIN
test/assets/sample_image.riv
Normal file
Binary file not shown.
@ -6,16 +6,9 @@ import 'package:mocktail/mocktail.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
|
||||
import 'mocks/mocks.dart';
|
||||
import 'src/network.dart';
|
||||
import 'src/utils.dart';
|
||||
|
||||
class MockHttpClient extends Mock implements HttpClient {}
|
||||
|
||||
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
|
||||
|
||||
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
|
||||
|
||||
class MockHttpHeaders extends Mock implements HttpHeaders {}
|
||||
|
||||
void main() {
|
||||
late MockHttpClient mockHttpClient;
|
||||
late MockHttpClientRequest request;
|
||||
@ -26,25 +19,9 @@ void main() {
|
||||
// Build our app and trigger a frame.
|
||||
final riveBytes = loadFile('assets/rive-flutter-test-asset.riv');
|
||||
final body = riveBytes.buffer.asUint8List();
|
||||
mockHttpClient = MockHttpClient();
|
||||
request = MockHttpClientRequest();
|
||||
mockHttpClient = getMockHttpClient();
|
||||
|
||||
when(() => request.headers).thenReturn(MockHttpHeaders());
|
||||
|
||||
when(() => mockHttpClient.openUrl(any(), any())).thenAnswer((invocation) {
|
||||
final response = MockHttpClientResponse();
|
||||
when(request.close).thenAnswer((_) => Future.value(response));
|
||||
when(() => request.addStream(any())).thenAnswer((_) async => null);
|
||||
when(() => response.headers).thenReturn(MockHttpHeaders());
|
||||
when(() => response.handleError(any(), test: any(named: 'test')))
|
||||
.thenAnswer((_) => Stream.value(body));
|
||||
when(() => response.statusCode).thenReturn(200);
|
||||
when(() => response.reasonPhrase).thenReturn('OK');
|
||||
when(() => response.contentLength).thenReturn(body.length);
|
||||
when(() => response.isRedirect).thenReturn(false);
|
||||
when(() => response.persistentConnection).thenReturn(false);
|
||||
return Future.value(request);
|
||||
});
|
||||
request = prepMockRequest(mockHttpClient, body);
|
||||
});
|
||||
|
||||
testWidgets('Using the network, calls the http client without headers',
|
||||
|
39
test/src/network.dart
Normal file
39
test/src/network.dart
Normal file
@ -0,0 +1,39 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockHttpClient extends Mock implements HttpClient {}
|
||||
|
||||
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
|
||||
|
||||
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
|
||||
|
||||
class MockHttpHeaders extends Mock implements HttpHeaders {}
|
||||
|
||||
MockHttpClient getMockHttpClient() => MockHttpClient();
|
||||
MockHttpClientRequest prepMockRequest(
|
||||
MockHttpClient httpClient,
|
||||
Uint8List body,
|
||||
) {
|
||||
MockHttpClientRequest request = MockHttpClientRequest();
|
||||
|
||||
when(() => request.headers).thenReturn(MockHttpHeaders());
|
||||
|
||||
when(() => httpClient.openUrl(any(), any())).thenAnswer((invocation) {
|
||||
final response = MockHttpClientResponse();
|
||||
when(request.close).thenAnswer((_) => Future.value(response));
|
||||
when(() => request.addStream(any())).thenAnswer((_) async => null);
|
||||
when(() => response.headers).thenReturn(MockHttpHeaders());
|
||||
when(() => response.handleError(any(), test: any(named: 'test')))
|
||||
.thenAnswer((_) => Stream.value(body));
|
||||
when(() => response.statusCode).thenReturn(200);
|
||||
when(() => response.reasonPhrase).thenReturn('OK');
|
||||
when(() => response.contentLength).thenReturn(body.length);
|
||||
when(() => response.isRedirect).thenReturn(false);
|
||||
when(() => response.persistentConnection).thenReturn(false);
|
||||
return Future.value(request);
|
||||
});
|
||||
return request;
|
||||
}
|
Reference in New Issue
Block a user