Unleashing the Magic of Implicit Animations in Flutter

Abdul Rehman
7 min readMar 12, 2023

Create smooth and dynamic animations for your Flutter apps with implicit animations. We will implement different types of implicit animations using simple code examples in this blog post.

What Are Implicit Animations? 🎨

Implicit animation are set of predefined widget which helps to add smooth animation to our flutter app without using complex code. Implicit animations in Flutter are highly customizable, and we can adjust the animation speed, duration, and other properties to achieve the desired effect.

Read more: 🔗 https://docs.flutter.dev/codelabs/implicit-animations

TLDR 📝

We will create a pizza app with cool animation to showcase implicit animation. We will use Getx state management for our app. All the resources used in the app will be linked below.

Create Home Screen Widget 🏠

We will create a Stateless widget which will contain all the animations. You can customize the UI according to your needs.

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:modern_dominos/data/pizza_controller.dart';
import 'package:modern_dominos/generated/assets.dart';
import 'package:modern_dominos/ui/pizza_size_widget.dart';
import 'package:modern_dominos/ui/pizza_widget.dart';
import 'package:modern_dominos/utils/constants.dart';

class HomeScreen extends StatelessWidget {
HomeScreen({Key? key}) : super(key: key);

//We will create this in next step
final ctr = Get.put(PizzaController());

@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
//Bg gredient of app
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black.withOpacity(0.6), Colors.black],
),
),
child: Stack(
children: [
Positioned(
top: 62,
left: 0,
right: 0,
child: Center(
child: Image.asset(Assets.imagesDominosLogo, width: 150),
),
),
const Positioned(
top: 112,
left: 0,
right: 0,
child: Center(
child: Text(
'SELECT YOUR PIZZA',
style: TextStyle(color: Colors.grey),
),
),
),
const Positioned(
top: 72,
left: 16,
child: Icon(Icons.menu_rounded, color: Colors.white),
),
const Positioned(
top: 72,
right: 16,
child: Icon(Icons.person_3_rounded, color: Colors.white),
),
//Pizza widget at the bottom of app we will implement this below
//We are using Getx builder to update the UI of the widget.
GetBuilder<PizzaController>(builder: (_) {
return Positioned(
bottom: -ctr.pizzaSize / 1.0,
left: -ctr.extraOverlapPizzaSpace,
right: -ctr.extraOverlapPizzaSpace,
child: GestureDetector(
onHorizontalDragUpdate: (details) {
int sensitivity = 10;
if (details.delta.dx > sensitivity) {
ctr.onRightSwipe();
} else if (details.delta.dx < -sensitivity) {
ctr.onLeftSwipe();
}
},
child: Stack(
children: List.generate(pizzaAssets.length, (index) {
return PizzaWidget(index);
}),
),
),
);
}),
//Pizza size widget we will implement this below
const Positioned(
top: 300,
left: 0,
right: 0,
child: PizzaSizeWidget(),
),
],
),
),
);
}
}

In the above code snippet we have added different widgets which we will implement below. We will use Getx builder to update the state of our widgets.

Implement Widget Controller 🎮

We will create a Getx Controller to drive our animation. All our app logic will be defined in the controller.

Note: Creating controller is not necessary, you can do the same with Stateful widget.

import 'package:get/get.dart';
import '../utils/constants.dart';
import '../utils/pizza_size_enum.dart';

