Flutter ShaderMask using an ImageShader

Viewed 1117

I'm trying to get a ShaderMask that uses an Image as a Shader. I'm reading the image from memory, so my image is a File. How do I create an ImageShader using an image from memory?

File imageFile;
Image image = Image.file(imageFile)

ShaderMask(
   shaderCallback: (bounds) {
      Float64List matrix4 = new Matrix4.identity().storage; // <--- DO I NEED THIS OR THE BOUNDS?
      return ImageShader(image, TileMode.mirror, TileMode.mirror, matrix4);
   },
   child: child
)

There's an error with ImageShader as image is the wrong type (I need ui.Image, which I don't understand how to create).

How do I create the ImageShader from a File image?

PS: is matrix4 correct or should I use the bounds somehow?

1 Answers

The important thing to know is that ImageShader uses an Image from the dart:ui package and not an instance of the Image widget. There is no direct operation or constructor available to create an instance of ui.Image from a network location, a file or asset, so you need some code to get it done.

The best generic solution i came up with after looking up many resources and digging into the code how the Image widget is loading raw images, is to use an ImageProvider as source. The abstract ImageProvider has implementations like NetworkImage, FileImage, ExactAssetImage and MemoryImage to load your image from any resource you want.

First you get an ImageStream using the ImageProvider.resolve method. The resolve method takes an argument of type ImageConfiguration, that should be filled with as many information as you have available at the code location. You can use the global createLocalImageConfiguration function in most cases, but be aware that this will not work when you create the shader in the initState method of a StatefulWidget.

On the resolved ImageStream you can attach an ImageStreamListener, that takes an ImageListener callback as first parameter. Once the image is loaded, the callback will be called with an ImageInfo, which provides the requested image on the image property.

You can construct the ImageShader with both tile modes as TileMode.clamp and a simple identity matrix, which you can either create by hand or take that one offered by the Matrix4 class. If you need the image shader to be smaller than the size of the provided image, you can wrap your provider in an ResizeProvider and specify the desired width and height.

Below my implementation of an ImageMask widget as a reference, which can be used to mask widgets of any kind.

class ImageMask extends StatefulWidget {
  final ImageProvider image;
  final double width;
  final double height;
  final Widget child;

  const ImageMask({@required this.image, this.width, this.height, @required this.child});

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

class _ImageMaskState extends State<ImageMask> {
  Future<Shader> _shader;

  @override
  void initState() {
    super.initState();
    _shader = _loadShader(context);
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _shader,
      builder: (_, AsyncSnapshot<Shader> snapshot) {
        return snapshot.connectionState != ConnectionState.done || snapshot.hasError
            ? SizedBox(width: widget.width, height: widget.height)
            : ShaderMask(
                blendMode: BlendMode.dstATop,
                shaderCallback: (bounds) => snapshot.data,
                child: widget.child,
              );
      },
    );
  }

  Future<Shader> _loadShader(BuildContext context) async {
    final completer = Completer<ImageInfo>();

    // use the ResizeImage provider to resolve the image in the required size
    ResizeImage(widget.image, width: widget.width.toInt(), height: widget.height.toInt())
        .resolve(ImageConfiguration(size: Size(widget.width, widget.height)))
        .addListener(ImageStreamListener((info, _) => completer.complete(info)));

    final info = await completer.future;
    return ImageShader(
      info.image,
      TileMode.clamp,
      TileMode.clamp,
      Float64List.fromList(Matrix4.identity().storage),
    );
  }
}
Related