Flutter Transform with challenges

Alfonso García Santiago
9 min readJan 24, 2021

--

In this story, you will find some examples to learn about the Transform widget and 2 challenges to try at the end. We leave Rotation for the next story.

When it comes to transforming a container and its child widget tree, we can choose between:

  • using the container’s transform property
  • wrapping the container with a Transform widget

I prefer the second option: wrapping the Container widget in a Transform widget because this way we take advance of theTransform widget properties like origin. We can choose an origin for our transformation.

We will see here the four transforms we can do using the Transform widget:

  • Scale
  • Skew
  • Translate

Rotate Transform will be discussed in the next story.

Table of Contents

Examples and theory

  • Definition
  • Constructor vs. transform property
  • Scale
    Transform.scale constructor
    • scale argument
    • alignment argument
    • Scale down (0.5) with the origin in the top-left corner
    • Scale down (0.5) with the origin in the bottom-right corner
    • Scale down (0.5) with the origin in the center
    • Scale-up (1.7) with the origin in the top-left corner
  • Skew
    • There is not a Transform.skew constructor
    • alignment argument
    • Before transforming…
    • Skew along the X-axis with positive alpha
    • Skew along the X-axis with negative alpha
    • Changing the origin of the skew
    • Skewing along the Y-axis
  • Translate
    Transform.translate constructor
    • scale argument
    • Before translating…
    • Translate in the X-axis
    • Translate in the Y-axis
    • Translate in both X and Y axes
    • Translate and compose

Challenges

Rotate Transform will be discussed in the next story.

Read the following theory and examples to get background and ideas to solve the challenges you will find later.

EXAMPLES AND THEORY

Definition

Transform is a widget that applies a transformation before painting its child, which means the transformation is not taken into account when calculating how much space this widget’s child (and thus this widget) consumes.

Constructor vs. transform property

The Transform widget has a transform property with which we can tell the operation’s type we want: Scale, Skew, Translate, Rotate, or composition of some of them, and also describe how this transform operation will be.

However, instead of using the transform property of the Transform widget, we will try to use the specific constructor if it exists. I mean:

  • Transform.scale
  • Transform.translate
  • Transform.rotate (we will discuss it in the next story)

There is not a constructor for skew, so we will use the Transform widget with the transform property: Transform.skewX(double alpha) or Transform.skewY(double alpha) or Transform.skewZ(double alpha).

We will see how to use it throughout the examples in this story.

Scale

Transform.scale constructor

As we have already said, we can use the constructor Transform.scale instead of transform property of Transform widget.

scale argument

The scale argument must not be null. It gives the scalar by which to multiply the x and y axes.

alignment argument

The alignment controls the origin of the scale; by default, this is the center of the box.

The origin of the scale is the point that maintains the same in the scale operation, is the point towards and from which the scale is done.

Let’s see some examples.

Before transforming…

Scale down (0.5) with the origin in the top-left corner

Transform.scale(
alignment: Alignment.topLeft,
scale: 0.50,
child: Container(
color: Colors.teal,
width: 180,
height: 180,
),
)

Transform.scale is a widget. Now we put this widget in the widget tree of our App and, in the simplest way, we set it as the body property of our App’s Scaffold.

MaterialApp(
title: 'MyApp',
home: Scaffold(
appBar: AppBar(
title: Text('Translate'),
),
body: Transform.scale(
alignment: Alignment.topLeft,
scale: 0.50,
child: Container(
color: Colors.teal,
width: 180,
height: 180,
),
),
),
)

Scale down (0.5) with the origin in the bottom-right corner

Transform.scale(
alignment: Alignment.bottomRight,
scale: 0.50,
child: Container(
color: Colors.teal,
width: 180,
height: 180,
),
)

Scale down (0.5) with the origin in the center

Transform.scale(
alignment: Alignment.center,
scale: 0.50,
child: Container(
color: Colors.teal,
width: 180,
height: 180,
),
)

Scale-up (1.7) with the origin in the top-left corner