class PizzaController extends GetxController {
final animDuration = const Duration(milliseconds: 500);
final textScaleAnimDuration = const Duration(milliseconds: 200);
final extraOverlapPizzaSpace = 38.0;
var selectedPizzaSize = PizzaSizeEnum.MEDIUM;
var priceTextScale = 1.0;
final rotationTurn = 0.0.obs;
var currentVisiblePizzaIndex = 0;
var isRotating = false;

get pizzaSize => Get.width + (2 * extraOverlapPizzaSpace);

double get pizzaSizeIndicatorPosition {
final oneOptionSize = (Get.width - 64) / 3;
switch (selectedPizzaSize) {
//This is a simple enum to hold pizza sizes
case PizzaSizeEnum.SMALL:
return (oneOptionSize / 2);
case PizzaSizeEnum.MEDIUM:
return oneOptionSize + (oneOptionSize / 2);
case PizzaSizeEnum.LARGE:
return (2 * oneOptionSize) + (oneOptionSize / 2);
}
}

//On tapping size option we will update the size.
onSizeChanged(PizzaSizeEnum size) {
selectedPizzaSize = size;
_animatePriceText();
}

//Animating the scale of price text for cool bounce animation
_animatePriceText() async {
priceTextScale = 1.15;
update();
await Future.delayed(textScaleAnimDuration);
priceTextScale = 1;
update();
}

//Getting pizza prize from predifined lists.
String pizzaPrice(int index) {
switch (selectedPizzaSize) {
case PizzaSizeEnum.SMALL:
return pizzaSmallPrice[index];
case PizzaSizeEnum.MEDIUM:
return pizzaMediumPrice[index];
case PizzaSizeEnum.LARGE:
return pizzaLargePrice[index];
}
}

//Left swiping callback of pizza image. We are updating the pizza index on swiping.
onLeftSwipe() {
if (isRotating) return;//This will help in preventing multiple calls
rotationTurn.value = rotationTurn.value - 1;
if (currentVisiblePizzaIndex > 0) {
currentVisiblePizzaIndex = currentVisiblePizzaIndex - 1;
} else {
currentVisiblePizzaIndex = pizzaAssets.length - 1;
}
isRotating = true;
update();
Future.delayed(animDuration).then((value) => isRotating = false);
}

//Right swiping callback of pizza image. We are updating the pizza index on swiping.
onRightSwipe() {
if (isRotating) return;//This will help in preventing multiple calls
rotationTurn.value = rotationTurn.value + 1;
if (currentVisiblePizzaIndex < pizzaAssets.length - 1) {
currentVisiblePizzaIndex = currentVisiblePizzaIndex + 1;
} else {
currentVisiblePizzaIndex = 0;
}
isRotating = true;
update();
Future.delayed(animDuration).then((value) => isRotating = false);
}

@override
void onReady() {
super.onReady();
//Update the UI first time.
Future.delayed(const Duration(milliseconds: 50)).then((value) => update());
}
}

Let's Add Implicit Animation 🎬

We will create two widgets PizzaWidget() and PizzaSizeWidget() this two stateless widget will contain implicit animation for our Pizza app.

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:modern_dominos/data/pizza_controller.dart';
import 'package:modern_dominos/generated/assets.dart';
import 'package:modern_dominos/ui/arrow_painter.dart';
import '../utils/constants.dart';

class PizzaWidget extends StatelessWidget {
final int index;
const PizzaWidget(this.index, {Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
final ctr = Get.put(PizzaController());

//This will change the visiblity of pizza image smmoothly according to current selected pizza.
return AnimatedOpacity(
opacity: ctr.currentVisiblePizzaIndex == index ? 1 : 0,
duration: ctr.animDuration,
//This will rotate the pizza image smmoothly according to current selected pizza.
child: AnimatedRotation(
duration: ctr.animDuration,
turns: ctr.rotationTurn.value,
child: Stack(
children: [
//Bg star image
Positioned(
top: 240,
child: Transform.rotate(
angle: math.pi / 4,
child: Opacity(
opacity: 0.5,
child: Image.asset(Assets.imagesStars),
),
),
),
//Arrow widgets to depict swipe option. We using ArrowPainter to draw arrow.
Positioned(
left: 62,
right: 62,
top: 350,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Transform.rotate(
angle: math.pi - math.pi / 7,
child: CustomPaint(
painter: ArrowPainter(
strokeColor: Colors.grey,
strokeWidth: 1,
arrowLength: 100,
paintingStyle: PaintingStyle.fill,
),
child: const SizedBox(width: 100),
),
),
Transform.rotate(
angle: math.pi / 7,
child: CustomPaint(
painter: ArrowPainter(
strokeColor: Colors.grey,
strokeWidth: 1,
arrowLength: 100,
paintingStyle: PaintingStyle.fill,
),
child: const SizedBox(width: 100),
),
),
],
),
),
Column(
children: [
SizedBox(
height: 100,
width: Get.width - 64,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 18),
Text(
pizzaName[index],
textAlign: TextAlign.center,
style: GoogleFonts.montserrat(
fontSize: 32,
fontWeight: FontWeight.w800,
color: Colors.white,
),
),
const SizedBox(height: 12),
Text(
pizzaDesc[index],
textAlign: TextAlign.center,
style: GoogleFonts.montserrat(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.white,
),
),
],
),
),
const SizedBox(height: 200),
SizedBox(
height: 38,
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Padding(
padding: EdgeInsets.only(bottom: 4, right: 2),
child: Text(
'₹',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
//This will create a bounce animation on pizza prize
AnimatedScale(
scale: ctr.priceTextScale,
duration: ctr.textScaleAnimDuration,
child: Text(
ctr.pizzaPrice(index),
style: GoogleFonts.montserrat(
fontSize: 38,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
),
),
const SizedBox(height: 18),
Image.asset(pizzaAssets[index], height: ctr.pizzaSize),
const SizedBox(height: 348),
],
),
],
),
),
);
}
}

