Google I/O Filter Animation in Flutter

Hello Flutter Learners,

Some days back, when I started Flutter development, I was very pleased with how UI designing changed from the era of XML/XAML/HTML to Dart. There is no way any developer in the world who is working on Flutter can get away without knowing animations. There was a time when animations were hard for me in Android, before I discovered value animators, object animators, which made jobs easier. But the way, Flutter has implemented animations makes it a hell of a lot easier and I can bet that it is the easiest way than any other languages out there.

Animating Google I/O Filter button.

What you will learn

Animation that is emulated in this tutorial

Designing UI

First and foremost, is designing the filter button by considering the fact that the elements in the button will animate when clicked.

Taking a Container of height 40.0 with a rounded border. Container’s child will be a stack having 3 children.

  • Rounded dot — Creating a rounded dot using a rectangle box decoration because it becomes easier when we change the width and height of the box only. Height = Width = 20.0 initially.
  • Text — Text has left padding of 40.0 and right padding of 20.0 initially.
  • Icon — Rounded container positioned 5.0 from right having a close icon.
Widget getWidget(BuildContext context, Widget widget) {
return Center(
child: InkWell(
onTap: () {},
child: Container(
height: 40.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(20.0),
),
border: Border.all(color: Colors.black26, width: 1.0),
),
child: Stack(
children: <Widget>[
Positioned(
left: 10.0,
top: 8.5,
child: Container(
height: 20.0,
width: 20.0,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(
Radius.circular(20.0),
),
),
),
),
Align(
widthFactor: 1.0,
alignment: Alignment.center,
child: Container(
padding: EdgeInsets.only(left: 40.0, right: 20.0),
child: Text(
"Flutter",
style: TextStyle(fontSize: 20.0, color: Colors.black),
),
),
),
Positioned(
width: 0.0,
height: 0.0,
right: 5.0,
top: 7.0,
child: Container(
padding: EdgeInsets.all(2.0),
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.grey.shade400),
child: Icon(
Icons.close,
size: 20.0,
color: Colors.white,
),
),
)
],
),
),
),
);
}

The output of the above code is —

What actually happens in the animation?

When the button is pressed —

  • The text moves to left by certain points
  • Dot covers the container with its color, basically, the dot which I created using rectangle changes its height to 40.0 and width to the height of the container.
  • Close icon grows from zero to a certain size

I have achieved these value changes using tweens in Flutter. Let’s have a look.

Implementing Tweens

What are tweens? Tween is just a set of 2 values — begin and end. When tween moves in forwarding direction by the controller, the begin value moves to become end value in some given time and in some given pattern(defined by curves). Vice-versa happens when the controller moves in reverse direction.

In this animation’s case, there are few values that change simultaneously to bring out the desired animation. Have a look at the below table below —

Tweens of the values which change for the animation

Text animation

3 values change for our text, left padding, right padding, and text color

AnimationController _controller;
Animation<double> _textLeftPaddingAnimation;
Animation<double> _textRightPaddingAnimation;
Animation<Color> _textColorAnimation;
@override
void initState() {
super.initState();
_controller =
AnimationController(
vsync: this,
duration: Duration(
milliseconds: 400),);
_textLeftPaddingAnimation =
Tween<double>(
begin: 40.0,
end: 20.0).
animate(_controller);
_textRightPaddingAnimation =
Tween<double>(
begin: 20.0,
end: 40.0).
animate(_controller);
_textColorAnimation =
ColorTween(
begin: Colors.black,
end: Colors.white).
animate(_controller);
}
//Code for the text in the stack
Align(
widthFactor: 1.0,
alignment: Alignment.center,
child: Container(
padding: EdgeInsets.only(
left: _textLeftPaddingAnimation.value,
right: _textRightPaddingAnimation.value),
child: Text(
"Flutter",
style: TextStyle(
fontSize: 20.0, color: _textColorAnimation.value),
),
),
),

Output

Text animation applied

Close Icon Animation

Updated code for Icon in the stack and applied animation tween. Using Scale transition(makes stuff easier when dealing with the size of a widget)

Animation<double> _closeIconAnimation;
_closeIconAnimation = Tween<double>(
begin: 0.0,
end: 1.0).
animate(_controller);
Positioned(
right: 5.0,
top: 7.0,
child: ScaleTransition(
scale: _closeIconAnimation,
child: Container(
padding: EdgeInsets.all(2.0),
decoration:
BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade400),
child: Icon(
Icons.close,
size: 20.0,
color: Colors.white,
),
),
),
)

Output

Close icon animation applied

Dot Animation

Well, so far so good. But now, the dot has to change its width according to the container’s width and height as 40.0 which is default in this case. We will change 4 values here, dot width, dot height, dot left position and dot top position.

Using Global keys we can get width of container once its built in the widget tree.

Create a key for the container which contains the stack

final GlobalKey _parentContainer = GlobalKey(debugLabel: 'parent container');
child: Container(
key: _parentContainer,
child: Stack()
)

Get the width of the container, considering default value as 20.0 for the dot.

var width = 20.0;
if (_parentContainer.currentContext != null) {
final RenderBox renderBox = _parentContainer.currentContext.findRenderObject();
width = renderBox.size.width;
}

Tweens for the dot positioning and size

_dotHeightAnimation = Tween<double>(
begin: 20.0,
end: 40.0).animate(_controller);
_dotLeftPositionAnimation = Tween<double>(
begin: 10.0,
end: 0.0).animate(_controller);
_dotTopPositionAnimation = Tween<double>(
begin: 8.5,
end: -1.0).animate(_controller);
Animation<double> getDotWidth() {
var width = 20.0;
if (_parentContainer.currentContext != null) {
final RenderBox renderBox = _parentContainer.currentContext.findRenderObject();
width = renderBox.size.width;
}
  return Tween<double>(
begin: 20.0,
end: width - 2).animate(_controller);
}

Change the dot UI with animations

Positioned(
left: _dotLeftPositionAnimation.value,
top: _dotTopPositionAnimation.value,
child: Container(
height: _dotHeightAnimation.value,
width: getDotWidth().value,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(20.0))),
),
),

Output

Yay!! Animation completed.
Key gems out of this animation — that I personally learned while making.
Keys- utilizing them to get the width, the height of a widget
AnimatedBuilder- Very important to accommodate animations while building the widget as in our case.
Circular dot- I could create a rounded dot by using both BoxShape.circle and BoxShape.rectangle.

The full code can be found on — https://github.com/PrateekSharma1712/flutter-articles/blob/master/googleiofilteroptionanimation.dart

Some code snippets that you should also know, I used in this animation

AnimationBuilder

@override
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: getWidget,
animation: _controller,
);
}

AnimationStatus

bool get animationStatus {
AnimationStatus status = _controller.status;
return status == AnimationStatus.completed;
}

InkWell — wrapping Container with _parentContainer key

Making the controller moving forward or reverse based on its current status. Basically toggling.

InkWell(
onTap: () {
animationStatus ? _controller.reverse() : _controller.forward();
})
That is it for this article. Feel free to ask questions or discuss better ways to implement this animation. Do clap 👏 if you enjoyed the article. Check out the Github. Thank you very much.