Flutter - drawAtlas circular cutout

104

Excellent question! Although in your code, you are calling Rect.fromCircle to specify a clipping area, but it's still a rect (you are constructing a rect from a circle). The drawAtlas method only supports clipping rectangular areas from a larger image, as you probably already found out.

The key here is to use canvas.clipRRect to reduce the "allowed painting area". This is similar to adding a selection box in Photoshop. After doing this, anything that paints outside of the selection box will be ignored. In other words, by calling canvas.clipRRect, we will no longer be able to draw anything outside of the clipped area.

Presumably, after drawing a circular atlas, you might still want to draw other things to the canvas. And for those other things, you probably won't want to be limited to the clipped circle. To solve this, we can use canvas.saveLayer to save the state of the canvas before clipping happens, then do the circular clipping, then draw the altas, then finally, call canvas.restore to restore the previously saved state. Essentially this would clear the clipping, allowing us to draw more things anywhere on the canvas if needed.

Solution:

In short, the paint method might look something like this:

  void paint(Canvas canvas, Size size) {
    // Save the canvas state so we can clip it and then restore it later
    canvas.saveLayer(Rect.largest, Paint());
    // Clip the canvas, so we are only allowed to draw inside the circle
    canvas.clipRRect(
      RRect.fromRectAndRadius(
        Rect.fromLTWH(0, 0, 100, 100),
        Radius.circular(50),
      ),
    );
    // Draw the atlas (call your method)
    _drawAtlas(canvas, size);
    // Restore the canvas to its original state, so further drawings are not clipped
    canvas.restore();
  }

Demo:

demo picture

In the demo, when you press the button, the app will take a screenshot of the Flutter Logo inside the grey gradient container, and use that as the ui.image data. Then it will do the drawAtlas to draw a circular portion of the image to canvas.

Full demo code (paste everything to main.dart to run):

import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _globalKey = GlobalKey();

  ui.Image? _image;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          children: [
            RepaintBoundary(
              key: _globalKey,
              child: Container(
                width: 300,
                height: 300,
                decoration: BoxDecoration(
                  gradient: RadialGradient(
                    colors: [Colors.white, Colors.grey],
                  ),
                ),
                child: FlutterLogo(),
              ),
            ),
            ElevatedButton(
              child: Text('Press Me'),
              onPressed: () async {
                final render = (_globalKey.currentContext!.findRenderObject()
                    as RenderRepaintBoundary);
                final image = await render.toImage();
                setState(() {
                  _image = image;
                });
              },
            ),
            if (_image != null)
              CustomPaint(
                size: Size(300, 300),
                painter: MyPainter(_image!),
              ),
          ],
        ),
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  final ui.Image image;

  MyPainter(this.image);

  @override
  void paint(Canvas canvas, Size size) {
    // Save the canvas state so we can clip it and then restore it later
    canvas.saveLayer(Rect.largest, Paint());
    // Clip the canvas, so we are only allowed to draw inside the circle
    canvas.clipRRect(
      RRect.fromRectAndRadius(
        Rect.fromLTWH(0, 0, 100, 100),
        Radius.circular(50),
      ),
    );
    // Draw the atlas (call your method)
    _drawAtlas(canvas, size);
    // Restore the canvas to its original state, so further drawings are not clipped
    canvas.restore();
  }

  _drawAtlas(Canvas canvas, Size size) {
    canvas.drawAtlas(
      image,
      [
        /* Identity transform */
        RSTransform.fromComponents(
          rotation: 0.0,
          scale: 1,
          anchorX: 0,
          anchorY: 0,
          translateX: 0,
          translateY: 0,
        )
      ],
      [
        Rect.fromCircle(
          center: Offset(size.width / 2, size.height / 2),
          radius: 50,
        ),
      ],
      [],
      null,
      null,
      Paint(),
    );
  }

  @override
  bool shouldRepaint(oldDelegate) => true;
}
Share:
104
Zero Live
Author by

Zero Live

:)

Updated on December 01, 2022

Comments

  • Zero Live
    Zero Live over 1 year

    I'm trying to implement a circular cutout of an image and I am using drawAtlas for it. Here is my implementation so far:

    canvas.drawAtlas(
      image!,
      [
        /* Identity transform */
        RSTransform.fromComponents(
          rotation: 0.0,
          scale: 1,
          anchorX: 0,
          anchorY: 0,
          translateX: 0,
          translateY: 0,
        )
      ],
      [
        Rect.fromCircle(
          center: Offset(size.width / 2, size.height / 2),
          radius: 200,
        ),
      ],
      [],
      null,
      null,
      Paint(),
    );
    

    While it does work, it paints a rectangular image. I want to paint a circular cutout of the same image with some strokeWidth. Is it possible to do so using drawAtlas?

    • pskink
      pskink about 2 years
      "I want to paint a circular cutout of the same image [...]" - you need to clip your canvas then, btw, if you have single image without any transformations why dont you just use Canvas.drawImage method?
    • Zero Live
      Zero Live about 2 years
      Actually, I am already using canvas.drawImage. This is for like zoomed view of the portion of the image, kind of like a magnifier glass interface that I am trying to implement. I want to make multiple magnified viewports within a single image.
    • Ruchit
      Ruchit about 2 years
      could you add reference image or something?
  • Zero Live
    Zero Live about 2 years
    Thank you for the answer. Although I had to make a couple of modifications, canvas.clipRRect did the trick. Weird that when I tried with clip oval, it didn't work out. But I managed to make it work.