Flutter 三方库 shimmer 的鸿蒙化适配与实战指南


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Hello 各位小伙伴!,上海某大学计算机专业大一学生 🎓。今天来聊一个"看不见摸不着但用户体验超重要"的东西——骨架屏动画

你有没有这种体验:打开一个 App,页面加载时是一片空白,然后内容突然蹦出来?那种"闪一下"的感觉特别不舒服!骨架屏就是来解决这个问题的!

一、什么是骨架屏?

骨架屏(Skeleton Screen)就是在内容加载时,先显示一个灰色的轮廓占位,用闪烁动画表示"正在加载中"。等真实数据到来后,骨架屏自然过渡到真实内容。

好处:

  • ✅ 减少用户等待的焦虑感
  • ✅ 告诉用户"数据正在路上"
  • ✅ 比 Loading 动画更轻量、更现代
  • ✅ 提升整体 App 品质感

二、shimmer 库介绍

shimmer 是 Flutter 里最常用的骨架屏库,它的特点:

  • 纯 Dart 实现,零平台依赖
  • API 简洁,一看就会
  • 动画效果流畅
  • 高度可定制

三、依赖配置

dependencies:
  shimmer: ^3.0.0

AtomGit 适配说明:纯 Dart 库,无平台特定代码,鸿蒙适配零成本,完美运行!

四、基础用法

最简单的骨架屏

import 'package:shimmer/shimmer.dart';

Shimmer.fromColors(
  baseColor: Colors.grey[300]!,   // 骨架基础色
  highlightColor: Colors.grey[100]!,  // 高亮闪烁色
  child: Container(
    width: 200,
    height: 100,
    decoration: BoxDecoration(
      color: Colors.white,  // 这里颜色不重要,会被覆盖
      borderRadius: BorderRadius.circular(8),
    ),
  ),
)

就这么简单!三行代码就能实现闪烁效果!

五、在聊天消息列表中实战

这是我在聊天 App 里加载消息列表时用的骨架屏:

import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';

/// 消息列表骨架屏
class MessageListSkeleton extends StatelessWidget {
  const MessageListSkeleton({super.key});

  
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 6,  // 显示6个骨架项
      padding: const EdgeInsets.all(16),
      itemBuilder: (context, index) => const _MessageSkeletonItem(),
    );
  }
}

/// 单条消息骨架项
class _MessageSkeletonItem extends StatelessWidget {
  const _MessageSkeletonItem();

  
  Widget build(BuildContext context) {
    return Shimmer.fromColors(
      baseColor: Colors.grey[300]!,
      highlightColor: Colors.grey[100]!,
      // 【重要】child 可以是任意 widget
      child: Padding(
        padding: const EdgeInsets.only(bottom: 16),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 头像骨架
            Container(
              width: 48,
              height: 48,
              decoration: const BoxDecoration(
                color: Colors.white,  // 会被 shimmer 覆盖
                shape: BoxShape.circle,
              ),
            ),
            const SizedBox(width: 12),
            // 文字内容骨架
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // 昵称骨架
                  Container(
                    width: 100,
                    height: 16,
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(4),
                    ),
                  ),
                  const SizedBox(height: 8),
                  // 消息内容骨架
                  Container(
                    width: double.infinity,  // 【重点】尽量用具体宽度或自适应
                    height: 14,
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(4),
                    ),
                  ),
                  const SizedBox(height: 6),
                  // 消息内容第二行
                  Container(
                    width: 200,
                    height: 14,
                    decoration: BoxDecoration(
                      color: Colors.white,
                      borderRadius: BorderRadius.circular(4),
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(width: 8),
            // 时间骨架
            Container(
              width: 50,
              height: 12,
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.circular(4),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

效果预览

┌─────────────────────────────────┐
│  [○]  张三                        │
│      你好,最近怎么样?            │
│      10:30                       │
├─────────────────────────────────┤
│  [○]  李四                        │
│      项目进度怎么样了?            │
│      昨天                        │
├─────────────────────────────────┤
│  ...闪烁中...                    │
└─────────────────────────────────┘

六、聊天详情页骨架屏

/// 聊天详情页骨架屏
class ChatDetailSkeleton extends StatelessWidget {
  const ChatDetailSkeleton({super.key});

  
  Widget build(BuildContext context) {
    return Shimmer.fromColors(
      baseColor: Colors.grey[300]!,
      highlightColor: Colors.grey[100]!,
      child: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // 对方消息
          _buildMessageBubble(isMe: false),
          const SizedBox(height: 12),
          
          // 我的消息
          Align(
            alignment: Alignment.centerRight,
            child: _buildMessageBubble(isMe: true),
          ),
          const SizedBox(height: 12),
          
          // 更多消息
          _buildMessageBubble(isMe: false),
          const SizedBox(height: 12),
          
          _buildMessageBubble(isMe: false),
          const SizedBox(height: 12),
          
          Align(
            alignment: Alignment.centerRight,
            child: _buildMessageBubble(isMe: true),
          ),
        ],
      ),
    );
  }

  Widget _buildMessageBubble({required bool isMe}) {
    return Row(
      mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start,
      children: [
        if (!isMe) ...[
          Container(
            width: 36,
            height: 36,
            decoration: const BoxDecoration(
              color: Colors.white,
              shape: BoxShape.circle,
            ),
          ),
          const SizedBox(width: 8),
        ],
        Container(
          width: 200,
          height: 44,
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(18),
          ),
        ),
        if (isMe) const SizedBox(width: 44),  // 头像占位
      ],
    );
  }
}

七、商品列表骨架屏

/// 商品网格骨架屏
class ProductGridSkeleton extends StatelessWidget {
  const ProductGridSkeleton({super.key});

  
  Widget build(BuildContext context) {
    return Shimmer.fromColors(
      baseColor: Colors.grey[300]!,
      highlightColor: Colors.grey[100]!,
      child: GridView.builder(
        padding: const EdgeInsets.all(16),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 0.7,
          crossAxisSpacing: 12,
          mainAxisSpacing: 12,
        ),
        itemCount: 6,
        itemBuilder: (context, index) => _ProductSkeletonItem(),
      ),
    );
  }
}

class _ProductSkeletonItem extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(8),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 商品图片
          Container(
            width: double.infinity,
            height: 120,
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(8),
            ),
          ),
          const SizedBox(height: 8),
          // 商品名称
          Container(
            width: double.infinity,
            height: 14,
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(4),
            ),
          ),
          const SizedBox(height: 6),
          Container(
            width: 80,
            height: 14,
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(4),
            ),
          ),
          const SizedBox(height: 8),
          // 价格
          Container(
            width: 60,
            height: 18,
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(4),
            ),
          ),
        ],
      ),
    );
  }
}

