How to Build a Responsive House with Flare and Flutter
--
Before Google I/O ‘18, at 2Dimensions we started experimenting with Flutter. We ended up building a game that was played by ~3000 people at the conference, but on the side we also built a Showcase app that seamlessly combines UI elements with smooth animated graphics in real-time!
On its first screen, the Showcase app contains an animation of a house shrinking and growing based on the position of a UI slider (see the gif below). This connection between the UI and the graphics was the most compelling part of the demo, which garnered a lot of interest and questions from the community. In this article we’re going to show you that this isn’t as complicated as it looks: we’ll be taking advantage of our Flare-Flutter API — more specifically the FlareController
.
The House in Flare
Let’s start by taking a look at the Flare file here.
(N.B. It’s forkable, so feel free to experiment with it!)
You’ll notice that there are 9 animations. The first one that is named “Demo Mode”: this is the full animation of the house shrinking down from its biggest to its smallest size. This animation will be playing on startup, and it loops until the user interacts with the slider.
The other animations are a breakdown of the single steps that the house performs while resizing down, one room at a time — in fact, they’re named based on the number of rooms that step will size to house to: “to 3”, “to 4” and so on.
Lastly, the other “Highlight” animations will show a circle in the right place at right time, giving a popping effect when a house element changes its shape or gets removed.
The House in Flutter
We’ve added the example in our Flare-Flutter repo here. The app is very simple: it starts by playing the “Demo Mode” animation we discussed above. The user can override this animation by interacting with the slider on the screen as shown in the gif above. After 2 seconds of inactivity, the app goes back to playing “Demo Mode.”
As you can see in the pubspec.yaml
, we’ve added the flare_flutter
dependency (N.B. We use a relative path since we’re in the library’s repo, but the package is also available on DartPub).
The assets
folder containing the Flare file has been added to the pubspec.yaml
so that its contents can be accessed.
Here’s a list of the example’s most important files:
/lib
- main.dart
- page.dart
- house_controller.dart
/assets
- Resizing_House.flr
page.dart
contains the Page
widget which is the main view of the app — a Stack
with the FlareActor
widget filling the screen, and the Slider
placed on top. Page
is a StatefulWidget
, so that it can rebuild whenever a value changes or an event occurs.
_PageState
contains the logic for this view, and it has two main fields:
// Inactivity Timer: if it fires,
// set the animation state back to "Demo Mode"
Timer _currentDemoSchedule; // Custom-made controller.
HouseController _houseController;
The two components we’ll be focusing on are the FlareActor
widget from our flare_flutter
library, and the Slider
widget, a native Flutter component, that allows us to manipulate the animation.
Let’s first look at the FlareActor
:
[...]
FlareActor(
"assets/Resizing_House.flr",
controller: _houseController
)
[...]
We pass the asset location that we had added to the pubspec.yaml
, and then specify that this FlareActor
will be controlled with our custom _houseController
.
Taking Control
Let’s move our focus onto the HouseController
. This component inherits directly from FlareController
— that is the basic abstract class for controlling a Flare animation in Flutter. Its subclasses needs to override three methods:
initialize(FlutterActorArtboard artboard)
This is called once: when theFlareActor
widget is first createdadvance(FlutterActorArtboard artboard, double elapsed)
This is called every frame: every time the artboard advances, it relays the elapsed time to the controller, which can thus perform custom actionssetViewTransform(Mat2D viewTransform)
This is also called every frame, and relays information regarding the current view matrix of the animation
The constructor for the HouseController
takes a function parameter called demoUpdated
: this allows the controller to communicate with the Page
widget to let it know when the animation value has changed.
We won’t be needing to change the view transform matrix for this component, so that override will just be empty. The other two, on the other hand, are where things get interesting.
Initialize and Animation Layers
initialize()
is pretty straightforward: we grab the references to the ActorAnimation
s.
You’ll notice that these animations are wrapped in another flare_flutter
class, called FlareAnimationLayer
.
Let’s take a brief tour of the API: when playing an animation, we need to call on the animation object the apply()
method — its signature looks like this:
apply(double time, ActorArtboard artboard, double mix)
Let’s break down the parameters:
time
is the current time for this animationartboard
is the Artboard that contains itmix
is a value[0,1]
: this is a blending parameter to allow smoothing between concurrent animations¹. By settingmix
to 1, the current animation will fully replace the existing values. By ramping up mix with values between 0 and 1, the transition from one animation to the next will be more gradual as it gets mixed in, preventing popping effects
The FlareAnimationLayer
class simplifies the API by wrapping all this relevant information and providing a simplified API call to apply()
. In fact, FlareAnimationLayer.apply()
has a single parameter, the artboard; the other two values are implicit within the class itself, and we can manipulate them directly.
Advance
Given what we said above, this is where the controller advances and applies animations to their new time. In fact this method's elapsed
parameter passes the time passed since the last frame.
First, the method will keep on playing the _skyAnimation
, which is always active in the background:
// Advance the current time by [elapsed].
_skyAnimation.time =
(_skyAnimation.time + elapsed) % _skyAnimation.duration;_skyAnimation.apply(artboard);
This FlareAnimationLayer
has been initialized with a mix
of 1.0
, and updates its time
et every frame, looping with the modulo operator. When apply()
is called, we just pass in the artboard
value because all the others parameters are already set.
We then use a List<FlareAnimationLayer>
to perform our custom logic.: when this list is not empty, we want to play the animations that it contains by mixing it with the current one; if any of the animations in the list are finished, they’re removed.
// Iterate from the top because elements might be removed.
int len = _roomAnimations.length - 1;for (int i = len; i >= 0; i--) {
FlareAnimationLayer layer = _roomAnimations[i];
layer.time += elapsed; // The mix quickly ramps up to 1
// but interpolates for the first few frames.
layer.mix = min(1.0, layer.time / 0.07);
layer.apply(artboard); // When done, remove it.
if (layer.isDone) {
_roomAnimations.removeAt(i);
}
}
Lastly we check if the the house is still in demo mode: if it is, keep playing the demo animation; otherwise quickly ramp down its mix
value and stop it.
Slider
And now onto the last piece of the puzzle, the Slider
. This component is part of the Page
widget sub-tree:
Slider(
[...]
onChanged: (double value) {
// setState() causes the widget to refresh its visual appearance
setState(() {
// Stop the demo
_houseController.isDemoMode = false; // When the value of the slider changes, the setter
// is invoked, which triggers an animation swap.
_houseController.rooms = value.toInt() + 3;
[...]
});
})
By defining the onChanged()
callback of the Slider
, we can relay the new values to the _houseController
. So, in this function, we first stop the demo, and then we update the number of rooms with the new value; when updating, we’re effectively invoking _houseController
room setter:
set rooms(int value) {
[...]
_enqueueAnimation("to $value");
[...]
}
The new animation with the correct room numbers is added to the list of playable animation. Our advance()
function will find that value, and start interpolating the current state with this new animation, quickly stopping _demoAnimation
.
Closing the Loop
Lastly, whenever the Page
widget detects the onPointerUp()
event — i.e. when the finger has been raised from the screen — it’ll start the _currentDemoSchedule
timer: when it fires, it resets the state of the _houseController
to “Demo Mode” and the animation plays once again.
That’s it!
With this tutorial, we took a tour of the Flare-Flutter API, particularly how to build a custom FlareController
, how to manage animations using its advance()
method override and the FlareAnimationLayer
class.
Be sure to check out the sources on GitHub and Flare, and join us at 2Dimensions.com!