Changing colour of CustomPaint changes for all previous points

2,132

Solution 1

I think, for different colors you have to use different Paints. I've added small changes to your code, it works.

class DrawPageState extends State<DrawPage> with TickerProviderStateMixin {
  ...
  List<Painter> painterList = [];

  @override
  Widget build(BuildContext context) {
    ...
          child: CustomPaint(
            painter: Painter(
                points: points, color: color, strokeCap: strokeCap, strokeWidth: strokeWidth, painters: painterList),
            size: Size.infinite,
          ),
    ...
                onPressed: () async {
                  Color temp;
                  temp = await showDialog(
                      context: context,
                      builder: (context) => ColorDialog());
                  if (temp != null) {
                    setState(() {
                      painterList
                          .add(Painter(points: points.toList(), color: color, strokeCap: strokeCap, strokeWidth: strokeWidth));
                      points.clear();
                      strokeCap = StrokeCap.round;
                      strokeWidth = 5.0;
                      color = temp;
                    });
                  }
    ...
  }
}

class Painter extends CustomPainter {
  List<Offset> points;
  Color color;
  StrokeCap strokeCap;
  double strokeWidth;
  List<Painter> painters;

  Painter({this.points, this.color, this.strokeCap, this.strokeWidth, this.painters = const []});

  @override
  void paint(Canvas canvas, Size size) {
    for (Painter painter in painters) {
      painter.paint(canvas, size);
    }

    Paint paint = new Paint()
      ..color = color
      ..strokeCap = strokeCap
      ..strokeWidth = strokeWidth;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null) {
        canvas.drawLine(points[i], points[i + 1], paint);
      }
    }
  }

  @override
  bool shouldRepaint(Painter oldDelegate) => oldDelegate.points != points;
}

Solution 2

2020, I have a nice solution for this, because the actually chosen doesn't work for me and I see some redundant calls.

So, I start creating a small class:

class _GroupPoints {
  Offset offset;
  Color color;
  _GroupPoints({this.offset, this.color});
}

next, i declare my CustomPainter like this:

class Signature extends CustomPainter {
  List<_GroupPoints> points;
  Color color;
  Signature({
    this.color,
    this.points,
  });

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = new Paint()
       // if you need this next params as dynamic, you can move it inside the for part
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;

    for (int i = 0; i < newPoints.length - 1; i++) {
      paint.color = points[i].color;
      if (points[i].offset != null && points[i + 1].offset != null) {
        canvas.drawLine(points[i].offset, points[i + 1].offset, paint);
      }
      canvas.clipRect(Offset.zero & size);
    }
  }

  @override
  bool shouldRepaint(Signature oldDelegate) => true;
}

And on my widget:

...
class _MyPageState extends State<MyPage> {
  ...
  List<_GroupPoints> points = [];
  ...

                           Container(
                                  height: 500,
                                  width: double.infinity,
                                  child: GestureDetector(
                                    onPanUpdate: (DragUpdateDetails details) {
                                      setState(() {
                                        points = new List.from(points)
                                          ..add(
                                            new _GroupPoints(
                                              offset: details.localPosition,
                                              color: myDynamicColor,
                                            ),
                                          );
                                      });
                                    },
                                    onPanEnd: (DragEndDetails details) {
                                      points.add(
                                        _GroupPoints(
                                            color: myDynamicColor,
                                            offset: null),
                                      );
                                    },
                                    child: CustomPaint(
                                      painter: Signature(
                                        newPoints: points,
                                        color: myDynamicColor,
                                      ),
                                    ),
                                  ),
                                ),
                              }

On this way we can use multiple draws of points with their respective color. Hope this can help anybody.

Share:
2,132
soupjake
Author by

soupjake

Updated on December 07, 2022