Black entities are informative, do not belong to the screenshot.
Transform.scale(
alignment: Alignment.topLeft,
scale: 1.70,
child: Container(
color: Colors.teal,
width: 180,
height: 180,
),
)

Skew

There is not a Transform.skew constructor.

So we will use the Transform widget with the transform property: Transform.skewX(double alpha) or Transform.skewY(double alpha) or Transform.skewZ(double alpha).

alignment argument

The alignment controls the origin of the skew; by default, this is the top-left corner (unlike the origin of the scale which remember that was the center)

Before transforming…

Before seeing how skew transformation works, we are going first to prepare the scene. Before skewing the blue container, we will put it inside another one, a grey-background container, which will be used as a background to see clearer the skewed blue container. We will also use this grey-background container to define the margins to the screen top and left sides. We do not want to place the margin definition in the blue-container because, in that case, the skew operation would affect the margins too.

Container( //grey-background container
margin: EdgeInsets.only(top: 60, left: 60),
color: Color(0xFFCCCCCC), //grey
width: double.infinity,
height: 240,
alignment: Alignment.topLeft, //align its child (blue-container)

child: Container( // blue container
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(10.0),
),
width: 220,
height: 120,
),
)

The blue container will be the target of the skew transformation. Let’s see it!

Skew along the X-axis with positive alpha

Black entities are informative, do not belong to the screenshot
Container( //grey-background container
margin: EdgeInsets.only(top: 60, left: 60),
color: Color(0xFFCCCCCC), //grey
width: double.infinity,
height: 240,
alignment: Alignment.topLeft, //align its child (blue-container)

child: Transform(
transform: Matrix4.skewX(0.3),
child: Container(
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(10.0),
),
width: 220,
height: 120,
),
),
)

If we do not give value for the alignment property of the Transform widget, then the skew transformation’s origin will get the default value (top-left corner).

Skew along the X-axis with negative alpha

Black entities are informative, do not belong to the screenshot.
Container( //grey-background container
margin: EdgeInsets.only(top: 60, left: 60),
color: Color(0xFFCCCCCC), //grey
width: double.infinity,
height: 240,
alignment: Alignment.topLeft, //align its child (blue-container)

child: Transform(
transform: Matrix4.skewX(-0.3),
child: Container(
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(10.0),
),
width: 220,
height: 120,
),
),
)

Changing the origin of the skew

Now we will apply a skew transformation but changing the origin from the default value (top-left corner) to the bottom-left corner (in this case).

We can do this by setting the alignment property of the Transform widget to Alignment.bottomLeft value.

Black entities are informative, do not belong to the screenshot.
Container( //grey-background container
margin: EdgeInsets.only(top: 60, left: 60),
color: Color(0xFFCCCCCC),
width: double.infinity,
height: 240,
alignment: Alignment.topLeft, //align its child (blue-container)
child: Transform(
transform: Matrix4.skewX(-0.3),
alignment: Alignment.bottomLeft, //changing the origin
child: Container(
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(10.0),
),
width: 220,
height: 120,
),
),
)

Skewing along the Y-axis

Black entities are informative, do not belong to the screenshot.
Container( //grey-background container
margin: EdgeInsets.only(top: 60, left: 60),
color: Color(0xFFCCCCCC),
width: double.infinity,
height: 240,
alignment: Alignment.topLeft, //align its child (blue-container)

child: Transform(
transform: Matrix4.skewY(0.3),
child: Container(
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(10.0),
),
width: 220,
height: 120,
),
),
)

Translate

Transform.translate constructor

We will use the constructor Transform.translate instead of transform property of Transform widget.

offset argument

It must not be null. It specifies the translation.

Before translating…

The design below is the initial container before being translated.

Container(
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(15.0),
),
width: 100,
height: 100,
)

Translate in the X-axis

Black entities are informative, do not belong to the screenshot.
Transform.translate(
offset: Offset(100, 0),
child: Container(
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(15.0),
),
width: 100,
height: 100,
),
)

