flutter CustomPainter - how to cut out a hole in line path

205

Okay I found a solution for it.

I created a rect path with the size of the CustomPainter area and built the difference with the cutout hole path.

That created a cutout template:

    final rectWithCutout = Path.combine(
        PathOperation.difference,
        Path()
          ..addRect(
            Rect.fromCenter(
              center: Offset.zero,
              width: size.width,
              height: size.height,
            ),
          ),
        holePath);

enter image description here

Which I could clip the canvas with: canvas.clipPath(rectWithCutout);

The final result:

enter image description here

This is the working full code example again:

import 'dart:math';

import 'package:flutter/material.dart';

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(
        body: Center(
          child: OvalCustomPaint(),
        ),
      ),
    );
  }
}

class OvalCustomPaint extends StatelessWidget {
  const OvalCustomPaint({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: LayoutBuilder(
        builder: (context, constraints) {
          return Center(
            child: CustomPaint(
              painter: _Painter(),
              child: SizedBox(
                width: constraints.maxWidth,
                height: constraints.maxHeight,
              ),
            ),
          );
        },
      ),
    );
  }
}

class _Painter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    const curveRadius = 40.0;
    const legLength = 130.0;
    canvas.rotate(pi / 2);

    final ovalPaint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 2.5
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;

    const fixPoint = Offset.zero;

    //* OVAL LINE
    final ovalPath = Path()..moveTo(fixPoint.dx, fixPoint.dy);
    ovalPath.relativeArcToPoint(
      const Offset(curveRadius * 2, 0),
      radius: const Radius.circular(curveRadius),
    );
    ovalPath.relativeLineTo(0, legLength);
    ovalPath.relativeArcToPoint(
      const Offset(-curveRadius * 2, 0),
      radius: const Radius.circular(curveRadius),
    );
    ovalPath.relativeLineTo(0, -legLength);

    //* CLIP HOLE
    final holePath = Path();
    holePath.addArc(Rect.fromCircle(center: fixPoint, radius: 13), 0, 2 * pi);

    final rectWithCutout = Path.combine(
        PathOperation.difference,
        Path()
          ..addRect(
            Rect.fromCenter(
              center: Offset.zero,
              width: size.width,
              height: size.height,
            ),
          ),
        holePath);

    canvas.clipPath(rectWithCutout);

    canvas.drawPath(
      ovalPath,
      ovalPaint,
    );
  }

  @override
  bool shouldRepaint(_Painter oldDelegate) => false;
}
Share:
205
tmaihoff
Author by

tmaihoff

Updated on December 19, 2022

Comments

  • tmaihoff
    tmaihoff over 1 year

    I have a CustomPaint which paints an oval.

    I want to cut out a hole at a specific position which I couldn't figure out yet how that works.

    I tried:

    canvas.drawPath(
          Path.combine(PathOperation.difference, ovalPath, holePath),
          ovalPaint,
        );
    

    but that doesn't cut the hole, but gives me the following result:

    enter image description here

    But this is what I want to achieve: enter image description here

    This oval is just an example, the "real" custom paint is gonna get more complex and I need more than just one cutout. So just painting several lines is not an alternative. I want to first define the path and then apply a cutout (or even inverted clipping) to get the hole.

    Is that possible?

    Here is a full working example of what I have:

    import 'package:flutter/material.dart';
    import 'dart:math';
    
    const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData.dark().copyWith(
            scaffoldBackgroundColor: darkBlue,
          ),
          debugShowCheckedModeBanner: false,
          home: const Scaffold(
            body: Center(
              child: OvalCustomPaint(),
            ),
          ),
        );
      }
    }
    
    class OvalCustomPaint extends StatelessWidget {
      const OvalCustomPaint({Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return Center(
          child: LayoutBuilder(
            builder: (context, constraints) {
              return Center(
                child: CustomPaint(
                  painter: _Painter(),
                  child: SizedBox(
                    width: constraints.maxWidth,
                    height: constraints.maxHeight,
                  ),
                ),
              );
            },
          ),
        );
      }
    }
    
    class _Painter extends CustomPainter {
    
      @override
      void paint(Canvas canvas, Size size) {
        canvas.translate(size.width / 2, size.height / 2);
        const curveRadius = 50.0;
        const legLength = 150.0;
        canvas.rotate(pi/2);
    
    
        final ovalPaint = Paint()
          ..color = Colors.blue
          ..strokeWidth = 2.5
          ..style = PaintingStyle.stroke
          ..strokeCap = StrokeCap.round;
    
        const fixPoint = Offset.zero;
    
        //* OVAL LINE
        final ovalPath = Path()..moveTo(fixPoint.dx, fixPoint.dy);
        ovalPath.relativeArcToPoint(
          const Offset(curveRadius * 2, 0),
          radius: const Radius.circular(curveRadius),
        );
        ovalPath.relativeLineTo(0, legLength);
        ovalPath.relativeArcToPoint(
          const Offset(-curveRadius * 2, 0),
          radius: const Radius.circular(curveRadius),
        );
        ovalPath.relativeLineTo(0, -legLength);
    
    
        //* CLP HOLE
        final holePath = Path();
        holePath.addArc(Rect.fromCircle(center: fixPoint, radius: 13), 0, 2 * pi);
    
        
        canvas.drawPath(
          Path.combine(PathOperation.difference, ovalPath, holePath),
          ovalPaint,
        );
      }
    
      @override
      bool shouldRepaint(_Painter oldDelegate) => false;
    
    }
    
    • pskink
      pskink about 2 years
      do you want to create some kind of stadium shaped progress indicator?
    • tmaihoff
      tmaihoff about 2 years
      no, nothing like that. This example only points out the problem. The real shape is gonna be more complex
    • pskink
      pskink about 2 years
      you could try PathMetric.extractPath method
    • tmaihoff
      tmaihoff about 2 years
      could you provide a little more information? What should I do with extracted paths then?
    • pskink
      pskink about 2 years
      PathMetric.extractPath returns a Path - you draw it with Canvas.drawPath method - canvas.drawPath(extracedPath, ovalPaint);
  • tmaihoff
    tmaihoff about 2 years
    Thanks @Yeasin Sheikh, but I need to cut out portions of after I draw it. Just altering the length does not work because my "real" is more complex and I might also cut out portions of the arc for example
  • Yeasin Sheikh
    Yeasin Sheikh about 2 years
    How about using Clipper