How can I mask a widget with a png UI image using flutter's custom paint

3.9k views Asked by At

I can't seem to mask a widget with a png ui.Image and keep transparency. The transparent (invisible) pixels are rendered black.

I have tried using BlendMode srcIn when drawing the mask png image on my custom painter, the expected behavior is the colorful background masked by the jelly bean shape. Yet the transparent (should be deleted) pixels are rendered black (see image attached). Is this a bug? or am I missing something? I'm also aware of the ImageShader option, but it lacks flexibility I require in my app. When transitioning between screens and applying hero animation, there is a brief moment in which the canvas is rendered as expected.

Any help will be much appreciated :)

App screen shot app print screen

Mask Image mask image

BG Image bg image

Expected expected

import 'dart:typed_data';
import 'dart:ui' as ui;

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

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

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ui.Image mask;

  initState() {
    super.initState();

    load('https://img.pngio.com/shape-png-free-download-free-shapes-png-493_315.png')
        .then((image) {
      setState(() {
        mask = image;
      });
    });
  }

  Future<Uint8List> _loadFromUrl(String url) async {
    final response = await http.get(url);

    if (response.statusCode >= 200 && response.statusCode < 300) {
      return response.bodyBytes;
    } else {
      return null;
    }
  }

  Future<ui.Image> load(String asset) async {
    var a = await _loadFromUrl(asset);
    ui.Codec codec = await ui.instantiateImageCodec(a);
    ui.FrameInfo fi = await codec.getNextFrame();
    return fi.image;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Stack(
        children: <Widget>[
          Positioned(
            top: 10,
            left: 100,
            child: Hero(
                tag: 'imageHero',
                child: Container(
                  width: 200,
                  height: 200,
                  child: CustomPaint(
                    foregroundPainter: LayerPainter(mask: mask),
                    child: Image.network(
                        "https://www.thecommercialhotel.com/wp-content/uploads/2015/11/Live-Lounge-BG.jpg",
                        fit: BoxFit.fill),
                  ),
                )),
          )
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(context,
              MaterialPageRoute(builder: (BuildContext context) {
            return Page2(mask: mask,);
          }));
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class LayerPainter extends CustomPainter {
  ui.Image mask;

  LayerPainter({@required this.mask}) {}

  @override
  void paint(Canvas canvas, Size size) {
    if (mask != null) {
      canvas.drawImageRect(
          mask,
          Rect.fromLTWH(0, 0, mask.width.toDouble(), mask.height.toDouble()),
          Rect.fromLTWH(0, 0, size.width, size.height),
          new Paint()..blendMode = BlendMode.dstIn);
    }
  }

  @override
  bool shouldRepaint(LayerPainter oldDelegate) {
    return oldDelegate.mask != this.mask;
  }
}

class Page2 extends StatelessWidget {

  final ui.Image mask;

  Page2({ @required this.mask});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Stack(
        children: <Widget>[
          Positioned(
            top: 400,
            left: 100,
            child: Hero(
                tag: 'imageHero',
                child: Container(
                  width: 200,
                  height: 200,
                  child: CustomPaint(
                    foregroundPainter: LayerPainter(mask: mask),
                    child: Image.network(
                        "https://www.thecommercialhotel.com/wp-content/uploads/2015/11/Live-Lounge-BG.jpg",
                        fit: BoxFit.fill),
                  ),
                )),
          )
        ],
      ));
  }
}

2

There are 2 answers

4
jony On

Unable to solve the Custom Painter rendering issue, I ended up using a different approach. I simply render the masked png on a separate canvas, export a ui.Image and draw it using the custom painter. Thins enables me to use BlendMode.srcIn and keep transparency. You can read more about it on this blog post

0
Ryohei Nagao On

Without custom painter, you can get expected result by using ShaderMask and ImageShader. This video and my repository will help you.