I'm using a FutureBuilder to load album thumbnails into a ListTile, as follows:
ListTile _albumTile(Album album, BuildContext context) {
return ListTile(
leading: FutureBuilder<Uint8List>(
future: AndroidContentResolver.instance.loadThumbnail(
uri: album.thumbnailUri,
width: 56,
height: 56,
),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(snapshot.data!);
} else {
return const FittedBox(
fit: BoxFit.contain, child: Icon(Icons.album, size: 56));
}
}),
title: Text(album.album),
subtitle: Text(album.artist),
);
}
But, as the image loads, the placeholder icon is replaced with a blank image, and then with the final thumbnail. The blank image has the wrong width, which results in the ListTile title and subtitle jumping around, causing a flicker.
The following sequence of screenshots shows three consecutive frames:
Image placeholders.
Some of the futures have completed, and are showing thumbnails. The other tiles have a blank image (and the text jumps to the left).
All of the futures have completed.
Even when I fix the text jumping around -- by specifying fit: BoxFit.contain, width: 56, height: 56 for the image -- I still get a flash of white before the thumbnail appears.
What am I doing wrong?
Use precacheImage function to prefetch an image into the image cache.
If the image is later used by an Image or BoxDecoration or FadeInImage, it will probably be loaded faster.
I solved it by using frameBuilder. The documentation says:
If this is null, this widget will display an image that is painted as soon as the first image frame is available (and will appear to "pop" in if it becomes available asynchronously).
This describes exactly what I'm seeing.
It continues with the following:
Callers might use this builder to add effects to the image (such as fading the image in when it becomes available) or to display a placeholder widget while the image is loading.
...and the attached example shows how to implement a fade-in effect. For simplicity, however, I opted for the following:
return Image.memory(
snapshot.data!,
frameBuilder: (context, child, frame, wsl) {
if (frame != null) {
return child;
} else {
return _albumThumbnailPlaceholder();
}
},
Widget _albumThumbnailPlaceholder() {
return FittedBox(fit: BoxFit.contain, child: Icon(Icons.album, size: 56));
}
This seems to completely get rid of the blank frame.
Related
to display image according to it's original size i do the following
Image.file(File(fileCreated!.path)),
to display image to fit height and width (full screen) i do the following
Image.file(File(fileCreated!.path),fit: BoxFit.cover,height: double.infinity,width: double.infinity,),
ok it is now in full screen but i noticed there is some missing parts of the image contents is no longer visible on screen like 20 pixel from width and height , i know flutter do it for keeping the image quality the same. but How i ignore that behavior
How could i display image in full screen so the whole image parts is visible on screen ?
i tried with following
height:MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
whatever i change the height. the width will be auto changing too, even if i did not change the width !!!!
but same result
edit :
full parent
#override
Widget build(BuildContext context) {
return Scaffold(
body: Image.file(File(widget.imageFile,),fit: BoxFit.cover,height: MediaQuery.of(context).size.height,),
);
}
}
You can use MediaQuery, but I will prefer using LayoutBuilder on top widget. Also check the parent widget if it is having padding/margin.
Image.file(
File(fileCreated!.path),
fit: BoxFit.fill,
height:MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
),
In total I have three files: Home.dart, Post.dart and PostBuilder.dart
In Home.dart I have a listView.seperated:
List post = [];
void initState() {
super.initState();
getPost();
}
Future getPost()async
{
//function that gets and paginates the data from the backend and adds it to the list.
}
body: ListView.separated(
controller: _scrollController,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: post.length,
itemBuilder: (context, index) {
return SizedBox(
width: double.infinity,
child: PostBuilder(post[index]);
},
separatorBuilder: (context, index) {
return const Divider();
}),
And this code on my PostBuilder.dart file:
class PostBuilder extends StatefulWidget {
final post;
const PostBuilder(this.post, {super.key});
#override
State<PostBuilder> createState() => _PostBuilderState();
}
class _PostBuilderState extends State<PostBuilder> {
return Container(child: CachedNetworkImage(
imageUrl: widget.post['postUrl'],
fit: BoxFit.contain,
),
),
Now the issue I'm facing is that whenever I navigate back from Post.dart screen to the home screen, since the height of the cached network image is dynamic, it rebuilds and it seems like a glitch.
Is there any way to stop this behaviour? Maybe something like saving the state of the screen when navigating, which might prevent reloading/rebuilding the list and hence everything stays as it was.
Also I did try the following things:
Adding listview.separated with cacheExtent.
Adding listview.separated in a parent SingleChildListView widget.
Adding 'AutomaticKeepAliveClientMixin' to the class
Unfortunately, none of this worked.
After a couple of days of experimentation, I ended up creating my own "image cacher" (similar to my video cacher) that got me the height and width of the image and used that to set the container size of the list which solve the "glitchiness" when the Image would reload after navigating back.
Here is how I did it if anyone is still looking for the answer:
I used a package called image_pixels
When I got the image information I convert it into flutter logical pixels by simply using this formula:
For getting height: MediaQuery.of(context).width/ (image.width/image.height)
For getting width: MediaQuery.of(context).height/(image.width/image.height)
What it does is first it gets the aspect ratio of the image and then that ratio is used to determine the height of the container to preserve the intrinsic aspect ratio of the image.
Since child size is determined by the Parent if you have a dynamic image you will have to get its size and use it to determine the parent. Or else you will have this glitchy list behaviour as it only builds visible widgets and if it is dynamic, then as it rebuilds the child from scratch it will resize the list.
Hope this helps to anyone looking for an answer.
Lets say you have a ListView of variable height:
List items are a Container with a mix of text and images. As such, the list items are of variable height. Sometimes no images. The text renders immediately as expected, but the images may take time to retrieve and render on screen
The images are retrieved from the network using CachedNetworkImage
Images are of variable height
When the Screen is opened the ListView automatically scrolls to item#11 (using ensureVisible technique)
So there are items both above and below your current position
At this point, when one of the network images above your position load up, the entire ListView will be pushed and you will no longer be looking at Item #11, rather somewhere randomly higher up
I considered initiating a new scroll in a callback after each image loads, however, due to network speeds, usually the listview scroll will finish before all the images load. If there are a lot of images, the images could take time to load, so it would be unreasonable to initiate a new scroll each time a new image is loaded, the screen just keeps scrolling forward every few seconds. It becomes dizzying and annoying.
Alternatively, the scrollview could jumpTo a new position as soon as the image loads, but I'm imagining there would be a slight delay between the two events and the user perceive a small "glitch" as the image loads and the listview immediately jumps to offset the image load. Even using a Future.microtask there is a very small perceptible 'glitch' as the image loads and the jumpto fires
It would be most preferable to have the listview expand the content upward somehow, so that the users current scroll position is maintained, as far as they are concerned.
Is it possible to have the ListView keep its position as the images load?
Assuming you have a predefined size for your images, you can wrap the image in a SizedBox(). This way your list will always have the same height and your items won't get pushed around.
EDIT:
Since your images are of variable size, I would probably animate to the desired location on every image load.
CachedNetworkImage has a callback
imageBuilder: (context, imageProvider) {
/// Animate to desired index
return Image(image: imageProvider);
}
Animated container, might help you. It can adjust the height automatically, depending on the height u provide in builder.
Also you can use this answer to determnin image height and width in rnutime.
Images are of variable height
To overcome this, Either we take the image size or aspect ratio of the image while storing the image along with other data.
While retrieving data, along with other text data we will receive the aspect ratio or height for the image.
I would use the same height or ratio and show placeholder image till images are loaded.
CachedNetworkImage(
imageUrl: countryList[index].flagUrl,
height: 60, // Set your height according to aspect ratio or fixed height
width: 60,
fit: BoxFit.cover,
placeholder: (_, __) => Container(
alignment: Alignment.center,
height: 60, // Set your height
width: 60,
color: Colors.red.withAlpha(80),
child: Text(
countryList[index].name[0],
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
),
)
Image must be of specific aspect ratio I believe. You can define height according to aspect ratio.
Try as follows:
ListView.builder(
shrinkWrap: true,
itemCount: images.length,
itemBuilder: (ctx, i) {
return Column(children: [
ButtonItems(i),
const SizedBox(height: 10),
]);
}));
Button Items class
class ButtonItems extends StatefulWidget {
final int i;
ButtonItems(this.i);
#override
_ButtonItems createState() => _ButtonItems();
}
class _ButtonItems extends State<ButtonItems> {
var images = [
"https://opengraph.githubassets.com/2ddb0ff05ef9ccfce35abb56e30d9c5068e01d1d10995484cfd07becee9accf7/dartpad/dartpad.github.io",
null,
"https://opengraph.githubassets.com/2ddb0ff05ef9ccfce35abb56e30d9c5068e01d1d10995484cfd07becee9accf7/dartpad/dartpad.github.io"
];
#override
Widget build(BuildContext context) {
print(images[widget.i]);
return Container(
height: 50,
color: Colors.grey,
child: Row(children: [
AspectRatio(
aspectRatio: 3 / 2,
child: images[widget.i] == null
? Container()
: Image.network(images[widget.i]!, fit: BoxFit.cover),
),
Text("Title " + widget.i.toString()),
]));
}
}
I have 2 screens.
One the first screen I load up an image from the web and have my user draw a rectangle on the image. I am using local position from onPanStart and onPanUpdate callbacks in the GestureDetector. I am saving the start and end Offset values.
final taggableImage = GestureDetector(
onPanStart: (DragStartDetails details){
provider.updateRectangleStart(details.localPosition);
},
onPanUpdate: (DragUpdateDetails details ){
provider.updateRectangleEnd(details.localPosition);
},
child: CustomPaint(
foregroundPainter: provider.drawRect,
child: image,
),
);
Screenshot of drawing the rectangle on the first screen:
Now I would like to load up the images again in a new screen and draw the rectangle back onto the image from the Offset values I have saved earlier. But the rectangle always shows up in the wrong spot and sometimes even outside the image.
Here is how I am redrawing the rectangle from the saved Offset values.
final image = CustomPaint(
foregroundPainter: DrawRectangleService(provider.selectedDetection?.detectionRect ?? Rect.zero),
child: FittedBox(
fit: BoxFit.fill,
child: CachedNetworkImage(
placeholder: (context, url) => loadingWidget("Loading image"),
imageUrl: imageURL,
),
),
);
Screenshot of how it looks like when I redraw the rectangle
Question: How do I use the Offset values I saved on my first screen to redraw the rectangle on the image in my second screen.
convert your coordinates to double, in the range of 0 to 1, both the width and height by dividing them with original width and height of the Image widget,
on the new screen multiply them back with the new width and height of the Image widget.
I am loading images using Image.network for each item in a list using the following code:
Image getEventImageWidget(AustinFeedsMeEvent event) {
return event.photoUrl.isNotEmpty ?
Image.network(
event.photoUrl,
width: 77.0,
height: 77.0,
) : Image.asset(
'assets/ic_logo.png',
width: 77.0,
height: 77.0,
);
}
When I scroll up and down, the list sometimes hangs when loading the images. Is there a way I can load the images on a background thread? What can I do to help fix scrolling performance?
NOTE: When I looked back at this, I found that the images that I was using were really large.
There are two was to speed up the rendering of your ListView of images.
The first is to set the cacheExtent property to a larger value in your ListView constructor. This property controls how much offscreen widgets are rendered, and will help by causing the rendering to start a bit sooner.
The second is to pre-cache your images using precacheImage. Flutter has an in-memory cache, so it is generally to necessary to cache everything to disk to get good read performance. Instead, you can ask Flutter to download these images ahead of time so that they are ready when the widget is built. For example, if you have a list of urls of your image, then in an initState method you could ask Flutter to cache all of them.
final List<String> imageUrls = [ /* ... */ ];
#override
void initState() {
for (String url in imageUrls) {
precacheImage(new NetworkImage(url), context);
}
super.initState();
}
Are you sure your images are not very heavy? Check the size of the images first.
Also you can use the package named: cache_network_image
It's very simple :
new Image(image: new CachedNetworkImageProvider(url))
UPDATE (Package was updated)
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => new CircularProgressIndicator(),
errorWidget: (context, url, error) => new Icon(Icons.error),
),
you can also use:
FadeInImage.assetNetwork(
placeholder: 'assets/ic_logo.png',
image: event.photoUrl,
height: 77.0,
width: 77.0,
fit: BoxFit.cover,
fadeInDuration: new Duration(milliseconds: 100),
),
but yeah, per diegoveloper, you sure your images aren't huge? Listview has no problem rendering anything that's close to reasonable in size.
You can create a Stateful Widget that creates the ListView with placeholder images, then have it have an async method you call after build() that loads the images from network (one by one) and then changes the state of the previously mentioned widget to replace the placeholder with the correct image. As a bonus, you can create a cache that stores the images so they don't have to be downloaded each time the ListView enters scope (here you would have the async method look in the cache for the image and if it doesn't find it there, download it).
As a side note, this would obviously require giving each of the images in the ListView an index.