You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 11:27:00