10 February 2020
Categories: Software Development
Posted in: Flutter, Dart, Android, iOS, Web, Image, Widget
Note: the complete Dart source code is at the end of this article
Goals
precacheImage
Transform.rotate
Matrix4.rotationX
, Matrix4.rotationY
Rotate and flip an image in Flutter, with or without animations
This article will show you how to flip an image in Flutter on it's horizontal or vertical axis. I will also show you how to achieve this effect gradually via animations and how to change the image when flipping it (like showing the back of a card).
Preload images used in a widget
As a first step, we need to preload our images so that they'll be ready to use:
class CardWidget extends StatefulWidget {
@override
createState() => _CardWidgetState();
}
class _CardWidgetState extends BaseState<CardWidget> {
Image cardFront;
Image cardBack;
@override
void initState() {
super.initState();
// cardFront = Image.asset("assets/card-front.png");
// cardBack = Image.asset("assets/card-back.png");
cardFront = Image.network("https://alex.domenici.net/media/1206/card-front.png");
cardBack = Image.network("https://alex.domenici.net/media/1207/card-back.png");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(cardFront.image, context);
precacheImage(cardBack.image, context);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: <Widget>[
// TODO: The card will be shown here
],
),
);
}
}
It is important to preload the images because we don't want to see a gap or flicker (due to the image being loaded) when changing from an image to the other. For a complete explanation of image preloading on Flutter please see my article Preload images in a stateful widget on Flutter
Flipping an image on it's X, Y or Z axis
Now that we have our images, let's add the front image and a button to flip it on it's X axis:
class CardWidget ...
class _CardWidgetState extends State<CardWidget> {
Image cardFront;
Image cardBack;
bool showFront = true;
...
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: <Widget>[
Transform(
transform: Matrix4.rotationX(
(showFront ? 0 : -2) * Math.pi / 2
),
alignment: Alignment.center,
child: Container(
height: MediaQuery.of(context).size.height - 130,
alignment: Alignment.center,
child: cardFront,
),
),
FlatButton(
child: Text("flip me"),
onPressed: () {
// Flip the image
setState(() => showFront = !showFront);
},
),
],
),
);
}
}
To flip the image on it's Y axis, we would use method Transform.rotateY, as follows:
Transform(
transform: Matrix4.rotationY(
(showFront ? 0 : -2) * Math.pi / 2
),
alignment: Alignment.center,
child: Container(
height: MediaQuery.of(context).size.height - 130,
alignment: Alignment.center,
child: cardFront,
),
),
Rotate an image by a given angle
If we wish to rotate our image any arbitrary angle, we can use method Transform.rotate, as follows:
// Rotate image 45 degrees
Transform.rotate(
angle: Math.pi / 180 * 45,
alignment: Alignment.center,
child: Container(
height: MediaQuery.of(context).size.height - 130,
alignment: Alignment.center,
child: cardFront,
),
),
The following code rotates our image 360 degrees when we click the "flip" button, equivalent to flipping it on both the X and Y axis:
class CardWidget ...
class _CardWidgetState extends State<CardWidget> {
Image cardFront;
Image cardBack;
bool showFront = true;
...
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: <Widget>[
Transform.rotate(
angle: showFront ? 0 : Math.pi * -1,
alignment: Alignment.center,
child: Container(
height: MediaQuery.of(context).size.height - 130,
alignment: Alignment.center,
child: cardFront,
),
),
FlatButton(
child: Text("flip me"),
onPressed: () {
// Flip the image
setState(() => showFront = !showFront);
},
),
],
),
);
}
}
Change an image after an event, with no flickering or gaps
In order to show a different image (the back of the card) when we press the button, we use the following code:
class CardWidget ...
class _CardWidgetState extends State<CardWidget> {
Image cardFront;
Image cardBack;
bool showFront = true;
...
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: <Widget>[
showFront ? cardFront : cardBack,
FlatButton(
child: Text("flip me"),
onPressed: () {
// Flip the image
setState(() => showFront = !showFront);
},
),
],
),
);
}
}
Since we have preloaded all images, the transition between an image an the next will be smooth (no gaps of flickering effect).
Animate the flipping of an image
Now let's add an animation so that flipping the image will actually be achieved by rotating the card front image on it's X axis, changing the image when the rotation is half way through, then rotating the image (now the card back image) on it's X axis back to the original position.
In order to handle the animation we need to use a mixin called SingleTickerProviderStateMixin, then setup an instance of AnimationController to handle the animation:
class CardWidget ...
class _CardWidgetState extends State<CardWidget> with SingleTickerProviderStateMixin {
Image cardFront;
Image cardBack;
bool showFront = true;
AnimationController controller;
@override
void initState() {
super.initState();
// cardFront = Image.asset("assets/card-front.png");
// cardBack = Image.asset("assets/card-back.png");
cardFront = Image.network("https://alex.domenici.net/media/1206/card-front.png");
cardBack = Image.network("https://alex.domenici.net/media/1207/card-back.png");
// Initialize the animation controller: the animation will last 300 milliseconds
controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300), value: 0);
}
@override
void didChangeDependencies() ...
We will then use an AnimatedBuilder (a widget for building animations) in our build method, to handle the animation:
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: <Widget>[
AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Transform(
transform: Matrix4.rotationX(
(controller.value) * Math.pi / 2
),
alignment: Alignment.center,
child: Container(
height: MediaQuery.of(context).size.height - 130,
margin: EdgeInsets.only(top: 20),
alignment: Alignment.center,
child: showFront ? cardFront : cardBack,
),
);
},
),
Once the user taps the "flip" button we'll start the animation:
FlatButton(
child: Text("flip me"),
onPressed: () async {
// Flip the image
await controller.forward();
setState(() => showFront = !showFront);
await controller.reverse();
},
),
The steps we are performing to run the animation are:
Complete source code
Putting it all together, please find a complete Flutter sample below.
Happy Coding! ;)
import 'package:flutter/material.dart';
import 'dart:math' as Math;
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: CardWidget(),
),
),
);
}
}
class CardWidget extends StatefulWidget {
@override
createState() => _CardWidgetState();
}
class _CardWidgetState extends State<CardWidget> with SingleTickerProviderStateMixin {
Image cardFront;
Image cardBack;
bool showFront = true;
AnimationController controller;
@override
void initState() {
super.initState();
// cardFront = Image.asset("assets/card-front.png");
// cardBack = Image.asset("assets/card-back.png");
cardFront = Image.network("https://alex.domenici.net/media/1206/card-front.png");
cardBack = Image.network("https://alex.domenici.net/media/1207/card-back.png");
// Initialize the animation controller
controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300), value: 0);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(cardFront.image, context);
precacheImage(cardBack.image, context);
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Column(
children: <Widget>[
AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Transform(
transform: Matrix4.rotationX((controller.value) * Math.pi / 2),
alignment: Alignment.center,
child: Container(
height: MediaQuery.of(context).size.height - 130,
margin: EdgeInsets.only(top: 20),
alignment: Alignment.center,
child: showFront ? cardFront : cardBack,
),
);
},
),
FlatButton(
child: Text("flip me"),
onPressed: () async {
// Flip the image
await controller.forward();
setState(() => showFront = !showFront);
await controller.reverse();
},
),
],
),
);
}
}