Comments

  • soupjake
    soupjake over 1 year

    So I'm trying to create a draw app using Flutter following the "signature canvas" method. However, I'm having trouble being able to change the colour of the CustomPaint object without it already changing the colours for each line draw before the change as shown here: enter image description here

    As you can see, the colour change happens once the Page Widget's state is changed (either by clicking on the main FAB or if I were to draw on the canvas again). Here is my code below for my DrawPage:

    class DrawPage extends StatefulWidget {
      @override
      DrawPageState createState() => new DrawPageState();
    }
    
    class DrawPageState extends State<DrawPage> with TickerProviderStateMixin {
      AnimationController controller;
      List<Offset> points = <Offset>[];
      Color color = Colors.black;
      StrokeCap strokeCap = StrokeCap.round;
      double strokeWidth = 5.0;
    
      @override
      void initState() {
        super.initState();
        controller = new AnimationController(
          vsync: this,
          duration: const Duration(milliseconds: 500),
        );
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: GestureDetector(
              onPanUpdate: (DragUpdateDetails details) {
                setState(() {
                  RenderBox object = context.findRenderObject();
                  Offset localPosition =
                      object.globalToLocal(details.globalPosition);
                  points = new List.from(points);
                  points.add(localPosition);
                });
              },
              onPanEnd: (DragEndDetails details) => points.add(null),
              child: CustomPaint(
                painter: Painter(
                    points: points,
                    color: color,
                    strokeCap: strokeCap,
                    strokeWidth: strokeWidth),
                size: Size.infinite,
              ),
            ),
          ),
          floatingActionButton:
              Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
            Container(
              height: 70.0,
              width: 56.0,
              alignment: FractionalOffset.topCenter,
              child: ScaleTransition(
                scale: CurvedAnimation(
                  parent: controller,
                  curve: Interval(0.0, 1.0 - 0 / 3 / 2.0, curve: Curves.easeOut),
                ),
                child: FloatingActionButton(
                  mini: true,
                  child: Icon(Icons.clear),
                  onPressed: () {
                    points.clear();
                  },
                ),
              ),
            ),
            Container(
              height: 70.0,
              width: 56.0,
              alignment: FractionalOffset.topCenter,
              child: ScaleTransition(
                scale: CurvedAnimation(
                  parent: controller,
                  curve: Interval(0.0, 1.0 - 1 / 3 / 2.0, curve: Curves.easeOut),
                ),
                child: FloatingActionButton(
                  mini: true,
                  child: Icon(Icons.lens),
                  onPressed: () {},
                ),
              ),
            ),
            Container(
                height: 70.0,
                width: 56.0,
                alignment: FractionalOffset.topCenter,
                child: ScaleTransition(
                    scale: CurvedAnimation(
                      parent: controller,
                      curve:
                          Interval(0.0, 1.0 - 2 / 3 / 2.0, curve: Curves.easeOut),
                    ),
                    child: FloatingActionButton(
                        mini: true,
                        child: Icon(Icons.color_lens),
                        onPressed: () async {
                          Color temp;
                          temp = await showDialog(
                              context: context,
                              builder: (context) => ColorDialog());
                          if (temp != null) {
                            setState(() {
                              color = temp;
                            });
                          }
                        }))),
            FloatingActionButton(
              child: AnimatedBuilder(
                animation: controller,
                builder: (BuildContext context, Widget child) {
                  return Transform(
                    transform: Matrix4.rotationZ(controller.value * 0.5 * math.pi),
                    alignment: FractionalOffset.center,
                    child: Icon(Icons.brush),
                  );
                },
              ),
              onPressed: () {
                if (controller.isDismissed) {
                  controller.forward();
                } else {
                  controller.reverse();
                }
              },
            ),
          ]),
        );
      }
    }
    

    What I've tried so far:

    I've tried playing around with how the points are added to my List of Offsets as this list is recreated after each "draw" gesture such as just adding to the current List without recreating it but this breaks the "draw" gesture:

    setState(() {
      RenderBox object = context.findRenderObject();
      Offset localPosition =
         object.globalToLocal(details.globalPosition);
      points = new List.from(points);
      points.add(localPosition);
    });
    

    I've tried making a reference to the CustomPaint object or my Painter object outside of the build() scope and updating the color property that way but this also breaks the "draw" gesture.

    Any help would be greatly appreciated!

    Also, here's the code for my Painter class in case people wish to see it:

    class Painter extends CustomPainter {
      List<Offset> points;
      Color color;
      StrokeCap strokeCap;
      double strokeWidth;
    
      Painter({this.points, this.color, this.strokeCap, this.strokeWidth});
    
      @override
      void paint(Canvas canvas, Size size) {
        Paint paint = new Paint();
        paint.color = color;
        paint.strokeCap = strokeCap;
        paint.strokeWidth = strokeWidth;    
    
        for (int i = 0; i < points.length - 1; i++) {
          if (points[i] != null && points[i + 1] != null) {
            canvas.drawLine(points[i], points[i + 1], paint);
          }
        }
      }
    
      @override
      bool shouldRepaint(Painter oldPainter) => oldPainter.points != points;
    }