Flutter SliverList加载assets图片时卡顿,如何判断全部渲染完成以控制加载动画
问题解决方案
一、检测所有图片加载完成,实现加载动画自动关闭
Flutter的Image组件可以通过ImageStream监听加载状态,我们只需要统计全部待加载图片的总数,每成功加载一张就更新计数,计数等于总数时即可关闭加载动画。
代码修改示例
首先修改页面State代码:
class _YourPageState extends State<YourPage> { // 待加载图片总数 late int totalImageCount; // 已加载完成图片数 int loadedImageCount = 0; // 是否显示加载动画 bool get showLoading => loadedImageCount < totalImageCount; @override void initState() { super.initState(); // 提前统计所有待加载图片总数:头像数+帖子内图片数 int avatarCount = posts.where((post) => post.avatarPath != null).length; int postImageCount = posts.fold(0, (sum, post) => sum + post.imagePaths.length); totalImageCount = avatarCount + postImageCount; } // 图片加载完成的回调 void onImageLoaded() { if (!mounted) return; setState(() { loadedImageCount++; }); } @override Widget build(BuildContext context) { return Stack( children: [ CustomScrollView( slivers: [ SliverAppBar( title: Text("Cossy", style: TextStyle(color: Colors.purple)), pinned: true, backgroundColor: backgroundColor, systemOverlayStyle: SystemUiOverlayStyle( systemNavigationBarColor: backgroundColor, ), ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) => Padding( child: Post( onImageLoaded: onImageLoaded, ownerName: posts[index].ownerName, avatarPath: posts[index].avatarPath, imagePaths: posts[index].imagePaths, ), padding: EdgeInsets.only( bottom: (index == posts.length - 1) ? 82 : 12, ), ), childCount: posts.length, ), ) ], ), // 加载动画,加载完成后自动隐藏 if (showLoading) const Center( child: CircularProgressIndicator(color: Colors.purple), ) ], ); } }
然后修改Post组件,添加加载监听:
class Post extends StatelessWidget { Post({ Key? key, this.avatarPath, required this.ownerName, required this.imagePaths, required this.onImageLoaded, }) : super(key: key); final String? avatarPath; final String ownerName; final List<String> imagePaths; final VoidCallback onImageLoaded; @override Widget build(BuildContext context) { if (imagePaths.isEmpty) return const Text("no image?"); // 提前拿到屏幕尺寸,避免重复计算 final screenSize = MediaQuery.of(context).size; // 监听头像加载完成 if (avatarPath != null) { AssetImage(avatarPath!).resolve(const ImageConfiguration()).addListener( ImageStreamListener((_, __) => onImageLoaded(), onError: (_, __) => onImageLoaded()) ); } return Container( decoration: BoxDecoration( boxShadow: [ BoxShadow( color: Color(0xFFF2F2F2), offset: Offset(0.0, 1.0), blurRadius: 6.0, ), ], color: Colors.deepOrange, borderRadius: BorderRadius.all(Radius.circular(10)), ), child: Column( children: [ Padding( padding: const EdgeInsets.all(4.0), child: Row( children: [ CircleAvatar( child: avatarPath == null ? Icon(Icons.person) : null, backgroundImage: avatarPath == null ? null : AssetImage(avatarPath ?? ""), ), SizedBox(width: 10), Text(ownerName), Expanded( child: SizedBox(height: 30), ), Icon(Icons.settings), ], ), ), Carousel( indicatorBarColor: Colors.transparent, autoScrollDuration: Duration(seconds: 2), animationPageDuration: Duration(milliseconds: 500), activateIndicatorColor: Colors.grey.shade200, animationPageCurve: Curves.bounceInOut, indicatorBarHeight: 30, indicatorHeight: 10, indicatorWidth: 20, unActivatedIndicatorColor: Colors.grey.shade700, stopAtEnd: true, autoScroll: false, items: imagePaths .map( (imagePath) => Container( decoration: BoxDecoration(color: Colors.black), child: ConstrainedBox( constraints: BoxConstraints( minHeight: 100, minWidth: 100, maxHeight: screenSize.height * 1.5, maxWidth: screenSize.width, ), child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.center, child: Image( image: AssetImage(imagePath), // 指定缓存尺寸,大幅降低内存占用 cacheWidth: screenSize.width.floor(), // 监听图片加载完成 frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { if (wasSynchronouslyLoaded || frame != null) { WidgetsBinding.instance.addPostFrameCallback((_) { onImageLoaded(); }); } return child; }, ), ), ), ), ) .toList(), ), Padding( padding: const EdgeInsets.only( left: 16, right: 16, top: 4, ), child: Row( children: [ TextButton( onPressed: () { print(123); }, child: Icon( Icons.star_border_outlined, size: 30, ), ), Expanded( child: SizedBox( height: 30, ), ), Icon( Icons.send_outlined, size: 30, ), ], ), ), ], ), ); } }
二、卡顿优化可行方案
- 指定图片缓存尺寸:上面代码中添加的
cacheWidth/cacheHeight参数是优化核心,Flutter默认会加载图片原始分辨率到内存,一张4K图哪怕显示在300px宽的区域也会占用几十MB内存,指定缓存尺寸后会把图片压缩到对应分辨率再加载,内存占用可降低90%以上,基本解决加载大图导致的卡顿。 - 轮播图懒加载:当前使用的Carousel组件会一次性加载所有轮播图片,哪怕用户没滑到对应位置也会加载,建议换成PageView.builder自定义轮播,只有用户滑到对应页码时再加载图片,减少同时加载的资源数量。
- 资源分级加载:如果帖子数量较多,不要一次性加载所有帖子的图片,只加载当前屏幕可视区域内的帖子资源,滚动到其他区域时再加载对应位置的图片。
- 提前预加载首屏资源:进入当前页面前,可以用
precacheImage方法提前加载首屏前2-3条帖子的图片到内存,进入页面后可直接显示,不会出现卡顿。 - 优化图片资源本身:提前把assets目录下的图片做压缩处理,不要放分辨率过高的原图,根据目标设备屏幕分辨率准备2x、3x的切图,减少资源本身的体积。
内容的提问来源于stack exchange,提问作者Muhammed Aydogan
相关产品推荐
相关产品推荐

