Flutter tutorial: realistic sticky note

flutterclutter

flutter-clutter

Posted on July 29, 2020

Flutter tutorial: realistic sticky note

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

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.

import 'package:flutter/material.dart';
class StickyNote extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      painter: StickyNotePainter()
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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 1
We create the SVG path by tracing the outline of an actual image of a sticky note

Flutter sticky note Inkscape 2

We then get a shape resembling the real sticky note

That is the resulting path after having used the converter

Path path = new Path();
paint.color = Color(0xffffff00);
path = Path();
path.lineTo(0, 0);
path.cubicTo(0, 0, size.width * 0.01, size.height * 0.44, size.width * 0.01, size.height * 0.44);
path.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);
path.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);
path.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);
path.cubicTo(size.width * 0.51, size.height, size.width, size.height, size.width, size.height);
path.cubicTo(size.width, size.height, size.width, 0, size.width, 0);
path.cubicTo(size.width, 0, 0, 0, 0, 0);
canvas.drawPath(path, gradientPaint);
Enter fullscreen mode Exit fullscreen mode

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

@override
void paint(Canvas canvas, Size size) {
  _drawNote(size, canvas, Paint()..color=const Color(0xffffff00));
}
void _drawNote(Size size, Canvas canvas, Paint paint) {
  Path path = new Path();
  path.moveTo(0, 0);
  path.lineTo(size.width, 0);
  path.lineTo(size.width, size.height);
  double foldAmount = 0.12;
  path.lineTo(size.width * 3/4, size.height);
  path.quadraticBezierTo(
    size.width * foldAmount * 2, size.height, size.width * foldAmount, size.height - (size.height * foldAmount)
  );
  path.quadraticBezierTo(
    0, size.height - (size.height * foldAmount * 1.5), 0, size.height / 4
  );
  path.lineTo(0, 0);
  canvas.drawPath(path, paint);
}
Enter fullscreen mode Exit fullscreen mode

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

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.

@override
void paint(Canvas canvas, Size size) {
  Paint gradientPaint = _createGradientPaint(size);
  _drawNote(size, canvas, gradientPaint);
}
Paint _createGradientPaint(Size size) {
  Paint paint = new Paint();
  Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
  RadialGradient gradient = new RadialGradient(
    colors: [const Color(0xffffff00), const Color(0xffeeee00)],
    radius: 1.0,
    stops: [0.5, 1.0],
    center: Alignment.bottomLeft
  );
  paint.shader = gradient.createShader(rect);
  return paint;
}
Enter fullscreen mode Exit fullscreen mode

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

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.

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

void _drawShadow(Size size, Canvas canvas) {
  Rect rect = Rect.fromLTWH(12, 12, size.width - 24, size.height - 24);
  Path path = new Path();
  path.addRect(rect);
  canvas.drawShadow(path, color, 12.0, true);
}
Enter fullscreen mode Exit fullscreen mode
void _drawShadow(Size size, Canvas canvas) {
  Paint shadowPaint = Paint()
    ..maskFilter = MaskFilter.blur(BlurStyle.normal, 8)
    ..color = Colors.black.withOpacity(0.16);
  Path shadowPath = new Path();
  shadowPath.moveTo(0, 24);
  shadowPath.lineTo(size.width, 0);
  shadowPath.lineTo(size.width, size.height);
  shadowPath.lineTo(size.width / 6, size.height);
  shadowPath.quadraticBezierTo(
    -2, size.height + 2, 0, size.height - (size.width / 6)
  );
  shadowPath.lineTo(0, 0);
  canvas.drawPath(shadowPath, shadowPaint);
}
Enter fullscreen mode Exit fullscreen mode

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 shadow 1

Flutter sticky note shadow 2

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.

class StickyNote extends StatelessWidget {
  StickyNote({
    this.color = const Color(0xffffff00)
  });
  final Color color;
...
StickyNotePainter({
  this.color
});
class StickyNotePainter extends CustomPainter {
  StickyNotePainter({
    this.color
  });
  Color color;
  ...
  Paint _createGradientPaint(Size size) {
    Paint paint = new Paint();
    Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
    RadialGradient gradient = new RadialGradient(
      colors: [_brightenColor(color), color],
      radius: 1.0,
      stops: [0.5, 1.0],
      center: Alignment.bottomLeft
    );
    paint.shader = gradient.createShader(rect);
    return paint;
  }
  Color _brightenColor(Color c, [int percent = 30]) {
    double floatValue = percent / 100;
    return Color.fromARGB(
        c.alpha,
        c.red + ((255 - c.red) * floatValue).round(),
        c.green + ((255 - c.green) * floatValue).round(),
        c.blue + ((255 - c.blue) * floatValue).round()
    );
  }
Enter fullscreen mode Exit fullscreen mode

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

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.

class StickyNote extends StatelessWidget {
  StickyNote({
    this.child,
    this.color = const Color(0xffffff00)
  });
  final Widget child;
  final Color color;
  @override
  Widget build(BuildContext context) {
    return Transform.rotate(
      angle: 0.01 * pi,
      child: CustomPaint(
        painter: StickyNotePainter(
          color: color
        ),
        child: Center(
          child: child
        )
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

child becomes the new constructor argument being also the named argument of the CustomPaint and optionally 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 sitcky note multiple colors

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.

FULL CODE

💖 💪 🙅 🚩
flutterclutter
flutter-clutter

Posted on July 29, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related