/flutter_painter

A Flutter widget to draw scribbles and text on a background image or color 🎨

Primary LanguageDartMIT LicenseMIT

Flutter Painter 🎨🖌️

pub package Buy Me A Pizza

A pure-Flutter package for painting.

Summary

Flutter Painter provides you with a widget that can be used to draw on it. Right now, it supports:

  • Free-style drawing: Scribble anything you want with any width and color.
  • Objects that you can move, scale and rotate in an easy and familiar way, such as:
    • Text with any TextStyle.
    • Shapes such as lines, arrows, ovals and rectangles with any Paint.
    • Images that can be flipped.
  • Free-style eraser to erase any part of a drawing or object you don't want on the painter.*

These are called drawables.

You can use a color or an image for the background of your drawing, and export your painting as an image.

Example

You can check out the example tab for an example on how to use the package.

The example is hosted here if you want to try it out yourself!

A video recording showing the example running:

Flutter Painter Video Demo

Usage

First, you'll need a PainterController object. The PainterController controls the different drawables, the background you're drawing on and provides the FlutterPainter widget with the settings it needs. Then, in your UI, use the FlutterPainter widget with the controller assigned to it.

class ExampleWidget extends StatefulWidget {
  const ExampleWidget({Key? key}) : super(key: key);

  @override
  _ExampleWidgetState createState() => _ExampleWidgetState();
}

class _ExampleWidgetState extends State<ExampleWidget> {
  PainterController controller = PainterController();

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: 300,
      height: 300,
      child: FlutterPainter(
        controller: controller,        
      ),
    );
  }
}

You can also use the FlutterPainter.builder constructor, which uses a builder method that automatically updates whenever a change happens in the controller, without using setState, callbacks, or listeners. However, this will perform worse than a StatefulWidget since it will rebuild more often, so it is recommended to use if the widget tree that depends on PainterController is simple.

class ExampleWidget extends StatefulWidget {
  const ExampleWidget({Key? key}) : super(key: key);

  @override
  _ExampleWidgetState createState() => _ExampleWidgetState();
}

class _ExampleWidgetState extends State<ExampleWidget> {
  PainterController controller = PainterController();

  @override
  Widget build(BuildContext context) {
    return FlutterPainter.builder(
      controller: controller,
      builder: (context, painter){
        return SizedBox(
          width: 300,
          height: 300,
          child: painter
        );
      }
    ); 
  }
}

NOTE: FlutterPainter does not define its own constraints on its size, so it is advised to use a widget that can provide its child with size constraints, such as SizedBox or AspectRatio (more on constraints here).

NOTE: If mutiple parts of your UI depend on the PainterController, you can use a ValueListeneableBuilder with the valueListenable being your controller, which will re-build automatically whenever the controller updates. This is the approach used in the example project.

Callbacks

FlutterPainter has some helpful callbacks that are called when internal changes happen in the widget itself.

  • onDrawableCreated: Called when a drawable is created from FlutterPainter. Passes the drawable as an arugment.
  • onDrawableDeleted: Called when a drawable is deleted from FlutterPainter. Passes the drawable as an arugment.
  • onSelectedObjectDrawableChanged: Called when the selected object drawable changes. This can be useful if you want to display some UI to edit the object's properties. Passes the selected object drawable as an argument.
    • If the drawable is updated (moved, for example), the passed drawable will become invalid. Make sure to use PainterController.selectedObjectDrawable to get the up-to-date value of the selected drawable.
  • onPainterSettingsChanged: Called when the settings of PainterController are changed from FlutterPainter itself. Passes the new settings as an argument.

PainterController

The PainterController is the heart of the operation of Flutter Painter. It controls the settings for FlutterPainter, its background, and all of its drawables, and the selected object drawable.

All setters on PainterController directly notify your FlutterPainter to respond and repaint. If you're using FlutterPainter.builder, the builder is automatically called to build the widget tree. If not, make sure to use setState and listen to the callbacks

NOTE: If you are using multiple painters, make sure that each FlutterPainter widget has its own PainterController, do not use the same controller for multiple painters.

Settings

There are currently three types of settings:

  • freeStyleSettings: They control the parameters used in drawing scribbles, such as the width and color. It also has a field to enable/disable scribbles, to prevent the user from drawing on the FlutterPainter.
  • textSettings: They mainly control the TextStyle of the text being drawn. It also has a focus node field (more on focus nodes here) to allow you to detect when the user starts and stops editing text.
  • objectSettings: These settings control objects that can be moved, scaled and rotated. Texts, shapes and images are all considered objects. It controls layout assist, which allows to center objects and rotate them at a right angle, and settings regarding the object controls for scaling, rotating and resizing.
  • shapeSettings: These control the paint and shape factory used (Shape Factory is used to create shapes), and whether the shape is drawn once or continiously.
  • scaleSettings: These settings control the scaling on the painter (zooming in/out). By default, scaling is disabled.