In the above PizzaWidget() we have used:

  • AnimatedOpacity this will smoothly show the current pizza and hide other pizzas. The opacity changes from 0 to 1 and vice versa.
  • AnimatedRotation this will smoothly rotate the pizza image. We have defined turns for rotation. The turns value changes from our PizzaController. On changing turns value, the pizza image rotates.
  • AnimatedScale this animates the pizza price. It gives a bounce like animation to pizza price when we change it.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:modern_dominos/data/pizza_controller.dart';
import '../utils/pizza_size_enum.dart';

class PizzaSizeWidget extends StatelessWidget {
const PizzaSizeWidget({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
const unSelectedSizeStyle = TextStyle(
fontSize: 18,
color: Colors.white,
fontWeight: FontWeight.w400,
);

final selectedSizeStyle = TextStyle(
fontSize: 18,
color: Colors.red.shade700,
fontWeight: FontWeight.w800,
);

final ctr = Get.put(PizzaController());

return Container(
width: Get.width,
margin: const EdgeInsets.symmetric(horizontal: 32),
child: Stack(
children: [
Column(
children: [
const SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Container(height: 0.5, color: Colors.grey),
),
const SizedBox(width: 42),
Expanded(
child: Container(height: 0.5, color: Colors.grey),
),
],
),
GetBuilder<PizzaController>(builder: (_) {
return Row(
children: [
Container(height: 58, width: 0.5, color: Colors.grey),
...List.generate(PizzaSizeEnum.values.length, (index) {
final size = PizzaSizeEnum.values[index];
final isSelected = ctr.selectedPizzaSize == size;
return Expanded(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => ctr.onSizeChanged(size),
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
size.name,
style: isSelected
? selectedSizeStyle
: unSelectedSizeStyle,
textAlign: TextAlign.center,
),
),
),
);
}),
Container(height: 58, width: 0.5, color: Colors.grey),
],
);
}),
Container(height: 0.5, color: Colors.grey),
const SizedBox(height: 8),
],
),
GetBuilder<PizzaController>(
builder: (_) {
//Changing the position of dot at bottom of options.
//We are calculating the new position and updating the UI to get smooth animaiton
return AnimatedPositioned(
duration: const Duration(milliseconds: 400),
curve: Curves.elasticOut,
left: ctr.pizzaSizeIndicatorPosition,
top: 64,
child: Container(
height: 6,
width: 6,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.red.shade700,
),
),
);
},
),
const Center(
child: Text(
'Size',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
)
],
),
);
}
}

In above PizzaSizeWidget() we have used AnimatedPositioned this animates the bottom option dot from one position to another. We have also defined curve for our implicit animation. Using the curve, we can change the behavior of animation. Curves.elasticOut give a bounce like movement to our dot widget.

Run the animations

In our project we have defined multiple implicit animations, this animation changes according to user input. Now we can run our project using below main() function.

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;

return GetMaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
textTheme: GoogleFonts.montserratTextTheme(textTheme),
),
home: HomeScreen(),
);
}
}

You can check the entire code below:

Thank you, and I hope this article helped you in some way. If you like the post, please support the article by sharing it. Have a nice day!👋

I am an mobile enthusiast 📱. My expertise is in Flutter and Android development, with more than 5 years of experience in the industry.

I would love to write technical blogs for you. Writing blogs and helping people is what I crave for 😊 .You can contact me at my email(abdulrehman0796@gmail.com) for any collaboration 👋

🔗 LinkedIn: https://www.linkedin.com/in/abdul-rehman-khilji/

--

--

Abdul Rehman

Mobile developer in love with Flutter and Android ♥️