八、自定义 shimmer 效果

渐变方向

// 从左到右渐变(默认)
Shimmer.fromColors(
  direction: ShimmerDirection.ltr,
  ...
)

// 从右到左
Shimmer.fromColors(
  direction: ShimmerDirection.rtl,
  ...
)

// 从上到下
Shimmer.fromColors(
  direction: ShimmerDirection.ttb,
  ...
)

// 从下到上
Shimmer.fromColors(
  direction: ShimmerDirection.btt,
  ...
)

自定义颜色

// 浅蓝色主题
Shimmer.fromColors(
  baseColor: Colors.blueGrey[100]!,
  highlightColor: Colors.blueGrey[50]!,
  child: ...
)

// 深色主题
Shimmer.fromColors(
  baseColor: Colors.grey[800]!,
  highlightColor: Colors.grey[700]!,
  child: ...
)

// 品牌色主题
Shimmer.fromColors(
  baseColor: const Color(0xFF6366F1).withOpacity(0.3),
  highlightColor: const Color(0xFF6366F1).withOpacity(0.1),
  child: ...
)

渐变区间

Shimmer.fromColors(
  // gradient: 自定义渐变
  gradient: LinearGradient(
    colors: [
      Colors.grey[300]!,
      Colors.grey[100]!,
      Colors.grey[300]!,
    ],
    stops: const [0.0, 0.5, 1.0],  // 渐变位置
  ),
  child: ...
)

九、配合状态管理使用

class ChatListPage extends StatefulWidget {
  const ChatListPage({super.key});

  
  State<ChatListPage> createState() => _ChatListPageState();
}

class _ChatListPageState extends State<ChatListPage> {
  bool _isLoading = true;
  List<ChatItem> _chats = [];

  
  void initState() {
    super.initState();
    _loadChats();
  }

  Future<void> _loadChats() async {
    setState(() => _isLoading = true);
    
    // 模拟网络请求
    await Future.delayed(const Duration(seconds: 2));
    
    final chats = await ChatService.getChats();
    
    setState(() {
      _chats = chats;
      _isLoading = false;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: _isLoading 
          ? const MessageListSkeleton()  // 加载中显示骨架屏
          : _buildChatList(),            // 加载完成显示真实内容
    );
  }

  Widget _buildChatList() {
    return ListView.builder(
      itemCount: _chats.length,
      itemBuilder: (context, index) => ChatListItem(
        chat: _chats[index],
        onTap: () => _openChat(_chats[index]),
      ),
    );
  }
}

十、踩坑纪实

踩坑1:骨架屏颜色太深/太浅 🎨

一开始用的默认灰色,结果在深色模式下根本看不出效果。后来根据主题动态调整颜色:

// 根据主题选择颜色
Shimmer.fromColors(
  baseColor: Theme.of(context).brightness == Brightness.dark
      ? Colors.grey[700]!
      : Colors.grey[300]!,
  highlightColor: Theme.of(context).brightness == Brightness.dark
      ? Colors.grey[600]!
      : Colors.grey[100]!,
  child: ...
)

踩坑2:Container 高度为 0 📏

用 Shimmer 包裹 Container 时,如果 Container 没有设置宽高,骨架屏不会显示!因为 Shimmer 的 child 是用来"裁剪"渐变的。

踩坑3:和动画冲突 🔄

我之前在一个动画组件里用了骨架屏,结果动画和 shimmer 闪烁叠在一起特别奇怪。后来把骨架屏放在 AnimatedSwitcher 里面,切换时用了渐隐效果:

AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  child: _isLoading 
      ? const MessageListSkeleton(key: ValueKey('skeleton'))
      : _buildChatList(key: const ValueKey('content')),
)

十一、效果展示

在这里插入图片描述

功能验证结果:

  • ✅ 骨架屏加载动画正常显示
  • ✅ 渐变动画流畅无卡顿
  • ✅ 不同场景(列表、详情、网格)均可使用
  • ✅ 深色/浅色主题适配正常
  • ✅ 与数据加载状态切换正常

十二、总结心得

骨架屏真的是"小投入大回报"的功能!加上去之后 App 质感直接提升好几个档次,用户体验也好了很多。

关键收获:

  1. Shimmer 的 child 就是"画布",会从左到右被渐变色覆盖
  2. 骨架屏的每个元素宽高要尽量符合真实内容,避免布局跳动
  3. 要考虑深色模式的适配
  4. 配合 AnimatedSwitcher 使用,切换更自然

给新手的话:
别觉得这是"面子工程"!用户体验就是由这些细节决定的。一个加载友好的 App 和一个"白屏等待"的 App,给人的感觉完全不一样!


今天的分享就到这里!骨架屏虽小,但作用大大的!有问题评论区见!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