You can provide initial settings for the things you want to draw through the settings parameter in the constructor of the PainterController.

Each setting and sub-setting has extension setters and getters which you can use to read and modify the value of that setting.*

For example, this is how you would modify the stroke width of free-style drawings:

void setStrokeWidth(double value){
  controller.freeStyleStrokeWidth = value;
}

NOTE: If you're not using the extensions library, note that all of the settings objects are immutable and cannot be modified, so in order to change some settings, you'll have to create a copy of your current settings and apply the changes you need (this is similar to how you would copy ThemeData).

Background

You can also provide a background for the FlutterPainter widget from the controller. You can either use a color or an image as a background.

In order to use a color, you can simply call the backgroundDrawable extension getter on any color.*

void setBackground(){
  // Sets the background to the color black
  controller.background = Colors.black.backgroundDrawable;
}

In order to use an image, you will need an Image object from the dart library dart:ui. Since Flutter has an Image widget from the Material package, we'll refer to the image type we need as ui.Image.

import 'dart:ui' as ui;
ui.Image? myImage;

In order to get the ui.Image object from usual image sources (file, asset, network), you can use an ImageProvider with the image extension getter (Examples of ImageProvider: FileImage, MemoryImage, NetworkImage). This getter returns Future<ui.Image>.*

Then, you can use the backgroundDrawable extension getter on the ui.Image.*

void setBackground() async {
  // Obtains an image from network and creates a [ui.Image] object
  final ui.Image myImage = await NetworkImage('https://picsum.photos/960/720').image;
  // Sets the background to the image
  controller.background = myImage.backgroundDrawable;
}

The background can also be assigned from the constructor of PainterController directly.

Drawables

All the drawables drawn on FlutterPainter are stored and controller by the PainterController. On most use cases, you won't need to interact with the drawables directly. However, you may add, insert, replace or remove drawables from the code (without the user actually drawing them).

You can assign an initial list of drawables from the PainterController constructor to initialize the controller with them. You can also modify them from the controller, but be careful, use the methods from the PainterController itself and don't modify the drawables list directly.

DO:

void addMyDrawables(List<Drawable> drawables){
  controller.addDrawables(drawables);
}

DON'T:

void addMyDrawables(List<Drawable> drawables){
  controller.drawables.addAll(drawables);
}

Selected Object Drawable

PainterController also provides the currently-selected ObjectDrawable from the getter field PainterController.selectedObjectDrawable. This value stays up-to-date for any changes from the UI (the user selecting a new object drawable, for example). You can also programatically select and de-select an object drawable, granted it is in the list of drawables of the controller.

void selectObjectDrawable(ObjectDrawable drawable){
  controller.selectObjectDrawable(drawable);
}

void deselectObjectDrawable(){
  controller.deselectObjectDrawable();
}

The selected object drawable will also be automatically update if it is replaced or removed from the controller.

Rendering Image

From the PainterController, you can render the contents of FlutterPainter as a PNG-encoded ui.Image object. In order to do that, you need to provide the size of the output image. All the drawings will be scaled according to that size.

From the ui.Image object, you can convert it into a raw bytes list (Uint8List) in order to display it with Image.memory or save it as a file.

Uint8List? renderImage(Size size) async {
  final ui.Image renderedImage = await controller.renderImage(size);
  final Uint8List? byteData = await renderedImage.pngBytes;
  return byteData;
}

Notes

Erasing

Flutter Painter supports free-style erasing of drawables. However, whenever you use the erase mode, all object drawables will be locked in place and cannot be modified. This is done because erasing is just another layer, and if objects stayed movable, you'd be able to move from under and around erased areas of the painting, which doesn't make sense. If you un-do the action of using the erase mode, the objects will be unlocked again and you'll be able to move them.

Extensions

Flutter Painter consists of 3 libraries:

  • flutter_painter_pure, which contains all the APIs of Flutter Painter except for extensions on Flutter and Flutter Painter itself.
  • flutter_painter_extensions, which contains all the extensions defined and used by Flutter Painter.
  • flutter_painter which includes both previously mentioned libraries.

This is done so that people who don't want to use the extensions (conflicts, too many getters/setters, etc...) can use the pure library, and for people who only need the extensions to be able to import them alone.

If you're trying to use the extensions and they're showing as undefined, make sure you're importing the correct library.

Flutter Web

The html renderer for Flutter Web is not supported, and using it will cause unexpected behavior and errors (also includes the auto renderer which chooses the renderer depending on the device). If you're using it for Flutter Web, make sure to use --web-renderer canvaskit as an argument for your run/build commands. If you need to use auto or html for any reason (such as better performance), consider using another package.

If anybody is willing to help out the Flutter Web issue or with testing it would be highly appreciated (either contact me through my GitHub or contribute and post a pull request).

Support Me

If you like my work and would like to support me, feel free to do so :D

Buy Me A Pizza