Create a sticky note in Flutter

Create a sticky note in Flutter

It’s said that the trend of skeuomorphism which Apple had made extensive use of until the release of iOS 7, is now celebrating its comeback and renamed itself to neumorphism or neo-skeuomorphism. Skeuomorphism denotes a UI-Design pattern in which virtual elements are created in way that they mimic the look and feel of their real-life relatives.

Let’s implement something as simple as a sticky note to see how easy it can be to create realistic looking UI-elements.

The goal

Flutter Sticky Note
The result we want to have

Let’s talk about what makes this sticky note look like a sticky note and not just like a plain rectangle containing text. In order to find that out, we have a look at a real image. A quick Google search brought me here. A summary of the important details:

  • The note has a dog-ear on the bottom left, indicating that it only sticks to the background at the top
  • The dog-ear changes the reflection of light making the part around it slightly brighter
  • There is a shadow underneath the note indicating that there is space between the note and the underlying surface
  • The shadow exceeds the usual boundaries of the note a little bit
  • The note is rotated because in the real world we rarely manage to stick it totally straight to the surface

The implementation

Let’s start with the basic setup by creating a StatelessWidget that does nothing but rendering our CustomPainter we will be creating afterwards.

 1import 'package:flutter/material.dart';
 2
 3class StickyNote extends StatelessWidget {
 4  @override
 5  Widget build(BuildContext context) {
 6    return CustomPaint(
 7      painter: StickyNotePainter()
 8    );
 9  }
10}

For the implementation of the StickyNotePainter we are going to choose an iterative way. Let’s start with the basic which is a yellow shape that is not quite rectangular as the bottom left corner is fold over.

I am going to propose two possibilities of implementing it. One quick and easy way and another way that involves only a bit more thinking.

The quick and easy way

Using the SVG converter we can easily create the desired SVG with an application that lets us create vector graphics and convert it to the path. In my case Inkscape was used to perform that kind of task.

Flutter sticky note inkscape
We create the SVG path by tracing the outline of an actual image of a sticky note
Flutter sticky note inkscape finished
We then get a shape resembling the real sticky note

That is the resulting path after having used the converter

 1Path path = new Path();
 2paint.color = Color(0xffffff00);
 3path = Path();
 4path.lineTo(0, 0);
 5path.cubicTo(0, 0, size.width * 0.01, size.height * 0.44, size.width * 0.01, size.height * 0.44);
 6path.cubicTo(size.width * 0.01, size.height * 0.44, size.width * 0.01, size.height * 0.56, size.width * 0.02, size.height * 0.64);
 7path.cubicTo(size.width * 0.03, size.height * 0.72, size.width * 0.03, size.height * 0.82, size.width * 0.06, size.height * 0.88);
 8path.cubicTo(size.width * 0.08, size.height * 0.95, size.width * 0.09, size.height * 0.96, size.width * 0.14, size.height * 0.97);
 9path.cubicTo(size.width * 0.51, size.height, size.width, size.height, size.width, size.height);
10path.cubicTo(size.width, size.height, size.width, 0, size.width, 0);
11path.cubicTo(size.width, 0, 0, 0, 0, 0);
12canvas.drawPath(path, gradientPaint);

Looks good enough in my opinion. But with a little bit of thinking we might be able to create it with less lines of code and get a better sense of what we want.

The “let’s think for a moment” way

 1@override
 2void paint(Canvas canvas, Size size) {
 3  _drawNote(size, canvas, Paint()..color=const Color(0xffffff00));
 4}
 5
 6void _drawNote(Size size, Canvas canvas, Paint paint) {
 7  Path path = new Path();
 8  path.moveTo(0, 0);
 9  path.lineTo(size.width, 0);
10  path.lineTo(size.width, size.height);
11
12  double foldAmount = 0.12;
13  path.lineTo(size.width * 3/4, size.height);
14
15  path.quadraticBezierTo(
16    size.width * foldAmount * 2, size.height, size.width * foldAmount, size.height - (size.height * foldAmount)
17  );
18  path.quadraticBezierTo(
19    0, size.height - (size.height * foldAmount * 1.5), 0, size.height / 4
20  );
21  path.lineTo(0, 0);
22
23  canvas.drawPath(path, paint);
24}

