Eric Windmill
Imagine sitting and your living room and a friend appearing in front of you, rather than transitioning in the room by walking through the door. That isn’t natural, and human brains don’t like it.
Animations and transitions are important for modern UX.
Natural Transitions
Responsive
Associative
This transition is simple to implement. I’ll argue that should always be used if you’re working with a large network Image.
It’s used default image while your app goes out on the internet to grab a big ol’ image and download it.
In this gif you can see the difference between fading in an image (the profile picture), and letting them pop in (the bottom accessory images). Notice the hat. It’s a larger image file. There’s some time before the image loads that it looks like the ‘Favorite Hat’ text is just hnaging out for no reason.
Having an image transition in after it loads is as easy as providing two images to a FadeInImage
Widget.
Widget transitonImage = new FadeInImage(
placeholder: new AssetsImage('path/to/image');
image: new NetworkImage('url');
);
Flutter makes it incredibly easy on us to customize the transition. You have access to the kind of animation, and (more importantly) the duration.
Widget transitonImage = new FadeInImage(
placeholder: new AssetsImage('path/to/image');
src: new NetworkImage('url');
fadeOutDuration: new Duration(milliseconds: 300),
fadeOutCurve: Curves.decelerate
);
Important Properties
placeholder
must not be null. It can be any class that implementsImageProvider
.image
must not be null. It can be any class that implementsImageProvider
.fadeOutDuration
takes a Duration.fadeInDuration
works the same.fadeOutCurve
uses the Curves class to establish the type of animation.fadeInCurve
works the same.
NB: Shorter animations for UX are better. While you can set an animation to seconds, minutes, etc, under 500 milliseconds is usually best.
AnimatedCrossFade is used to transition Widgets when they re-render on state change.
Expanding form fields like this is very strong use case for this type of animation. The mobile screen is small. It’s important to save space where you can.
This is a basic example that uses all four required properties, but nothing else.
Widget crossFade(Widget first, Widget second, bool fade ) {
return new AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
firstChild: first,
secondChild: second,
crossFadeState:
fade ? CrossFadeState.showFirst : CrossFadeState.showSecond,
);
}
firstChild
and secondChild
are the widgets you want to fade between.
The crossFadeState
will always look like this. It’s used to tell Flutter which of the two children to render. Using a ternary you have an easy way to switch back and forth. Here’s a complete Widget that will use that animation.
class Fader extends StatefulWidget {
...
}
class _FaderState extends State<Fader> {
bool fade = true;
toggleFade() {
setState(() {
fade = !fade;
});
}
Widget crossFade(Widget first, Widget second, bool fade ) {
return new AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
firstChild: first,
secondChild: second,
crossFadeState:
fade ? CrossFadeState.showFirst : CrossFadeState.showSecond,
);
}
Widget build(context) {
return new Column(
children: <Widget>[
_crossFade(
new Text('The First Text Displayed'),
new Text('The Second Text Displayed'),
fade
),
new RaisedButton(
child: new Text('Toggle Fade'),
onTap: toggleFade,
)
],
),
}
}
Importante:
You should put your AnimatednCrossFade
into a function that returns it. It works this way (and not when the Widget is in the build function) because of the way Flutter’s setState
method and re-rendering work.
You can also customize this fade style to your liking. This is the exact code used to make the animation in the gif above.
return new AnimatedCrossFade(
duration: const Duration(milliseconds: 400),
firstChild: first,
secondChild: second,
crossFadeState:
fade ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstCurve: const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
secondCurve: const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
);
These new properties are based on animation curves
. This intimidated me, at first. But, they’re actually pretty simple.
Measuring the time within an animation, from beginning to end, is done on a scale from 0.0 to 1.0. So, 0.5 is half way through an animation. (This is the same in CSS). So, imagine the first curve as the ‘fade out’ and the second curve as the ‘fade in’. They happen at the same time, so, in order to make the transition seem natural, they overlap slightly. The fade out is completely done 6/10ths of the way through.
Check out this very slow contrived version. The duration has been expanded, and the fades have been seperated.
return new AnimatedCrossFade(
duration: const Duration(milliseconds: 1000),
firstChild: first,
secondChild: second,
crossFadeState:
fade ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstCurve: const Interval(0.0, 0.3, curve: Curves.fastOutSlowIn),
secondCurve: const Interval(0.7, 1.0, curve: Curves.fastOutSlowIn),
sizeCurve: Curves.fastOutSlowIn,
);
It’s easy to see that theres a gap in time where the fade out is finished and the fade in hasn’t started.
The Curves
are basically the visual effect that the fades take. For this particular animator, I encourage you to play around the Curve that you pass to the sizeCurve
property. You can see all the different curve properties here: Flutter Curves Class
Finally, we have Hero Animations:
Essentially, you’re flying an image from one route or page to another. Shopping carts, detail pages for items, profile pages — the use-cases are abundant.
Hero Animations can get a bit more complicated than transitions, but Flutter has still made it super easy.
Flutter Hero Animations in 4 Steps
- Establish an element — most likely an image — on your page. Wrap it in some special widgets:
Hero
andMaterial
.- Make The widget tapable by wrapping it in an Inkwell.
onTap
, push a new MaterialRoute onto theNavigator
.- In that new route, the same element should exist, wrapped in the same special widgets, with the same tag.
The most basic of implementations:
class MyPage extends StatefulWidget {
// ....
}
class _MyPageState extends State<MyPage> {
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('My Transitions),
),
body:
child: new Column(
children: <Widget>[
new Hero(
// Tag is required and must match exactly.
tag: 'hero01',
// You must wrap your Inkwell in a Material Widget
// This is where the animation comes from.
// It will work without it, but it simply pops from one screen to the other.
child: new Material(
// Any Widget that makes it tap-able will do.
child: new InkWell(
onTap: heroAnimation,
child: new Image.network(
'url to image'),
),
),
),
],
),
),
);
}
heroAnimation() {
// Here, you're pushing to the next page when the image above is tapped.
return Navigator.of(context).push(
new MaterialPageRoute<Null>(builder: (BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Hero Page 2'),
),
body: new Container(
child: new Hero(
tag: 'hero01',
child: new Material(
child: new InkWell(
// On tap, go back to the other page. This isn't necessary.
onTap: () => Navigator.of(context).pop(),
child: new Image.network(
'same url to image'),
),
),
),
),
),
);
}),
);
}
}
That’s it. That gives you a super basic example. You’ll probably want to play around with making the photos on the separate routes different (in size, location on page, etc). Get creative with it.
These three animations are never overkill. They aren’t used for delight, they’re used for a great user experience.
The first two animations should be used liberally throughout an app. If we consider Pareto’s Law, those two animations alone will give you 80% of the ‘good ux animation’ results. The Hero Animations will give you another 19%.
Sign up for my mailing list to receive new articles, mainly about Dart and Flutter, and other programming technologies.