How to create an animated Fancy Button for Flutter Games and Apps

Mariano Zorrilla
Flutter Community
Published in
4 min readJun 22, 2019

--

This time I bring to you a useful Widget to implement in your Flutter Games and Apps. This solution came to me after watching a cool game having a similar logic and the need to cool buttons for my Flutter Game:

The idea is to have a base color, for example “Colors.red” and applied a lighter and darker color to make it feel more like a button and a 3D shape.

The way to achieve this beautiful UI is by modifying 3 simple values:

  • hue
  • saturation
  • lightness
Color _hslRelativeColor({double h = 0.0, s = 0.0, l = 0.0}) {
final hslColor = HSLColor.fromColor(widget.color);
h = (hslColor.hue + h).clamp(0.0, 360.0);
s = (hslColor.saturation + s).clamp(0.0, 1.0);
l = (hslColor.lightness + l).clamp(0.0, 1.0);
return HSLColor.fromAHSL(hslColor.alpha, h, s, l).toColor();
}

Using _hslRelativeColor(); with our base color, that’s what we’ll have. If we tune the saturation and lightness, we can create the darker color:

_hslRelativeColor(s: -0.20, l: -0.20),

To get the lighter color? Last value, the hue:

_hslRelativeColor(l: 0.06),

To create the shape of the button was really simple, the 3D illusion is creating using a Stack Widget and a bit of help using ClipRRect Widget. When we apply the border radius, we need to cut the inside color to only have top-left and top-right radius while keeping the shape of our Container.

The second factor we need is the translation of each “face”. The darker color should be at the bottom, then the lighter at the middle and the base color at the top. To “wrap” the content, we can use IntrinsicWidth and IntrinsicHeight:

IntrinsicWidth(
child: IntrinsicHeight(
child: Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
color: _hslRelativeColor(s: -0.20, l: -0.20),
borderRadius: radius,
),
),
AnimatedBuilder(
animation: _pressedAnimation,
builder: (BuildContext context, Widget child) {
return Transform.translate(
offset: Offset(0.0, _pressedAnimation.value),
child: child,
);
},
child: Stack(
overflow: Overflow.visible,
children: <Widget>[
ClipRRect(
borderRadius: radius,
child: Stack(
children: <Widget>[
DecoratedBox(
decoration: BoxDecoration(
color: _hslRelativeColor(l: 0.06),
borderRadius: radius,
),
child: SizedBox.expand(),
),
Transform.translate(
offset: Offset(0.0, vertPadding * 2),
child: DecoratedBox(
decoration: BoxDecoration(
color: _hslRelativeColor(),
borderRadius: radius,
),
child: SizedBox.expand(),
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(
vertical: vertPadding,
horizontal: horzPadding,
),
child: widget.child,
)
],
),
),
],
),
),
)

You’ll notice that our middle and top layers have an AnimatedBuilder which is needed to achieve the pressing 3D animation. To do it, we need our TickerProviderStateMixin:

AnimationController _animationController;
Animation<double> _pressedAnimation;

TickerFuture _downTicker;

double get buttonDepth => widget.size * 0.2;

void _setupAnimation() {
_animationController?.stop();
final oldControllerValue = _animationController?.value ?? 0.0;
_animationController?.dispose();
_animationController = AnimationController(
duration: Duration(microseconds: widget.duration.inMicroseconds ~/ 2),
vsync: this,
value: oldControllerValue,
);
_pressedAnimation = Tween<double>(begin: -buttonDepth, end: 0.0).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}

Ptsss you need to dispose it as well!

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

We need 3 states to simulate the pressing state of the button:

  • Finger Down (onTapDown)
  • Finger Up (onTapUp)
  • Cancel (onTapCancel)

We can use a regular GestureDetector for this goal and the methods for each parameter:

void _onTapDown(_) {
if (widget.onPressed != null) {
_downTicker = _animationController.animateTo(1.0);
}
}

void _onTapUp(_) {
if (widget.onPressed != null) {
_downTicker.whenComplete(() {
_animationController.animateTo(0.0);
widget.onPressed?.call();
});
}
}

void _onTapCancel() {
if (widget.onPressed != null) {
_animationController.reset();
}
}
GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
onTapCancel: _onTapCancel,
child: ...
)

Once you connect all the dots, you’ll have an amazing animation like this one:

Current Implementation

I’m using the “Fancy Button” all over my TapHero Game. I noticed how much the game changed using this Widget and how simple it was to create it using Flutter! Not only that, I didn’t made any changes to make it work over the Web as well!

Current Implementation

Usage

This Widget takes any child Widget as a parameter and it’s required. The other 2 parameters required to make it work are: Size and Color.

If you want to have a non pressing button, simply avoid calling the onPressed method:

Pressing Button

FancyButton(
child: Icon(
Icons.close,
size: 50,
color: Colors.black54,
),
size: 50,
color: Colors.green,
onPressed: () {},
)

Non Pressing Button

FancyButton(
child: Text(
"Regular Text",
style: TextStyle(color: Colors.white),
),
size: 18,
color: Colors.red,
)

DOWNLOAD

LINK: Github Gist

--

--

Mariano Zorrilla
Flutter Community

GDE Flutter — Tech Lead — Engineer Manager | Flutter | Android @ Venmo