Translate in the Y-axis

Black entities are informative, do not belong to the screenshot.
Transform.translate(
offset: Offset(0, 100),
child: Container(
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(15.0),
),
width: 100,
height: 100,
),
)

Translate in both X and Y axes

Black entities are informative, do not belong to the screenshot.
Transform.translate(
offset: Offset(100, 100),
child: Container(
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(15.0),
),
width: 100,
height: 100,
),
)

Translate and compose

We are going now to compose several containers, some of them being translated.

Black entities are informative, do not belong to the screenshot.

Every child widget is a translated version of its parent.

Container(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(15.0),
),
width: 100,
height: 100,
child: Transform.translate(
offset: Offset(0, 60),
child: Container(
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(15.0),
),
width: 100,
height: 100,
child: Transform.translate(
offset: Offset(0, 60),
child: Container(
decoration: BoxDecoration(
color: Colors.amber,
borderRadius: BorderRadius.circular(15.0),
),
width: 100,
height: 100,
),
),
),
),
)

CHALLENGES

Now, it is time to put into practice what you have just learned. Enjoy the following challenges and take them seriously. They will boost your level !!

The first part of the challenge

Could you achieve the right design from the left design, in the screenshot below, by scaling the text up (not by increasing the text’s size)?

Left-design code:

Center(
child: Text(
'Scale',
style: TextStyle(
fontSize: 36,
color: Colors.black,
),
),
)

And now, stop reading and try to do it by yourself before seeing my solution.

Good luck!

My solution

This is not the only possible solution; it is just my solution…

Center(
child: Transform.scale(
alignment: Alignment.center,
scale: 24,
child: Text(
'Scale',
style: TextStyle(
fontSize: 36,
color: Colors.black,
),
),
),
)

The second part of the challenge

Once you got the first part of the challenge, now you must use the previous fragment of code and apply another Transform.scale over the existing Transform.scale to go back to the left design (original size).

Try to do it by yourself before seeing my solution.

Good luck!

My solution

This is not the only possible solution; it is just my solution…

Center(
child: Transform.scale(
alignment: Alignment.center,
scale: (1 / 24).toDouble(),
child: Transform.scale(
alignment: Alignment.center,
scale: 24,
child: Text(
'Scale',
style: TextStyle(
fontSize: 36,
color: Colors.black,
),
),
),
),
)

The challenge

Considering the following colors:

const colors = [
Color(0xFF2257A3),
Color(0xFF2E22A3),
Color(0xFF6E22A3),
Color(0xFFA32296),
Color(0xFFA32357),
Color(0xFFA32D22)
];

The challenge consists of creating the design below by composing containers and using translation.

Try to do it the way you want, and once you get it done, I encourage you to do it again by using a recursive method to get the widget tree (if this method was not your first choice).

Try to do it by yourself before seeing my solution.

Good luck!

My solution

This is not the only possible solution; it is just my solution…

I have created a separate widget for this exercise. To do it more reusable, it receives a list of colors as a parameter in the constructor.

class Challenge extends StatelessWidget {

final List<Color> colors;

Challenge(this.colors);
@override
Widget build(BuildContext context) {
return getWidget(0);
}

Widget getWidget(int index) {
if (index < colors.length) {
return Transform.translate(
offset: (index == 0) ? Offset(0, 0) : Offset(40, 40),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colors[index],
),
width: 100,
height: 100,
child: getWidget(index + 1),
),
);
}
else
return null;
}
}

We can call it as follows:

const kColors = [
Color(0xFF2257A3),
Color(0xFF2E22A3),
Color(0xFF6E22A3),
Color(0xFFA32296),
Color(0xFFA32357),
Color(0xFFA32D22)
];
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Challenge'),
),
body: Challenge(kColors),
),
),
);
}

You can do it even more reusable if you pass the circles' size and the translation’s offset as parameters.

If you have found this article enjoyable or it has helped you in some way, give me many 👏.Thanks for reading me!

--

--