The upper left, upper right and lower right point are easily connected via lineTo() because this part of the sticky note is not special in any way. Then we add a line that follows the bottom line of the rectangle and has a quarter of the length. Using a bezier path we the create a curvy line to a point that is a little bit shifted to the inside from the bottom left point of the rectangle to mimic the fold. With a similar curve we then turn back to the left side of the rectangle and close the path by adding a line to the top left corner. How far the note is fold over is denoted by foldAmount.

Flutter sticky note first iteration
First iteration

Giving the sticky note a gradient

Now that the shape itself is finished, let’s take care of the color. We want to simulate a difference in the lighting due to the corner being fold over by applying a gradient.

 1@override
 2void paint(Canvas canvas, Size size) {
 3  Paint gradientPaint = _createGradientPaint(size);
 4  _drawNote(size, canvas, gradientPaint);
 5}
 6
 7Paint _createGradientPaint(Size size) {
 8  Paint paint = new Paint();
 9
10  Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
11  RadialGradient gradient = new RadialGradient(
12    colors: [const Color(0xffffff00), const Color(0xffeeee00)],
13    radius: 1.0,
14    stops: [0.5, 1.0],
15    center: Alignment.bottomLeft
16  );
17  paint.shader = gradient.createShader(rect);
18  return paint;
19}

We use a RadialGradient for that purpose and let the center be the bottom left of the shape. The color ranges from yellow in the corner to a darker yellow in the surrounding.

Flutter sticky note gradient
Second iteration with gradient

Putting a shadow under the sticky note

Without any further code, the sticky note does not look realistic. That’s because the rounded corner at the bottom left does not indicate being fold over. As long as we do not simulate depth, it will just look like the edge was rounded by cutting it. Let’s try to make it a little bit more realistic by simulating a space between the sticky note and the background. We do that by applying a shadow which is mostly seen under the bottom left corner.

1void _drawShadow(Size size, Canvas canvas) {
2  Rect rect = Rect.fromLTWH(12, 12, size.width - 24, size.height - 24);
3  Path path = new Path();
4  path.addRect(rect);
5  canvas.drawShadow(path, color, 12.0, true);
6}
 1void _drawShadow(Size size, Canvas canvas) {
 2  Paint shadowPaint = Paint()
 3    ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8)
 4    ..color = Colors.black.withOpacity(0.16);
 5  Path shadowPath = new Path();
 6  shadowPath.moveTo(0, 24);
 7  shadowPath.lineTo(size.width, 0);
 8  shadowPath.lineTo(size.width, size.height);
 9  shadowPath.lineTo(size.width / 6, size.height);
10  shadowPath.quadraticBezierTo(
11    -2, size.height + 2, 0, size.height - (size.width / 6)
12  );
13  shadowPath.lineTo(0, 0);
14  canvas.drawPath(shadowPath, shadowPaint);
15}

There are multiple options how we could approach the task of adding a shadow. There are two ways I want to show you.

The first way is fairly simple. We create a Rect that has a little offset so that it looks like the top is stuck to the background. The width and height are also reduced. We then take the drawShadow() method of the canvas to let it actually draw. This is the shadow that is used in Material widgets. One can provide an elevation that determines size and blurriness.

An alternative is to first draw a path and then use a MaskFilter on the Paint object that blurs the path.

Flutter sticky note with shadow
Third iteration with shadow: drawShadow
Flutter sticky note with shadow
Third iteration with shadow: blur

Adding a configurable color

