Rotate and flip an image in Flutter with or without animations

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

  • Preload images used in a widget: precacheImage
  • Rotate an image an arbitrary number of degrees: Transform.rotate
  • Flip an image on it's X or Y axis: Matrix4.rotationX, Matrix4.rotationY
  • Animate the flipping or rotation of an image

 

 

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 FlutterOpen link in a new tab

 

 

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:

  1. call controller.forward() to start the animation, causing controller.value gradually going from 0 to 1
  2. call setState(() => showFront = !showFront) to cause the image to change from "card front" to "card back"
  3. call controller.reverse() to start the animation again, causing controller.value gradually going from 1 back to 0

 

 

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();
            },
          ),
        ],
      ),
    );
  }
}

 

 

Author

Alex Domenici