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?
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
Background
I have an image widget that loads an image remotely from the network.
Before/while the image is being retrieved I have a placeholder with a fixed height of 200.0 in its place.
Once the image is loaded, I update the Widget to display the image
The catch is I want to update the height of the container to the new height of the image. This is done in synchrony with changes to a scroll position so I can't just let the image container auto-adjust, I need the specific height of the newly downloaded image just before its displayed on screen.
My setup
I have a FutureBuilder as follows:
FutureBuilder(
future: getData(image:image, width:widget.width, height:widget.height),
builder: (ctx, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// If we got an error
if (snapshot.hasError) {
... placeholder
// if we got our data
} else if (snapshot.hasData) {
// Extracting data from snapshot object
final thisimage = snapshot.data as FutureImageTransporter;
final imageheight = thisimage.height
/// Do some stuff with image height here
return Container(
width: widget.width,
height:thisimage.height, ///This needs to have the height value retrieved when the image is downloaded
child: thisimage.imagewidget
);
}
else {
... placeholder
}
}
else
{
... placeholder
}
},
),
The future data retrieves data from getData, which is a sort of pass through to get the image from MyCustomImage which returns an image widget from the extended image package which is as follows:
Future<FutureImageTransporter> getData({image, width, height}) {
return Future.delayed(Duration(seconds: 6), () async {
var finalImageWidget = MyCustomImage(
input: image,
width:width,
height:height,
);
return finalImageWidget;
});
}
I'm using my own class called FutureImageTransporter to house the data, which consists of the actual image Widget and the height data (double).
class FutureImageTransporter{
late Widget imagewidget;
late double height;
}
The actual image widget is constructed as follows with MyCustomImage, and I also want to return back the height of the image, both pieces packaged in a FutureImageTransporter
FutureImageTransporter MyCustomImage ({
required input,
width,
height,
}) {
double _finalHeight = 0.0;
Widget finalImageWidget =
ExtendedImage.network(
input,
key: ValueKey(input),
width: width
height: height
enableMemoryCache: false,
handleLoadingProgress: true,
loadStateChanged: (ExtendedImageState state) {
switch (state.extendedImageLoadState) {
case LoadState.loading: return Container();
case LoadState.completed:
_finalHeight = state.extendedImageInfo?.image.height.toDouble() ?? 0.0;
debugPrint(_finalHeight.toString()); /// This is properly outputting the final height of the image
return ExtendedRawImage(
image: state.extendedImageInfo?.image,
fit: BoxFit.cover,
width: width
height: height
);
case LoadState.failed: return Container();
}
},
);
FutureImageTransporter tempvar = FutureImageTransporter();
tempvar.imagewidget = finalImageWidget;
tempvar.height = _finalHeight; ///This is not updating to the _finalHeight which updates above
return tempvar;
}
The Problem/Question
With the above setup, I'm using a FutureBuilder to successfully retrieve an image and then update the widget in my widget tree with the new image widget which is an ExtendedImage.network widget, which is something analogous to Image.Network().
Within ExtendedImage.network, the height of the image is successfully calculated _finalHeight = state.extendedImageInfo?.image.height.toDouble() ?? 0.0;.
However, which the widget does properly debugPrint the value of the image height, it never actually properly returns the value to my FutureBuilder. The FutureBuilder only sees the originally initialized value of 0.0 double _finalHeight = 0.0; (or whatever default value I stick in there).
This problem seems to have to do with how Im retrieving data from a future and setting it up to pass between the widgets, but I can't understand where I went wrong, or if this is simply impossible? This ends up being a TON of code, but what I originally thought would be a simple task. Any ideas?
im using flutter_svg package for svg. and now i want to use a svg inside a container as decoration like this,
Container(
decoration: BoxDecoration(
image: DecorationImage(
image: SvgPicture.string(
'''<svg viewBox="...">...</svg>'''
),
),
),
)
but the problem is DecorationImage peram expecting 'ImageProvider'. how can i do this ?
i tried flutter_svg_provider but its also not working. i found this solution, but dont know how to use.
use a custom Decoration like this:
class SvgDecoration extends Decoration {
SvgDecoration.string(String rawSvg, {this.key})
: rawSvgFuture = Future.value(rawSvg);
SvgDecoration.file(File file, {this.key})
: rawSvgFuture = file.readAsString();
SvgDecoration.asset(String asset, {this.key})
: rawSvgFuture = rootBundle.loadString(asset);
final Future<String> rawSvgFuture;
final String? key;
#override
BoxPainter createBoxPainter([ui.VoidCallback? onChanged]) {
return _SvgDecorationPainter(rawSvgFuture, onChanged, key);
}
}
class _SvgDecorationPainter extends BoxPainter {
_SvgDecorationPainter(this.rawSvgFuture, ui.VoidCallback? onChanged, String? key) {
rawSvgFuture
.then((rawSvg) => svg.fromSvgString(rawSvg, key ?? '(no key)'))
.then((d) {
drawable = d;
onChanged?.call();
});
}
final Future<String> rawSvgFuture;
DrawableRoot? drawable;
#override
void paint(ui.Canvas canvas, ui.Offset offset, ImageConfiguration configuration) {
if (drawable != null) {
canvas
..save()
..translate(offset.dx, offset.dy);
drawable!
..scaleCanvasToViewBox(canvas, configuration.size!)
..draw(canvas, offset & configuration.size!);
canvas.restore();
}
}
}
as you can see there are 3 constructors: SvgDecoration.string, SvgDecoration.file and SvgDecoration.asset but of course you can add some other custom constructors (like SvgDecoration.network for example)
The SvgPicture is a Widget, not an Image, which is why it can not be used as DecorationImage here. The way you can use the SvgPicture behind your Container is a Stack:
Stack(
children: [
SvgPicture.string(
'''<svg viewBox="...">...</svg>''',
(... width, height etc.)
),
Container(
child: (..., foreground widget)
),
],
)
Obviously, you have to make sure that both have the same size if you need it. But that depends on your usecase.
Due to CachedNetworkImage not working on flutter web, upon porting, I have tried to use this, but my question is do we really need this? Or we just use Image. Network and the browser and service worker will handle the cache part (which is then set by server's response header through, for example, cache-control= "max-age=43200, public"
This is used on the food delivery project I am working on, https://www.santaiyamcha.com
Below are the classes I use to replace CachedNetworkImage which doesn't seem to work well.
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:http_extensions_cache/http_extensions_cache.dart';
import 'package:http_extensions/http_extensions.dart';
/// Builds a widget when the connectionState is none and waiting
typedef LoadingBuilder = Widget Function(BuildContext context);
/// Builds a if some error occurs
typedef ErrorBuilder = Widget Function(BuildContext context, Object error);
class MeetNetworkImage extends StatelessWidget {
/// Image url that you want to show in your app.
final String imageUrl;
/// When image data loading from the [imageUrl],
/// you can build specific widgets with [loadingBuilder]
final LoadingBuilder loadingBuilder;
/// When some error occurs,
/// you can build specific error widget with [errorBuilder]
final ErrorBuilder errorBuilder;
final double scale;
final double width;
final double height;
final Color color;
final FilterQuality filterQuality;
final BlendMode colorBlendMode;
final BoxFit fit;
final AlignmentGeometry alignment;
final ImageRepeat repeat;
final Rect centerSlice;
final bool matchTextDirection;
/// Whether to continue showing the old image (true), or briefly show nothing
/// (false), when the image provider changes.
final bool gaplessPlayback;
final String semanticLabel;
final bool excludeFromSemantics;
MeetNetworkImage({
#required this.imageUrl,
this.loadingBuilder = null,
this.errorBuilder = null,
this.scale = 1.0,
this.height,
this.width,
this.color = const Color(0xFDFFFF),
this.fit = BoxFit.fill,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.semanticLabel,
this.centerSlice,
this.colorBlendMode,
this.excludeFromSemantics = false,
this.filterQuality = FilterQuality.low,
this.matchTextDirection = false,
this.gaplessPlayback = false,
}) : assert(imageUrl != null),
assert(alignment != null),
assert(repeat != null),
assert(matchTextDirection != null);
Future<Response> getUrlResponse() {
/*
//The caching part I tried, does not seems working
final client = ExtendedClient(
inner: Client(),
extensions: [
CacheExtension(
//logger: Logger("Cache"),
defaultOptions: CacheOptions(
expiry: const Duration(hours: 168),
// The duration after the cached result of the request will be expired.
//forceUpdate: false, // Forces to request a new value, even if an valid cache is available
//forceCache: false, // Forces to return the cached value if available (even if expired).
//ignoreCache: true, //Indicates whether the request should bypass all caching logic
//returnCacheOnError: true, //If [true], on error, if a value is available in the store if is returned as a successful response (even if expired).
keyBuilder: (request) => "${request.method}_${imageUrl.toString()}",
// Builds the unqie key used for indexing a request in cache.
store: MemoryCacheStore(),
// The store used for caching data.
shouldBeSaved: (response) =>
response.statusCode >= 200 && response.statusCode < 300,
),
)
],
);
return client.get(imageUrl);
*/
return get(imageUrl);
}
Widget getLoadingWidget(BuildContext context) {
if (loadingBuilder != null) {
return loadingBuilder(context);
} else
return Container(
height: height, width: width,
child: Center(
child: CircularProgressIndicator()
)
/*Image.asset(
'assets/img/loading4.gif',
height: height,
width: width,
fit: BoxFit.contain,
),*/
);
}
Widget getErrorWidget(BuildContext context, String error) {
if (errorBuilder != null) {
return errorBuilder(context, error);
} else
return Center(child: Icon(Icons.error));
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getUrlResponse(),
builder: (BuildContext context, AsyncSnapshot<Response> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return getLoadingWidget(context);
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.hasError)
return getErrorWidget(context, snapshot.error);
if (!snapshot.hasData)
return getErrorWidget(context, snapshot.error);
//return getLoadingWidget(context);
return Image.memory(
snapshot.data.bodyBytes,
scale: scale,
height: height,
width: width,
color: color,
fit: fit,
alignment: alignment,
repeat: repeat,
centerSlice: centerSlice,
colorBlendMode: colorBlendMode,
excludeFromSemantics: excludeFromSemantics,
filterQuality: filterQuality,
gaplessPlayback: gaplessPlayback,
matchTextDirection: matchTextDirection,
semanticLabel: semanticLabel,
);
}
return Container();
},
);
}
}
What do you suggest?
I am using FadeInImage.memoryNetwork. It is working fine. The browser handles the cache part.
No. This is an issue with Google Chrome which prefers "cache-control" over E-Tags or Last Modified Headers. In my case I am using Firefox which caches on the basis of E-Tags. So here is the thing.
If you are using setState((){}) or the flutter engine calls the setState due to some reason, the images are rebuilt and if the images are not cached, it is again fetched. To prevent it, use the header cache-control: max-age=<any value greater than 0> and it will work fine in Chrome.
Or just build the app using web renderer canvaskit - flutter build web --web-renderer canvaskit.
I have come to this conclusion based on my experience and I could not find it anywhere, Hope it helps :)
Try to update your flutter version if you are using old one, and use from image providers interface.
And the browser will take care of the rest
Example:
Create a container add a decoration in the decoration add an image decoration
Then add from memory or network image.
I need to render my custom object inside of a ListTile with custom painter in order to draw some custom text.
ListTile(
title: CustomPaint(
painter: RowPainter.name(
_titleFontSelected,
_titleFont,
text,
index,
MediaQuery.of(context),
currentRow,
),
),
);
Inside my RowPainter I draw the text with the font selected.
When the row is too large, it automatically wraps and get drawn outside the given paint size.
void paint(Canvas canvas, Size size)
I like this behavior, but how can I resize the height of my paint area? Because this is a problem since this overlaps the next List row.
I know that the CustomPaint has a property Size settable, but I know the text dimension only inside my paint function using the TextPainter getBoxesForSelection but it's too late.
How can I "resize" my row painter height dynamically if the text wraps?
TL;DR
You cannot dynamically size a custom painter, however, your problem can be solved using a CustomPaint.
I will first elaborate on the dynamic sizing and then explain how to solve this problem using a constant size.
Dynamic size
This is essentially, where CustomPaint has its limits because it does not provide a way for you to size the painter based on the content.
The proper way of doing this is implementing your own RenderBox and overriding performLayout to size your render object based on the contents.
The RenderBox documentation is quite detailed on this, however, you might still find it difficult to get into it as it is quite different from building widgets.
Constant size
All of the above should not be needed in your case because you do not have a child for your custom paint.
You can simply supply the size parameter to your CustomPaint and calculate the required height in the parent widget.
You can use a LayoutBuilder to get the available width:
LayoutBuilder(
builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
...
}
)
Now, you can simply use a TextPainter to retrieve the required size before even entering your custom paint:
builder: (context, constraints) {
...
final textPainter = TextPainter(
text: TextSpan(
text: 'Your text',
style: yourTextStyle,
),
textDirection: TextDirection.ltr,
);
textPainter.layout(maxWidth: maxWidth); // This will make the size available.
return CustomPaint(
size: textPainter.size,
...
);
}
Now, you can even pass your textPainter to your custom painter directly instead of passing the style arguments.
Your logic might be a bit more complicated, however, the point is that you can calculate the size before creating the CustomPaint, which allows you to set the size.
If you need something more complicated, you will likely have to implement your own RenderBox.
I haven't tested it out, but this might work:
First of all, you wrap the CustomPaint into a stateful widget (called e.g. DynamicCustomPaint), to manipulate your widget dynamically.
You give your CustomPainter a function onResize, which will give you the new size of the canvas when you know it.
You call this function once you know the exact size the Canvas has to be. By using, for example, this technique where you won't have to draw the text to know what size it will be.
When the onResize function will be called, you get the new size for the canvas and call setState in the DynamicCustomPaint state.
This might look like this:
class DynamicCustomPaint extends StatefulWidget {
#override
_DynamicCustomPaintState createState() => _DynamicCustomPaintState();
}
class _DynamicCustomPaintState extends State<DynamicCustomPaint> {
Size canvasSize;
#override
Widget build(BuildContext context) {
// Set inital size, maybe move this to initState function
if (canvasSize == null) {
// Decide what makes sense in your use-case as inital size
canvasSize = MediaQuery.of(context).size;
}
return CustomPaint(
size: canvasSize,
painter: RowPainter.name(_titleFontSelected, _titleFont, text, index, currentRow, onResize: (size) {
setState(() {
canvasSize = size;
});
}),
);
}
}
typedef OnResize = void Function(Size size);
class RowPainter extends CustomPainter {
RowPainter.name(
this._titleFontSelected,
this._titleFont,
this.text,
this.index,
this.currentRow,
{ this.onResize },
);
final FontStyle _titleFontSelected;
final FontStyle _titleFont;
final String text;
final int index;
final int currentRow;
final OnResize onResize;
#override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
// call onResize somewhere in here
// onResize(newSize);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
Use SingleChildRenderObjectWidget and RenderBox instead. Full simple example with dynamic resizing.
DartPad
import 'dart:async';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: [
SizedBox(height: 100,),
Text('I am above'),
MyWidget(),
Text('I am below')
],
),
),
),
));
}
class MyWidget extends SingleChildRenderObjectWidget {
#override
MyRenderBox createRenderObject(BuildContext context) {
return MyRenderBox();
}
}
class MyRenderBox extends RenderBox {
double myHeight = 200;
#override
void paint(PaintingContext context, Offset offset) {
Paint paint = Paint()
..color = Colors.black..style = PaintingStyle.fill;
context.canvas.drawRect(
Rect.fromLTRB(offset.dx, offset.dy,
offset.dx + size.width, offset.dy + size.height,), paint);
}
#override
void performLayout() {
size = Size(
constraints.constrainWidth(200),
constraints.constrainHeight(myHeight),
);
}
// Timer just an example to show dynamic behavior
MyRenderBox(){
Timer.periodic(Duration(seconds: 2), handleTimeout);
}
void handleTimeout(timer) {
myHeight += 40;
markNeedsLayoutForSizedByParentChange();
layout(constraints);
}
}
CustomPainter will only size to its children's size or initial value passed to the constructor. Documentation:
Custom painters normally size themselves to their child. If they do not have a child, they attempt to size themselves to the size, which defaults to Size.zero. size must not be null.
Basics of RenderBox
https://programmer.group/the-operation-instruction-of-flutter-s-renderbox-principle-analysis.html
https://api.flutter.dev/flutter/rendering/RenderBox-class.html
I am very new to flutter development, and I have to make a fairly quick decision on whether or not it is the right platform for my internship project.
I have to create an interface which requires all directional swipes to navigate to different menus (I'm thinking of doing nested horizontal and vertical scrolling, which I have had trouble with in Android Studio) - but more importantly, I have to save the raw data from the touching/tapping/swiping. I can't just save "left swipe" or "right swipe", I also have to know pressure, velocity, location, exact path, etc.
Is this feasible in flutter? How does flutter handle this raw data as opposed to Android studio? Does flutter only determine the approximate direction of the swipe and that's it?
I have tried searching for answers all day, but I must be missing some key word, because I have been unable to find the answer so far.
GestureDetector is a very extensive Widget in this regard. It has all the capabilities you are searching for. A simpler version of it, which also has Material design built in, is InkWell, but this might be lacking some of the functionality you are searching for.
With a GestureDetector wrapped about your Widget you will be able to catch all hit events (you can even specify HitTestBehavior (with the behavior parameter).
For your custom interactions there are plenty of callbacks implemented. I linked you to the constructor, which contains a bunch of useful parameters, like onTapDown/Up, onVertical/HorizontalDragStart/Update/End.
This is not even everything, but using those you can programatically define your behavior. Let me explain the concept with a small example:
Offset start;
void verticalDragStart(DragStartDetails details) {
// process the start by working with the details
start = details.globalPosition;
// ...
}
void verticalDragUpdate(DragUpdateDetails details) {
// apply your logic
Offset delta = details.delta;
// ...
}
// use DragEnd, also for horizontal, pan etc.
#override
Widget build(BuildContext context) => GestureDectector(
onVerticalDragStart: verticalDragStart,
// ...
);
I hope that you can imagine all the possibilties this enables. You can also combine different callbacks. Just take a look at the parameters in the documentation and experiment with what fits for you.
I think that this is the raw data you asked for.
You can get the raw touch movements using Listener. The following example shows how to grab a list of points. It uses them to draw the line you just traced with your finger. You can't tell the pressure, but can tell the exact path and velocity (if you stored time with each point). The higher level detector, GestureDetector, hides these raw movements from you, but interprets them into the traditional swipes.
(Notes about the example... shouldRepaint should be smarter, points returned by Listener are in global co-ordinates so may need to be converted to local (this simple example works because there's no AppBar))
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Gesture',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<Offset> points = [];
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Listener(
onPointerDown: (e) {
points = [];
points.add(e.position);
},
onPointerMove: (e) {
points.add(e.position);
},
onPointerUp: (e) {
points.add(e.position);
setState(() {});
},
child: new CustomPaint(
painter: new PathPainter(points),
child: new Container(
width: 300.0,
height: 300.0,
color: Colors.black12,
),
),
),
);
}
}
class PathPainter extends CustomPainter {
List<Offset> points;
Path path = new Path();
PathPainter(this.points) {
if (points.isEmpty) return;
Offset origin = points[0];
path.moveTo(origin.dx, origin.dy);
for (Offset o in points) {
path.lineTo(o.dx, o.dy);
}
}
#override
void paint(Canvas canvas, Size size) {
canvas.drawPath(
path,
new Paint()
..color = Colors.orange
..style = PaintingStyle.stroke
..strokeWidth = 4.0,
);
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true; // todo - determine if the path has changed
}
}