在 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: true、borderRadius: 24)固化,开发者无需关注细节,直接调用即可,开发效率提升 50%。
4.2 动画循环优化:视觉更自然
原代码的AnimatedContainer仅执行一次动画,优化后通过AnimationController+AnimatedBuilder实现:
- 动画往返播放(
repeat(reverse: true)),避免生硬跳转 - 动画曲线使用
Curves.easeInOut,模拟真实加载的 "呼吸感" - 及时释放
AnimationController,避免内存泄漏
4.3 样式适配:兼容多形态、多主题
- 圆形骨架自动计算圆角(
height/2),无需手动配置,避免开发者算错 - 基础色、高亮色支持自定义,适配浅色 / 深色模式(示例见下文 "注意事项")
ClipRRect裁剪渐变动画,避免溢出圆角,视觉更精致
4.4 组合复用:遵循 "单一职责"
基础SkeletonWidget仅负责单个组件的样式和动画,复杂布局(列表、表单)通过组合基础骨架实现,既降低了单个组件的复杂度,又提高了复用性,后续修改样式只需改基础组件,无需改动所有组合场景。
五、避坑指南(实际开发必看)
- 颜色搭配技巧 :基础色和高亮色的亮度差建议控制在 10-20 之间(如
#E8E8E8和#F5F5F5),差异过大导致动画刺眼,差异过小则看不见动画效果。 - 动画时长建议:1-1.5 秒是最佳区间,过快(<0.8 秒)会让用户视觉疲劳,过慢(>2 秒)会让用户误以为加载卡顿。
- 尺寸适配原则 :优先使用
MediaQuery或LayoutBuilder获取父容器尺寸(如MediaQuery.of(context).size.width * 0.8),避免固定像素导致不同屏幕适配问题。 - 及时替换骨架屏 :网络请求成功后,务必通过
State(如isLoading变量)切换骨架屏为真实内容,避免一直显示骨架。 - 深色模式支持 :通过
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,
),
六、进阶拓展场景(提升组件上限)
- 网格布局骨架 :结合
GridView.builder和SkeletonWidget,实现商品网格、图片墙的加载状态。 - 自定义渐变方向 :新增
Axis参数,支持水平 / 垂直渐变,适配按钮、长文本等场景。 - 骨架屏过渡动画 :通过
AnimatedSwitcher实现骨架屏到真实内容的淡入 / 缩放过渡,视觉更流畅:
dart
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLoading
? SkeletonWidget.textSkeleton()
: Text("真实文本内容", key: const ValueKey("content")),
),
- 骨架屏缓存:对于频繁切换的页面(如底部导航栏页面),缓存骨架屏实例,减少重建开销。
总结
本文封装的SkeletonWidget具备通用、灵活、高性能三大核心优势,无需依赖任何第三方库,直接集成到项目即可使用。通过静态快捷方法、动画优化、组合复用等技巧,覆盖了列表、表单、卡片、文本等绝大多数加载场景,有效解决了空白屏带来的用户体验问题。