diff --git a/flare_flutter/lib/flare_flutter.dart b/flare_flutter/lib/flare_flutter.dart new file mode 100644 index 0000000..834175c --- /dev/null +++ b/flare_flutter/lib/flare_flutter.dart @@ -0,0 +1,15 @@ +/// Flare offers powerful realtime vector design and animation for app +/// and game designers alike. The primary goal of Flare is to allow designers +/// to work directly with assets that run in their final product, eliminating +/// the need to redo that work in code. + +library flare_flutter; + +export 'package:flare_dart/math/mat2d.dart'; + +export 'flare.dart'; +export 'flare_actor.dart'; +export 'flare_controller.dart'; +export 'flare_controls.dart'; +export 'flare_progress_controller.dart'; +export 'flare_spammer_actor.dart'; diff --git a/flare_flutter/lib/flare_progress_controller.dart b/flare_flutter/lib/flare_progress_controller.dart new file mode 100644 index 0000000..712125a --- /dev/null +++ b/flare_flutter/lib/flare_progress_controller.dart @@ -0,0 +1,43 @@ +import 'package:flare_dart/math/mat2d.dart'; + +import 'flare.dart'; +import 'flare_controller.dart'; + +/// A naiive controller that allows you to scrub through a single animation. +/// Useful for wiring up progress bars or scroll based animations. +class FlareProgressController extends FlareController { + FlareProgressController(this.animation); + + final String animation; + + FlutterActorArtboard _artboard; + ActorAnimation _animation; + + @override + bool advance(FlutterActorArtboard artboard, double elapsed) { + return false; + } + + @override + void initialize(FlutterActorArtboard artboard) { + _artboard = artboard; + if (_artboard != null) { + _animation = artboard.getAnimation(animation); + + if (_animation != null) { + _animation.apply(0.0, _artboard, 1.0); + } + } + } + + /// Updates the animation progress and triggers a render + void update(double t) { + if (_animation != null) { + final time = _animation.duration * t; + _animation.apply(time, _artboard, 1.0); + } + } + + @override + void setViewTransform(Mat2D viewTransform) {} +} diff --git a/flare_flutter/lib/flare_spammer_actor.dart b/flare_flutter/lib/flare_spammer_actor.dart new file mode 100644 index 0000000..fafe546 --- /dev/null +++ b/flare_flutter/lib/flare_spammer_actor.dart @@ -0,0 +1,232 @@ +import 'package:flare_flutter/flare.dart'; +import 'package:flare_flutter/flare_render_box.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flare_dart/math/mat2d.dart'; +import 'package:flare_dart/math/aabb.dart'; + +/// Triggers a [FlareSpammerActor] to fire an animation +class FlareSpammerController extends ChangeNotifier { + void trigger() { + notifyListeners(); + } +} + +/// Allows spamming of multiple animations simultaneously. Useful for +/// firing multiple like animations simultaneously similar to hearts +/// in Facebook live streams. +class FlareSpammerActor extends LeafRenderObjectWidget { + const FlareSpammerActor( + this.filename, { + @required this.animationBuilder, + @required this.controller, + this.fit = BoxFit.contain, + this.alignment = Alignment.center, + this.snapToEnd = false, + this.artboard, + }); + + /// Name of the Flare file to be loaded from the AssetBundle. + final String filename; + + /// The name of the artboard to display. + final String artboard; + + /// The name of the animation to play. Provides the number of currently + /// playing animations on screen. + final String Function(int count) animationBuilder; + + /// When true, the animation will be applied at the end of its duration. + final bool snapToEnd; + + /// The BoxFit strategy used to scale the Flare content into the + /// bounds of this widget. + final BoxFit fit; + + /// The alignment that will be applied in conjuction to the [fit] to align + /// the Flare content within the bounds of this widget. + final Alignment alignment; + + final FlareSpammerController controller; + + @override + RenderObject createRenderObject(BuildContext context) { + return _FlareSpammerRenderObject(animationBuilder) + ..assetBundle = DefaultAssetBundle.of(context) + ..filename = filename + ..fit = fit + ..alignment = alignment + ..controller = controller; + } + + @override + void updateRenderObject( + BuildContext context, covariant _FlareSpammerRenderObject renderObject) { + renderObject + ..assetBundle = DefaultAssetBundle.of(context) + ..filename = filename + ..fit = fit + ..alignment = alignment + ..controller = controller; + } + + @override + void didUnmountRenderObject( + covariant _FlareSpammerRenderObject renderObject) { + renderObject.dispose(); + } +} + +class _RepaintAnimation { + final ActorAnimation animation; + double time = 0.0; + void apply(FlutterActorArtboard artboard) { + animation.apply(time, artboard, 1.0); + } + + double get duration => animation.duration; + bool get isDone => time >= animation.duration; + bool get hasAnimation => animation != null; + + _RepaintAnimation(this.animation); +} + +/// Does the heavy lifting +class _FlareSpammerRenderObject extends FlareRenderBox { + _FlareSpammerRenderObject(this.animationBuilder); + + final String Function(int) animationBuilder; + + String _filename; + FlutterActor _actor; + FlareSpammerController _controller; + + FlareSpammerController get controller => _controller; + set controller(FlareSpammerController value) { + if (_controller == value) { + return; + } + _controller = value; + _controller.addListener(fireHeart); + } + + void fireHeart() { + final newAnimation = _RepaintAnimation( + _artboard?.getAnimation( + animationBuilder(_repaintAnimations.length), + ), + ); + + if (newAnimation.hasAnimation) { + _repaintAnimations.add(newAnimation); + } + } + + final List<_RepaintAnimation> _repaintAnimations = []; + + FlutterActorArtboard _artboard; + AABB _setupAABB; + + void updateBounds() { + if (_artboard != null) { + _setupAABB = _artboard.artboardAABB(); + } + } + + /// We're playing if we're not paused and our controller is active (or + /// there's no controller) or there are animations running. + @override + bool get isPlaying => _repaintAnimations.isNotEmpty; + + @override + void onUnload() { + _repaintAnimations.clear(); + controller?.removeListener(fireHeart); + } + + String get filename => _filename; + set filename(String value) { + if (value == _filename) { + return; + } + _filename = value; + + if (_filename == null) { + markNeedsPaint(); + } + // file will change, let's clear out old animations. + _repaintAnimations.clear(); + load(); + } + + bool _instanceArtboard() { + if (_actor == null || _actor.artboard == null) { + return false; + } + FlutterActorArtboard artboard = + _actor.artboard.makeInstance() as FlutterActorArtboard; + artboard.initializeGraphics(); + _artboard = artboard; + _artboard.advance(0.0); + updateBounds(); + markNeedsPaint(); + return true; + } + + @override + Future load() async { + if (_filename == null) { + return; + } + _actor = await loadFlare(_filename); + if (_actor == null || _actor.artboard == null) { + return; + } + _instanceArtboard(); + } + + @override + void advance(double elapsedSeconds) { + // advance just moves every animation forward + if (isPlaying) { + for (final _RepaintAnimation animation in _repaintAnimations) { + animation.time += elapsedSeconds; + } + } + + if (_artboard != null) { + _artboard.advance(elapsedSeconds); + } + } + + @override + AABB get aabb => _setupAABB; + + @override + void prePaint(Canvas canvas, Offset offset) { + // disable clipping for now + // canvas.clipRect(offset & size); + } + + @override + void paintFlare(Canvas canvas, Mat2D viewTransform) { + if (_artboard == null) { + return; + } + + // Apply, paint, and prune. + List<_RepaintAnimation> prune = []; + for (final _RepaintAnimation animation in _repaintAnimations) { + animation.apply(_artboard); + // Don't have a sense of elapsed time here, so just pass 0 for time. + _artboard.advance(0); + _artboard.draw(canvas); + if (animation.isDone) { + prune.add(animation); + } + } + for (final _RepaintAnimation animation in prune) { + _repaintAnimations.remove(animation); + } + } +}