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
Mask Image
BG Image
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),
),
)),
)
],
));
}
}
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