As we know, in modern times, not every sticky note is yellow. It can be of any color. But our virtual sticky note still has a hardcoded color. Let’s make it dynamic so that the color can be specified from the outside.

 1class StickyNote extends StatelessWidget {
 2  StickyNote({
 3    this.color = const Color(0xffffff00)
 4  });
 5
 6  final Color color;
 7...
 8
 9StickyNotePainter({
10  this.color
11});
12
13class StickyNotePainter extends CustomPainter {
14  StickyNotePainter({
15    this.color
16  });
17
18  Color color;
19
20  ...
21
22  Paint _createGradientPaint(Size size) {
23    Paint paint = new Paint();
24
25    Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
26    RadialGradient gradient = new RadialGradient(
27      colors: [_brightenColor(color), color],
28      radius: 1.0,
29      stops: [0.5, 1.0],
30      center: Alignment.bottomLeft
31    );
32    paint.shader = gradient.createShader(rect);
33    return paint;
34  }
35
36  Color _brightenColor(Color c, [int percent = 30]) {
37    double floatValue = percent / 100;
38
39    return Color.fromARGB(
40        c.alpha,
41        c.red + ((255 - c.red) * floatValue).round(),
42        c.green + ((255 - c.green) * floatValue).round(),
43        c.blue + ((255 - c.blue) * floatValue).round()
44    );
45  }

color is added as a constructor argument to the widget and the Painter. In order to maintain the gradient that is brighter in the bottom left corner, we need a way to dynamically brighten the given color. The way we do it is fairly simple: we add a certain percentage of the original value for red, green and blue equally. This can be thought of as the luminance in that color model. Since RGB is an additive color model, we come nearer the white color by adding a value to every color channel. I also rotated it a bit, because it seems more realistic as no one can stick it perfectly straight to the background.

Flutter sticky note red
Third iteration with configurable color

Letting it have a child

Great, now we can have a sticky note in every color. Yet we have a quite empty sticky note, which makes them pretty useless. Instead, we want to be able to write something on it like in the reality. For that purpose, we want the sticky note to have the possibility to display a child.

 1class StickyNote extends StatelessWidget {
 2  StickyNote({
 3    this.child,
 4    this.color = const Color(0xffffff00)
 5  });
 6
 7  final Widget child;
 8  final Color color;
 9
10  @override
11  Widget build(BuildContext context) {
12    return Transform.rotate(
13      angle: 0.01 * pi,
14      child: CustomPaint(
15        painter: StickyNotePainter(
16          color: color
17        ),
18        child: Center(
19          child: child
20        )
21      ),
22    );
23  }
24}

child becomes the new constructor argument being also the named argument of the CustomPaint and optinally wrapped by a Center widget.
As a font, we take DK Yellow Lemon Sun by Hanoded, but certainly any other font that mimics hand-writing is suitable.

Flutter sticky notes different colors
Different sticky notes

Now we can spawn sticky notes with different colors and text on it. By mirroring the note, we can let the other bottom edge be fold over.

Wrap-up

Creating a realistic version of a sticky note is not that hard. It’s just about the shape, the resulting lighting being imitated by a gradient and the shadow underneath. This could be now part of a skeuomorphic design.

GET FULL CODE

Comments (4) ✍️

Waqas

Hi,

Awesome!! How can we use these sticky notes as ListView.builder items??

thanks WK

Reply to Waqas

Marc
In reply to Waqas's comment

Hello. Well, just like you would do it with any other widget. You would probably wrap the StickyNote with a SizedBox first. Here is a sample I created: https://codeshare.io/G6PK70
Reply to Marc

Jose
In reply to Marc's comment

Link is no more available https://codeshare.io/G6PK70. Do you still have the example code in safe place? I’d loved to see!
Reply to Jose

Marc
In reply to Jose's comment

Hey Jose!

Strange, I can still view the code. Do you have any plugins activated? Might try it in the incognito mode of your browser?

Anyways I copied off the code:

 1import 'package:flutter/material.dart';
 2import 'package:flutter_sticky_note/sticky_note_container.dart';
 3import 'sticky_note.dart';
 4
 5void main() {
 6  runApp(MyApp());
 7}
 8
 9class MyApp extends StatelessWidget {
10  @override
11  Widget build(BuildContext context) {
12    List<String> texts = ['this is the first sticky note text', 'this is the second one', 'and finally the third one'];
13    return MaterialApp(
14      home: ListView.builder(
15        itemBuilder: (context, index) {
16          return SizedBox(
17            width: 120,
18            height: 120,
19            child: StickyNoteContainer(
20              text: texts[index],
21            ),
22          );
23        },
24        itemCount: 3,
25      ),
26    );
27  }
28}
29
30class StickyNoteContainer extends StatelessWidget {
31  StickyNoteContainer({
32    this.text
33  });
34
35  final String text;
36
37  @override
38  Widget build(BuildContext context) {
39    return Scaffold(
40      body: Container(
41      color: Colors.white,
42        child: Center(
43          child: SizedBox(
44            width: 100,
45            height: 100,
46            child: Container(
47              color: Colors.white,
48              child: StickyNote(
49                child: Text(
50                  text,
51                  style: TextStyle(fontSize: 14),
52                )
53              )
54            )
55          )
56        )
57      )
58    );
59  }
60}

Greetings!

Comment this 🤌

You are replying to 's commentRemove reference