Flutter 通用骨架屏封装实战:提升加载体验的 SkeletonWidget

在 Flutter 开发中,网络请求、数据加载时的空白页面是破坏用户体验的 "隐形杀手"------ 用户面对毫无反馈的空白屏,容易产生焦虑感甚至直接退出应用。而骨架屏(Skeleton Screen)作为加载状态的最优解,能通过模拟页面布局结构,让加载过程可视化、过渡更自然。本文将手把手教你封装一个高可定制、易复用、性能优 的通用骨架屏组件SkeletonWidget,支持矩形 / 圆形形态、渐变动画、多场景组合,直接复制即可集成到项目!

一、核心需求拆解(直击开发痛点)

封装前先明确骨架屏的核心使用场景,确保组件通用性和实用性:

  • ✅ 形态适配:支持矩形(文本、按钮、卡片、图片)和圆形(头像、图标)两种基础形态,覆盖 80%+ 组件场景
  • ✅ 样式自定义:可灵活配置尺寸、圆角、基础色、高亮色,适配不同 APP 设计风格(浅色 / 深色模式、圆角大小)
  • ✅ 动画自然:内置线性渐变加载动画,支持自定义动画时长,模拟真实 "加载中" 的视觉反馈
  • ✅ 快捷复用:提供textSkeleton/avatarSkeleton/buttonSkeleton静态方法,无需重复写配置代码
  • ✅ 组合灵活:支持多骨架拼接,轻松实现列表、表单、详情页等复杂布局的加载状态

二、完整代码实现(可直接复制使用)

2.1 基础骨架屏组件(SkeletonWidget)

⚠️ 原代码优化点:将StatelessWidget改为StatefulWidget,通过AnimationController实现循环渐变动画 (原AnimatedContainer无法自动循环,视觉效果生硬),同时补充内存释放逻辑:

dart

复制代码
import 'package:flutter/material.dart';

/// 通用骨架屏组件(支持矩形/圆形、持续渐变动画)
class SkeletonWidget extends StatefulWidget {
  // 必选参数:骨架核心尺寸(宽高)
  final double width;
  final double height;

  // 可选参数:样式与动画配置(均有合理默认值)
  final double borderRadius; // 圆角大小(默认8px)
  final Color baseColor; // 骨架基础色(默认浅灰 #E8E8E8)
  final Color highlightColor; // 渐变高亮色(默认更浅灰 #F5F5F5)
  final Duration animationDuration; // 动画时长(默认1秒)
  final bool isCircle; // 是否为圆形骨架(默认false)

  const SkeletonWidget({
    super.key,
    required this.width,
    required this.height,
    this.borderRadius = 8.0,
    this.baseColor = const Color(0xFFE8E8E8),
    this.highlightColor = const Color(0xFFF5F5F5),
    this.animationDuration = const Duration(seconds: 1),
    this.isCircle = false,
  });

  /// 快捷方法:构建文本骨架(适配单行/多行文本、标题/描述等)
  static Widget textSkeleton({
    double width = double.infinity, // 默认占满父容器宽度
    double height = 16.0, // 默认文本高度(适配14-16号字体)
    double borderRadius = 4.0, // 文本骨架圆角更小,更贴合实际
  }) {
    return SkeletonWidget(
      width: width,
      height: height,
      borderRadius: borderRadius,
    );
  }

  /// 快捷方法:构建圆形头像骨架(适配用户头像、图标等)
  static Widget avatarSkeleton({
    double size = 40.0, // 默认头像尺寸(常用40-60px)
  }) {
    return SkeletonWidget(
      width: size,
      height: size,
      isCircle: true, // 强制圆形
    );
  }

  /// 快捷方法:构建按钮骨架(适配登录、提交、操作按钮等)
  static Widget buttonSkeleton({
    double width = double.infinity, // 默认占满父容器宽度
    double height = 48.0, // 默认按钮高度(常用44-48px)
    double borderRadius = 24.0, // 按钮默认大圆角,更美观
  }) {
    return SkeletonWidget(
      width: width,
      height: height,
      borderRadius: borderRadius,
    );
  }

  @override
  State<SkeletonWidget> createState() => _SkeletonWidgetState();
}

