Make implicit and explicit animations with the animated_styled_widget package

Wenkai Fan
ITNEXT
Published in
5 min readMar 15, 2021

--

This is what we will achieve in this article

The animated_styled_widget package just got an upgrade to support explicit animations. If you don’t know what the package could do before, here is a brief recap:

  1. There is the Style class which is a responsive data class for major UI ingredients like width, height, margin, padding, background decoration, shape, shadowing, transformation, text style, etc. They are responsive in the sense that all those class accepts a Dimension instance where a double px value was intended. For example, you can write something like:
var width = Dimension.clamp(100.toPXLength, 10.toVWLength, 200.toPXLength);

which specifies the width to be 10% of the screen width but also clamped between 100 px and 200 px. Check out the Dimension package here.

A style is defined like:

Style style = Style(
width: 80.toVMINLength,
height: 80.toVMINLength,
backgroundDecoration: BoxDecoration(
gradient:
LinearGradient(colors: [Colors.cyanAccent, Colors.purpleAccent])),
shapeBorder: RectangleShapeBorder(
cornerStyles: RectangleCornerStyles.all(CornerStyle.cutout),
borderRadius: DynamicBorderRadius.all(
DynamicRadius.circular(100.toPercentLength))),
);

2. The Style instance will be provided to the StyledContainer class which gives you a container widget.

Widget widget = StyledContainer(
style: style,
child: child
);

3. You can make the StyledContainer animate implicitly easily. Just use the AnimatedStyledContainer class:

Widget widget = AnimatedStyledContainer(
curve: Curves.linear,
duration: Duration(milliseconds: 100),
style: toggleStyle ? beginStyle : endStyle,
child: child
);

The widget will animate between the two styles automatically when you toggle between the beginning style and the end style.

Here are some of the effects the AnimatedStyledContainer can achieve:

Basically, this package gives you a super Container/AnimatedContainer class.

Now let's talk about explicit animations

Implicit animations are easy to use but can not achieve every effect we want. That's why I designed the ExplicitAnimatedStyledContainer class:

Widget widget = ExplicitAnimatedStyledContainer(
style: style,
child: child,
localAnimations: localAnimations,
globalAnimationIds: globalAnimationIds,
id: id,
...
);

You still provide an initial style to the widget, but then you use local/global animations to animate the widget’s style. Let’s first talk about the localAnimations:

Map<AnimationTrigger, MultiAnimationSequence> localAnimations

It is a map between AnimationTrigger and MultiAnimationSequence. Currently supported AnimationTrigger are the following:

enum AnimationTrigger {
mouseEnter,
mouseExit,
tap,
visible,
scroll,
}

When a trigger event happens(e.g. you tapped this widget), the corresponding MultiAnimationSequence is fired. A MultiAnimationSequence contains a sequences map:

Map<AnimationProperty, AnimationSequence> sequences

where AnimationProperty is an enum class corresponding to every property the Style class has, and AnimationSequence is a list of generic values, durations, delays, and curves that tells us how a certain animation property is evolved. For example:

MultiAnimationSequence(sequences: {
AnimationProperty.width: AnimationSequence()
..add(
delay: Duration(seconds: 1),
duration: Duration(milliseconds: 200),
curve: Curves.linear,
value: 100.toPXLength)
..add(
duration: Duration(milliseconds: 200),
curve: Curves.easeIn,
value: 200.toPXLength)
});

will delay 1 second, then animate the width from its current value to 100 px in 200ms, then to 200 px in 200ms. You can animate other properties using the same syntax.

This mouse hover effect is achieved by writing:

Widget widget = ExplicitAnimatedStyledContainer(
style: style,
child: child,
localAnimations: {
AnimationTrigger.mouseEnter: enterSequence,
AnimationTrigger.mouseExit: exitSequence,
}
);

You can have different durations and curves for mouse entering and exiting, and also for different style properties.

Now let's talk about other animation triggers. The AnimationTrigger.tap is easy to understand. The AnimationTrigger.visible is triggered when the widget becomes visible in the viewport (by using the visibility_detector package). The AnimationTrigger.scroll is triggered when the widget is inside a Scrollable (like a ListView). Then the widget will animate according to its position along the scroll direction:

