Search code examples
androidiosflutterimage-processingflutter-dependencies

Cropping And Swapping A Part Of Image With Another Image with Gradual Transparency in Flutter


  1. How do we crop a circular part of an bitmap image and put it on another image (bitmap)?
  2. How do we make a gradient transparency of the circular image?

Any thoughts?

Below is the illustration of what i meant.

enter image description here


Solution

  • So the thing you want is not that complicated but quite complex.

    For the first stage, you just need to crop a part of the image - depending on where you should do it - directly in memory, on UI, or using some combination of both you just need to pick a library(or method to do it). Here, here, here, just googling, or using an old regular

          ClipRRect(
              borderRadius: new BorderRadius.circular(50),
              child: Image.asset('your_image_path', height: 100, width: 100),
          )
    

    you will be able to find a suitable tool or at least an approach.

    For the second stage, you can either use a bitmap drawing using Canvas or create a Stack widget with the correct offsets and then rasterizing the resulting widget via RenderRepaintBoundry. By the way this library can help you with this task also.

    For the third task, I know only one relatively easy way - you should use ShaderMask:

                ShaderMask(
                  shaderCallback: (rect) {
                    return RadialGradient(
                      radius: 50,
                      colors: [Colors.black, Colors.transparent],
                    ).createShader(Rect.fromLTRB(0, 0, rect.width,
                        rect.height)); // I'm not sure about the correct Rect creation so may need to experiment
                  },
                  blendMode: BlendMode.dstIn,
                  child: Image.asset(
                    'your_image_path',
                    height: 100,
                    fit: BoxFit.contain,
                  ),
                ),
    

    By combining the approaches listed in the answer, you will be able to achieve what you want. The easiest implementation will look like this:

    class CaptureImage extends StatefulWidget {
      const CaptureImage({super.key});
    
      @override
      State<CaptureImage> createState() => _CaptureImageState();
    }
    
    class _CaptureImageState extends State<CaptureImage> {
      GlobalKey globalKey = GlobalKey();
    
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
          _captureImage();
        });
      }
    
      @override
      Widget build(BuildContext context) => RepaintBoundary(
            key: globalKey,
            child: Stack(
              alignment: Alignment.topCenter,
              children: <Widget>[
                AspectRatio(
                  aspectRatio: 1,
                  child: Image.asset('your_background_image', fit: BoxFit.cover),
                ),
                Positioned(
                  top: 20,
                  left: 20,
                  child: ShaderMask(
                    shaderCallback: (rect) => const RadialGradient(
                      radius: 50,
                      colors: [Colors.black, Colors.transparent],
                    ).createShader(
                      Rect.fromLTRB(0, 0, rect.width, rect.height),
                    ),
                    blendMode: BlendMode.dstIn,
                    child: ClipRRect(
                      borderRadius: BorderRadius.circular(50.0),
                      child:
                          Image.asset('your_image_path', height: 100, width: 100),
                    ),
                  ),
                )
              ],
            ),
          );
    
      Future<void> _captureImage() async {
        final RenderRepaintBoundary boundary =
            globalKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
        final ui.Image image = await boundary.toImage();
        final ByteData? byteData =
            await image.toByteData(format: ui.ImageByteFormat.png);
        final Uint8List pngBytes = byteData!.buffer.asUint8List();
        print(pngBytes);
      }
    }
    

    Edit

    The widget-less approach will look like this:

    Picture draw() {
        final recorder = PictureRecorder();
        final canvas = Canvas(recorder);
    
        canvas
          ..drawImage(background_image, Offset.zero, Paint()) //or
          ..drawImage(
              image_that_should_be_circle,
              const Offset(x_offset_based_on_you_backgroud_image,
                  y_offset_based_on_you_backgroud_image),
              // the offset from the corner of the canvas
              Paint()
                ..shader = const RadialGradient(
                  radius: needed_radius,
                  // the radius of the result gradient - it should depend on the circling image dimens
                  colors: [Colors.black, Colors.transparent],
                ).createShader(
                  Rect.fromLTRB(0, 0, your_image_width,
                      your_image_height), // the portion of your image that should be influenced by the shader - in this case I use the whole image.
                )
                ..blendMode = BlendMode
                    .dstIn); // for the black color of the gradient to be masking one
        return recorder.endRecording();
      }
    

    Edit2

    How to draw multiple images on one canvas - paintImage approach

    Picture draw() {
        final recorder = PictureRecorder();
        final canvas = Canvas(recorder);
    
        paintImage(
          canvas: canvas,
          image: backgroundImage,
          fit: BoxFit.fill,
          rect: Rect.fromLTWH(x, y, neededBackgroundWidth, neededBackgroundHeight),
        );
    
        paintImage(
          canvas: canvas,
          image: otherImage,
          fit: BoxFit.fill,
          rect: Rect.fromLTWH(x, y, neededOtherImageWidth, neededOtherImageHeight),
        );
        return recorder.endRecording();
      }
    

    I haven't tried it in action, so may need to adjust some stuff.

    Hope it helps.