Making freely moving canvas drawing app in flutter

94 Views Asked by At

I'm trying to make a drawing app, which the canvas can rotate, zoom and transfer freely.

The rotate, zoom and transfer action should be only activated when it sense two fingers. Else, when it pan on the canvas, it should be draw.

I made a image canvas, but now I'm struggling with painting on it.

This is a code, i got referenced to make it: https://github.com/JideGuru/flutter_drawing_board/tree/master

Here's the code of the widget.

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:label_up_study/models/sketch.dart';

class TestScreen1 extends StatefulWidget {
  const TestScreen1({super.key});

  @override
  State<TestScreen1> createState() => _TestScreen1State();
}

class _TestScreen1State extends State<TestScreen1> {
  Offset _offset = Offset.zero;
  late Offset _startOffset;
  late Offset _previousOffset;
  double _scale = 0.75;
  late double _previousScale;
  double _rotation = 0;
  Sketch _sketches = Sketch(points: []);
  List<Sketch> _allSketches = [];
  List<int> _pointerCounts = [];

  @override
  void initState() {
    // TODO: implement initState
    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      // TODO: 
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    // print(_sketches.points);
    return Scaffold(
      body: GestureDetector(
        onScaleStart: (ScaleStartDetails details) {
          _pointerCounts.clear();
          _pointerCounts.add(details.pointerCount);
          if (details.pointerCount == 2) {
            _startOffset = details.focalPoint;
            _previousOffset = _offset;
            _previousScale = _scale;
          }
        },
        onScaleUpdate: (ScaleUpdateDetails details) {
          _pointerCounts.add(details.pointerCount);
          if (details.pointerCount == 2) {
            setState(() {
              _rotation = details.rotation;
              _scale = _previousScale * details.scale;
              final Offset normalizedOffset =
                  (_startOffset - _previousOffset) / _previousScale;
              _offset =
                  details.focalPoint - normalizedOffset * _scale;
            });
          }
        },
        child: Stack(
          alignment: Alignment.center,
          children: [
            Container(
              color: Colors.grey,
            ),
            Transform.translate(
              offset: _offset,
              child: Transform.rotate(
                angle: _rotation,
                child: Transform.scale(
                  scale: _scale,
                  child: RepaintBoundary(
                    child: Listener(
                      onPointerDown: (pointEvent) {
                        if (_pointerCounts.where((element) => element == 1).toList().length > 5){
                          // print('onPointerDown');
                          setState(() {
                            _sketches.points.add(pointEvent.localPosition);
                          });
                        }
                      },
                      onPointerMove: (pointEvent) {
                        if (_pointerCounts.where((element) => element == 1).toList().length > 5){
                          // print('onPointerMove');
                          setState(() {
                            _sketches.points.add(pointEvent.localPosition);
                          });

                        }
                      },
                      onPointerUp: (pointEvent) {
                        if (_pointerCounts.where((element) => element == 1).toList().length > 5){
                          setState(() {
                            _allSketches = List<Sketch>.from(_allSketches)..add(_sketches!);
                            _sketches = Sketch(points: []);
                          });
                        }
                        // _sketches.points.clear();
                      },
                      child: CustomPaint(
                        foregroundPainter: MyCustomPainter(
                          allSketches: _allSketches, // _sketches == null ? [] : [_sketches],
                          currentSketches: _sketches,
                        ),
                        // child: Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl-2.jpg'),
                        child: Image.asset('asset/img/hydrocolloid_dressing.jpg'),
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class MyCustomPainter extends CustomPainter {
  final List<Sketch> allSketches;
  final Sketch currentSketches;


  MyCustomPainter({required this.allSketches, required this.currentSketches});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()..style = PaintingStyle.fill;

    for(int i=0; i < allSketches.length; i++){  //Sketch sketch in allSketches){
      final points = allSketches[i].points;
      if(points.isEmpty){
        return;
      }
      final allPath = Path();
      final currentPath = Path();
      allPath.moveTo(points.first.dx, points.first.dy);
      // currentPath.moveTo(currentSketches.points.first.dx, currentSketches.points.first.dy);

      for (int j=1; j < points.length -1 ; ++j){
        // if (currentSketches.points.isNotEmpty && currentSketches!.points.length == points.length){
        //   final cp0 = currentSketches!.points[j];
        //   final cp1 = currentSketches!.points[j+1];
        //   currentPath.quadraticBezierTo(cp0.dx, cp0.dy, (cp0.dx + cp1.dx)/2, (cp0.dy + cp1.dy)/2);
        // }
        final p0 = points[j];
        final p1 = points[j+1];
        allPath.quadraticBezierTo(p0.dx, p0.dy, (p0.dx + p1.dx)/2, (p0.dy + p1.dy)/2);

      }
      // canvas.drawPath(currentPath, paint);
      canvas.drawPath(allPath, paint);
    }
  }

  @override
  bool shouldRepaint(MyCustomPainter delegate) {
    return true;
  }
}

import 'package:flutter/material.dart';

class Sketch {
  final List<Offset> points;
  // final Color color;
  // final double size;

  Sketch({
    required this.points,
    // required this.color,
    // required this.size,
  });
}

Annotations are what I did to solve my problem.

The code of reference - github link is doing painting action with two widgets - one is for currentpath, and one for allpath. When currentpath activate allpath under it will show the whole drawing.

The code I wrote only shows allpath. When pan starts and updates, there's nothing, but as I panup - shows draw.

But for me, as I don't want to make two widgets, and I don't want to give up using Path widget, I'm struggling how to show currentPath when Pan starts. And I want to make drawing shows when Pan starts and updates.

My first thought is drawing currentPath at the last item of allPath, but it makes other paints glitches so I determined not to use.

I also searched for using canvas.restore() or canvas.save() at last statement of paint method but it was no use.

Is there only way to make two widgets? Why I don't want to use two widget: as the image widget should move freely, if I make two widget of it, I think it will make lots of state rebuild so I don't want. Maybe this would be wrong idea?

1

There are 1 best solutions below

0
jungeyonoh On

I failed to make it as one widget, but I think kindda solved it - just wrapping it with stack

child: Stack(
          alignment: Alignment.center,
          children: [
            Container(
              color: Colors.grey,
            ),
            Transform.translate(
              offset: offset,
              child: Transform.rotate(
                angle: rotation,
                child: Transform.scale(
                  scale: scale,
                  child: Listener(
                    onPointerDown: (pointEvent) => onPointerDown(pointEvent),
                    onPointerMove: (pointEvent) => onPointerMove(pointEvent),
                    onPointerUp: (pointEvent) => onPointerUp(pointEvent),
                    child: RepaintBoundary(
                      child: Stack(
                        children: [
                          CustomPaint(
                            foregroundPainter: MyCustomPainter(
                              sketches: allSketches,
                            ),
                            child: image,
                          ),
                          CustomPaint(
                            foregroundPainter: MyCustomPainter(
                              sketches: sketches == null ? [] : [sketches],
                            ),
                            child: Container(
                              color: Colors.transparent,
                              width: width,
                              height: height,
                            ),
                          )
                        ],
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),

In this way it works sync and moving same. I just have to handle size of image.

But i think there will be more ideas, but I don't know still.