Image widget 是 Flutter 中最常用的 widget 之一,但我相信我们没有充分利用它的功能,仅仅显示一个图片是不够的,你还应该给用户他们需要的最佳体验!
在这篇文章中,我将谈论一些图像技巧和最佳实践,以获得更好的性能和用户体验。
这些技巧是:
1.使用 WebP 而不是 JPG/PNG
2.设置宽度和高度以保留 UI 空间
3.降低图片的显示分辨率以减少内存使用
4.预加载/预缓存您的图像,以便即时加载图像
5.加载时显示进度指示器
6.加载时显示进度百分比指示器
7.加载时显示闪烁效果,以提高用户体验
8.显示 blurhash 作为占位符
9.使用渐变效果来提高用户体验
10.缓存图像以减少网络使用并提高性能
11.注意非经常性成本
12.在失败时显示重试按钮
结语
WebP 是下一代图像格式,它比 PNG 和 JPEG 小约 25%,并且比其他格式快。
这意味着,你的应用程序将使用更少的内存,构建速度更快。
这里有一些基准:
图片
图片
Image.asset(// 'image.jpg','image.webp',// PREFER);
它可以防止应用程序出现布局偏移
图片
之前——之后
Image.network(imageUrl,width:200,height:150,);
图片
图片
你的图片可能会导致设备内存膨胀,这是因为,尽管它们在 UI 中占据相对较小的一部分,Flutter 还是会以全分辨率渲染它们,从而消耗大量内存。
为了避免这种问题,可以使用 cacheWidth 或 cacheHeight 参数对指定大小的图像进行解码。
此外,我们可以使用 Flutter 开发者工具轻松检测超大图像。
如果图像过大,它会反转图像,使其颠倒。
注意!缓存大小不应该小于小部件的大小,否则,由于分辨率低,它看起来像素化!
Image.network( imageUrl, cacheWidth: 100, cacheHeight: 150, );
如果你在显示图像之前缓存它们,Flutter 将跳过构建的处理步骤并立即显示它们。
图片
class MyImage extends StatefulWidget { const MyImage({super.key});@overrideState createState()=>_MyImageState();} class _MyImageState extends State { late final Image myImage;@overridevoid initState(){ super.initState();myImage=Image.asset('path');}@overridevoid didChangeDependencies(){ super.didChangeDependencies();precacheImage(myImage.image,context);}@overrideWidget build(BuildContext context){returnmyImage;} }
突然弹出图像不是预期的行为,用户可能会因为网络连接不足而错过图像并向下滚动,或者可能在屏幕上看到一些空白,等等。我们应该始终通知用户图像正在加载。
图片
returnImage.network(imageUrl,loadingBuilder:(_,child,event){if(event==null)returnchild;returnconst Center(child: CircularProgressIndicator());},);
我们也可以显示进度百分比,而不是无限加载,这样对用户来说更有用。
图片
returnImage.network(imageUrl,loadingBuilder:(_,child,event){if(event==null)returnchild;returnCenter(child: CircularProgressIndicator(value: event.cumulativeBytesLoaded/(event.expectedTotalBytes ??0),),);},);
显示进度条是好的,但并不是最好的选择,显示闪烁效果(Shimmer)要比显示进度条好得多。
图片
returnImage.network(imageUrl,height:200,width:350,loadingBuilder:(_,child,event){if(event==null)returnchild;returnconst Shimmer(height:200,width:350,);});// Most Basic Shimmerclass Shimmer extends StatefulWidget { const Shimmer({ super.key,this.width,this.height,this.minOpacity=0.015,this.maxOpacity=0.15,this.borderRadius=const BorderRadius.all(Radius.circular(4)),this.child,});finaldouble? width;finaldouble? height;finaldoubleminOpacity;finaldoublemaxOpacity;final BorderRadius? borderRadius;final Widget? child;@overrideState createState()=>_ShimmerState();} class _ShimmerState extends StatewithSingleTickerProviderStateMixin { late final AnimationController controller;@overridevoid initState(){ super.initState();controller=AnimationController(vsync: this,duration: const Duration(seconds:1),lowerBound: widget.minOpacity,upperBound: widget.maxOpacity,)..repeat(reverse:true);}@overridevoid dispose(){ controller.dispose();super.dispose();}@overrideWidget build(BuildContext context){returnRepaintBoundary(child: FadeTransition(opacity: controller,child: Container(width: widget.width,height: widget.height,decoration: BoxDecoration(color: Colors.black,borderRadius: widget.borderRadius,),child: widget.child,),),);} }
我创建了一个简单的 shimmer 小部件,但你可以从官方文档中学习如何创建高级版本的 shimmer 效果。
❝
官方文档:创建 shimmer 加载效果(https://docs.flutter.dev/cookbook/effects/shimmer-loading)
为了改善用户体验,可以使用哈希代码显示图像的模糊版本,而不是在图像加载时显示空白的灰色区域。
图片
https://blurha.sh/
returnconst SizedBox(width:350,height:200,child: BlurHash(hash: hashCode,imageFit: BoxFit.cover,image: imageUrl,),);
默认情况下,图片加载后立即显示,这对我们的视觉体验来说非常糟糕,为了改善这一点,我们可以用一个小的渐入动画来显示它们。
我们可以使用 FadeInImage 来实现这个功能。
它需要字节或资源作为占位符,在这个例子中,我将使用 transparent_image 包来获取透明图像字节。
我们还可以使用 cached_network_image 包来实现这一点,以及更多。
returnFadeInImage.memoryNetwork(image: imageUrl,placeholder: kTransparentImage,);// 或者returnCachedNetworkImage(imageUrl: imageUrl,);
为了避免每次下载相同的图片,我们可以缓存第一次下载的图片并重复使用,为了实现这一点,我们可以创建自己的缓存机制,或者我们可以直接使用 cached_network_image。
它缓存网络图像,默认情况下自动显示它们的淡入效果,并提供了更多的图像控制。
Image widget 没有 const 构造函数,虽然这在大多数情况下都不是问题,但我们可以通过将它包装在自定义 widget 中来修复它。
它不仅可以让我们的应用程序更具性能,而且我们还可以根据我们的意愿定制小部件,例如,我们可以为每个图像小部件创建一个全局解决方案,而不是每次都处理 error/loading 情况。
enum_ImageType { asset,network } class AppImage extends StatelessWidget { const AppImage.asset(this.image,{ super.key,}):type=_ImageType.asset;const AppImage.network(this.image,{ super.key,}):type=_ImageType.network;final String image;final _ImageTypetype;@overrideWidget build(BuildContext context){ const errorWidget=Icon(Icons.error);returnswitch(type){ _ImageType.asset=>Image.asset(image,errorBuilder:(_,__,___)=>errorWidget,),_ImageType.network=>Image.network(image,errorBuilder:(_,__,___)=>errorWidget,),};} }/// 使用const AppImage.asset(''),// OKconst Image.asset(''),// 错误!!Image 没有const构造函数// 正如您所知,拥有 const 的小部件非常重要以获得更好的性能。
有时候由于网络连接不好或其他原因,图片无法第一次加载,显示错误消息是好的,但这还不够,如果我们想把应用的 UX 提升到另一个层次,我们应该让用户重新加载图片,并继续使用应用,而不会遇到任何麻烦。
图片
class MyImage extends StatefulWidget { const MyImage({super.key});@overrideState createState()=>_MyImageState();} class _MyImageState extends State {intattempt=0;@overrideWidget build(BuildContext context){returnCachedNetworkImage(imageUrl: imageUrl,cacheKey:'$attempt',height:200,width:250,fit: BoxFit.cover,errorWidget:(_,__,___){returnRetryWidget(height:200,width:250,onTap:()=>setState(()=>attempt++),);},);} }// Just a basic retry buttonclass RetryWidget extends StatelessWidget { const RetryWidget({ super.key,required this.height,required this.width,required this.onTap,});finaldouble? height;finaldouble? width;final voidFunction()onTap;@overrideWidget build(BuildContext context){returnGestureDetector(onTap: onTap,child: Container(height: height,width: width,alignment: Alignment.center,decoration: const BoxDecoration(color: Colors.black12),child: constColumn(mainAxisSize: MainAxisSize.min,children:[Icon(Icons.image_not_supported,size:20),SizedBox(height:12),Padding(padding: EdgeInsets.symmetric(horizontal:12),child:Text("Image couldn't load, tap here to retry",textAlign: TextAlign.center,style: TextStyle(fontSize:14,color: Colors.black),),),],),),);} }
仅仅展示图片是不够的!您还应该为用户提供他们需要的最佳体验!因此,我强烈建议您创建自己的自定义图片 widget,将它们随意组合并自由使用!
原文:https://medium.com/itnext/12-image-tips-and-best-practices-for-the-best-ux-performance-in-flutter-e7a1b2b1da2a&strip=0&vwsrc=1&referer=medium-parser