Animation progress from 0% to 100%

The animation progress by default is calculated as shown in the figure above (if scrolled horizontally). But you can also make the animation start/end earlier or later using two percentage offsets.

Scroll animations in both vertical and horizontal directions
A long animation where we animate every property slowly

Preset Animations

Now those MultiAnimationSequence stuff looks powerful, but also complicated to code. I’ve prepared some predefined animations for general usages. They are categorized into entrance, attention seeker, and exit. For example, one common entrance animation called SlideInAnimation is defined as:

class SlideInAnimation extends PresetAnimation {
final AxisDirection direction;
final Dimension distance;
const SlideInAnimation(
{this.distance = const Length(100, unit: LengthUnit.vmax),
this.direction = AxisDirection.up,
Duration duration = const Duration(seconds: 1),
Duration delay = Duration.zero,
Curve curve = Curves.linear,
CustomAnimationControl control = CustomAnimationControl.PLAY})
: super(duration: duration, delay: delay, curve: curve, control: control);
...
}

You can configure the slide distance and direction, as well as duration, delay, curve, and control (whether the animation should play once or infinitely). Other predefined animations are:

FadeInAnimation
ZoomInAnimation
FadeOutAnimation
SlideOutAnimation
ZoomOutAnimation
FlipAnimation
FlashAnimation
PulseAnimation
SwingAnimation
WobbleAnimation
RainbowAnimation
ElevateAnimation
...

You can use them like this:

Widget widget = ExplicitAnimatedStyledContainer(
style: style,
child: child,
localAnimations: {
AnimationTrigger.visible: FadeInAnimation().getAnimationSequences()
}
);

Then every time the widget moves into the screen it will fade in (opacity from 0 to 1). Another feature of MultiAnimationSequence is the ability to merge or extend other MultiAnimationSequence. So you can do something like this:

Widget widget = ExplicitAnimatedStyledContainer(
style: style,
child: child,
localAnimations: {
AnimationTrigger.visible: FadeInAnimation().getAnimationSequences()..merge(
SlideInAnimation().getAnimationSequences())
}
);

Then the widget will both fade and slide in. If you use extend, the animation will play one after another. Preset animations make animations much easier to use while still offer you great flexibility.

A simple animation editor to let you choose among various preset animations

Global explicit animations

If we want to stagger animations across different widgets, we can do that by providing global animations. A global animation contains a map between String and MultiAnimationSequence where the String is the identifier of a widget. You provide all the global animations you want to use in an animation pool. Then you can trigger a global animation like this:

var animationPool = {
"animation1": GlobalAnimation(sequences: {
"container1" : sequence1,
"container2": sequence2,
...})
}
...ChangeNotifierProvider<GlobalAnimationNotifier>(
create: (_) =>
GlobalAnimationNotifier(animationPool: animationPool), child: child
)
...Widget widget = ExplicitAnimatedStyledContainer(
id: "container1",
style: style,
child: child,
globalAnimationIds: {
AnimationTrigger.visible: "animation1"
}
);

The id does not need to be unique. You can have multiple widgets with the same id so they will all animate under the same global animation. Notice if a widget does not use global animations at all, there is no need for an id.

A staggered global animation triggered by the floating action button

There you have it, implicit and explicit animations for the animated_styled_container package. There are other features in this package like:

  1. Everything is serializable. So you can design, store, and reuse those styles & animations.
  2. You can programmatically change the state of the animation by calling something like inside the child of the ExplicitAnimatedStyledContainer:
Provider.of<LocalAnimationNotifier>(context, listen: false)
.updateAnimationStatus(animationSequence, status);
Provider.of<GlobalAnimationNotifier>(context, listen: false)
.updateAnimationStatus(animationId, status);

3. You can provide callbacks to the AnimationTrigger events along with animations:

Widget widget = ExplicitAnimatedStyledContainer(
style: style,
child: child,
localAnimations: {
AnimationTrigger.visible: animationSequence
},
onVisible: onVisible,
);

This package is still under quick development and documentation is still lacking. So please be patient and kindly provide your feedback. Check it out on pub.dev. Thank you!

--

--

Ph.D. in Nuclear Physics, M.S. in Computer Science at Duke University, Flutter lover