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:

  1. initialize(FlutterActorArtboard artboard) 
    This is called once: when the FlareActor widget is first created
  2. advance(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 actions
  3. setViewTransform(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 ActorAnimations. 
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 animation
  • artboard is the Artboard that contains it
  • mix is a value [0,1]: this is a blending parameter to allow smoothing between concurrent animations¹. By setting mix 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.

Changing the slider value triggers its callback. A simplified call chain is displayed with the new value being passed to the controller.

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!