class _SkeletonWidgetState extends State<SkeletonWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController; // 动画控制器
  late Animation<double> _animation; // 动画值(控制渐变位置)

  @override
  void initState() {
    super.initState();
    // 初始化动画:持续循环,往返播放(避免生硬跳转)
    _animationController = AnimationController(
      vsync: this,
      duration: widget.animationDuration,
    )..repeat(reverse: true); // reverse: true 实现渐变往返滑动

    // 动画值范围:-1.0 ~ 1.0,控制渐变起始位置
    _animation = Tween<double>(begin: -1.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeInOut, // 动画曲线:先慢后快再慢,更自然
      ),
    );
  }

  @override
  void dispose() {
    _animationController.dispose(); // 释放动画资源,避免内存泄漏
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // 1. 基础样式:控制骨架形状(圆形/矩形)和背景色
    final BoxDecoration baseDecoration = BoxDecoration(
      color: widget.baseColor,
      borderRadius: widget.isCircle
          ? BorderRadius.circular(widget.height / 2) // 圆形:圆角=高度的1/2
          : BorderRadius.circular(widget.borderRadius),
    );

    return Container(
      width: widget.width,
      height: widget.height,
      decoration: baseDecoration,
      // 2. 裁剪圆角:避免渐变动画溢出圆角范围,视觉更精致
      child: ClipRRect(
        borderRadius: widget.isCircle
            ? BorderRadius.circular(widget.height / 2)
            : BorderRadius.circular(widget.borderRadius),
        child: Stack(
          children: [
            // 3. 渐变动画层:通过AnimatedBuilder监听动画值变化
            Positioned.fill(
              child: AnimatedBuilder(
                animation: _animation,
                builder: (context, child) {
                  return Container(
                    decoration: BoxDecoration(
                      gradient: LinearGradient(
                        colors: [
                          widget.baseColor,
                          widget.highlightColor,
                          widget.baseColor,
                        ],
                        // 动态调整渐变起始/结束位置,实现滑动效果
                        begin: Alignment.centerLeft.add(
                          Alignment(_animation.value, 0.0),
                        ),
                        end: Alignment.centerRight.add(
                          Alignment(_animation.value, 0.0),
                        ),
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

2.2 组合示例:列表项骨架屏(ListItemSkeleton)

基于SkeletonWidget组合,实现 APP 中最常用的 "头像 + 文本 + 箭头" 列表项加载状态:

dart

复制代码
/// 列表项骨架屏(组合使用示例:适配消息列表、商品列表、设置项等)
class ListItemSkeleton extends StatelessWidget {
  const ListItemSkeleton({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 10.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中对齐
        children: [
          // 左侧:圆形头像骨架
          SkeletonWidget.avatarSkeleton(size: 50.0),
          const SizedBox(width: 15.0), // 头像与文本间距

          // 中间:文本区域(2行文本,模拟标题+描述)
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start, // 水平左对齐
              mainAxisSize: MainAxisSize.min, // 仅占用子组件高度,避免不必要拉伸
              children: [
                // 标题文本骨架(略高,模拟粗体)
                SkeletonWidget.textSkeleton(width: 150.0, height: 20.0),
                const SizedBox(height: 8.0), // 文本行间距
                // 描述文本骨架(略矮,模拟常规字体)
                SkeletonWidget.textSkeleton(width: 100.0, height: 16.0),
              ],
            ),
          ),

          // 右侧:圆形箭头图标骨架
          SkeletonWidget(width: 20.0, height: 20.0, isCircle: true),
        ],
      ),
    );
  }
}

三、实战使用示例(覆盖主流场景)

3.1 单个基础骨架(适配图片、卡片、独立组件)

适用于单个组件的加载状态(如商品图片、广告位、独立卡片):

dart

复制代码
// 单个矩形骨架(示例:商品图片加载)
SkeletonWidget(
  width: 200.0,
  height: 100.0,
  borderRadius: 12.0, // 自定义圆角
  baseColor: const Color(0xFFF0F0F0), // 自定义基础色(更深一点的灰)
  highlightColor: const Color(0xFFF8F8F8), // 自定义高亮色
  animationDuration: const Duration(milliseconds: 1500), //  slower动画(更柔和)
),

3.2 快捷文本骨架(适配标题、描述、多行文本)

无需重复配置,通过静态方法快速构建文本加载状态:

dart

复制代码
// 场景1:单行标题骨架(如页面标题)
SkeletonWidget.textSkeleton(width: 180.0, height: 22.0),

// 场景2:多行描述骨架(如商品详情、文章摘要)
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    SkeletonWidget.textSkeleton(width: double.infinity), // 占满父容器宽度
    const SizedBox(height: 8.0),
    SkeletonWidget.textSkeleton(width: double.infinity),
    const SizedBox(height: 8.0),
    SkeletonWidget.textSkeleton(width: 250.0), // 部分宽度(模拟文本截断效果)
  ],
),

3.3 列表骨架(适配 ListView、Column 列表)

配合ListView.builder实现无限列表加载状态(如消息列表、商品列表):

dart

复制代码
ListView.builder(
  itemCount: 5, // 展示5个骨架项(避免过多导致滚动卡顿)
  padding: const EdgeInsets.all(10.0),
  itemBuilder: (context, index) => const ListItemSkeleton(),
),

3.4 表单骨架(适配登录页、个人中心、设置页)

组合头像、文本、按钮骨架,实现表单类页面加载状态:

dart

复制代码
// 场景:个人中心加载状态
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    SkeletonWidget.avatarSkeleton(size: 60.0), // 头像骨架
    const SizedBox(height: 20.0),
    SkeletonWidget.textSkeleton(width: 120.0, height: 24.0), // 用户名文本
    const SizedBox(height: 15.0),
    SkeletonWidget.textSkeleton(width: double.infinity, height: 16.0), // 个性签名
    const SizedBox(height: 30.0),
    SkeletonWidget.buttonSkeleton(width: 180.0), // 编辑资料按钮
  ],
),

四、核心封装技巧(让组件更易用、更优雅)

4.1 静态快捷方法:降低使用成本

针对文本、头像、按钮等高频场景,封装静态方法,将重复配置(如isCircle: trueborderRadius: 24)固化,开发者无需关注细节,直接调用即可,开发效率提升 50%。

4.2 动画循环优化:视觉更自然

原代码的AnimatedContainer仅执行一次动画,优化后通过AnimationController+AnimatedBuilder实现:

  • 动画往返播放(repeat(reverse: true)),避免生硬跳转
  • 动画曲线使用Curves.easeInOut,模拟真实加载的 "呼吸感"
  • 及时释放AnimationController,避免内存泄漏

4.3 样式适配:兼容多形态、多主题

  • 圆形骨架自动计算圆角(height/2),无需手动配置,避免开发者算错
  • 基础色、高亮色支持自定义,适配浅色 / 深色模式(示例见下文 "注意事项")
  • ClipRRect裁剪渐变动画,避免溢出圆角,视觉更精致

4.4 组合复用:遵循 "单一职责"

基础SkeletonWidget仅负责单个组件的样式和动画,复杂布局(列表、表单)通过组合基础骨架实现,既降低了单个组件的复杂度,又提高了复用性,后续修改样式只需改基础组件,无需改动所有组合场景。

五、避坑指南(实际开发必看)

  1. 颜色搭配技巧 :基础色和高亮色的亮度差建议控制在 10-20 之间(如#E8E8E8#F5F5F5),差异过大导致动画刺眼,差异过小则看不见动画效果。
  2. 动画时长建议:1-1.5 秒是最佳区间,过快(<0.8 秒)会让用户视觉疲劳,过慢(>2 秒)会让用户误以为加载卡顿。
  3. 尺寸适配原则 :优先使用MediaQueryLayoutBuilder获取父容器尺寸(如MediaQuery.of(context).size.width * 0.8),避免固定像素导致不同屏幕适配问题。
  4. 及时替换骨架屏 :网络请求成功后,务必通过State(如isLoading变量)切换骨架屏为真实内容,避免一直显示骨架。
  5. 深色模式支持 :通过Theme动态切换颜色,示例如下:

dart

复制代码
// 深色模式适配逻辑
Color baseColor = Theme.of(context).brightness == Brightness.dark
    ? const Color(0xFF333333)
    : const Color(0xFFE8E8E8);
Color highlightColor = Theme.of(context).brightness == Brightness.dark
    ? const Color(0xFF444444)
    : const Color(0xFFF5F5F5);

// 使用时传入颜色
SkeletonWidget(
  width: 200.0,
  height: 100.0,
  baseColor: baseColor,
  highlightColor: highlightColor,
),

六、进阶拓展场景(提升组件上限)

  1. 网格布局骨架 :结合GridView.builderSkeletonWidget,实现商品网格、图片墙的加载状态。
  2. 自定义渐变方向 :新增Axis参数,支持水平 / 垂直渐变,适配按钮、长文本等场景。
  3. 骨架屏过渡动画 :通过AnimatedSwitcher实现骨架屏到真实内容的淡入 / 缩放过渡,视觉更流畅:

dart

复制代码
AnimatedSwitcher(
  duration: const Duration(milliseconds: 300),
  child: isLoading
      ? SkeletonWidget.textSkeleton()
      : Text("真实文本内容", key: const ValueKey("content")),
),
  1. 骨架屏缓存:对于频繁切换的页面(如底部导航栏页面),缓存骨架屏实例,减少重建开销。

总结

本文封装的SkeletonWidget具备通用、灵活、高性能三大核心优势,无需依赖任何第三方库,直接集成到项目即可使用。通过静态快捷方法、动画优化、组合复用等技巧,覆盖了列表、表单、卡片、文本等绝大多数加载场景,有效解决了空白屏带来的用户体验问题。

https://openharmonycrossplatform.csdn.net/content

相关推荐
恋猫de小郭12 小时前
Android 禁止侧载将正式实施,需要等待 24 小时冷静期
android·flutter·harmonyos
FFF-X13 小时前
解决 Flutter Gradle 下载报错:修改默认 distributionUrl
flutter
程序员Ctrl喵1 天前
异步编程:Event Loop 与 Isolate 的深层博弈
开发语言·flutter
前端不太难1 天前
Flutter 如何设计可长期维护的模块边界?
flutter
小蜜蜂嗡嗡1 天前
flutter列表中实现置顶动画
flutter
始持1 天前
第十二讲 风格与主题统一
前端·flutter
始持1 天前
第十一讲 界面导航与路由管理
flutter·vibecoding
始持1 天前
第十三讲 异步操作与异步构建
前端·flutter
新镜1 天前
【Flutter】 视频视频源横向、竖向问题
flutter
黄林晴2 天前
Compose Multiplatform 1.10 发布:统一 Preview、Navigation 3、Hot Reload 三箭齐发
android·flutter