- 文章信息 - Author: Jack Lee (jcLee95)
Visit me at: https://jclee95.blog.csdn.net
Email: 291148484@163.com.
Shenzhen China
Address of this article:https://blog.csdn.net/qq_28550263/article/details/134224135
【介绍】:本文介绍Flutter应用开发中,两个优秀的UI骨骼化模块以其实战中的用法。
目 录
- 1. 骨架化加载简介
- 2. 基于 shimmer 实现骨架化加载
- 2.1 shimmer 的安装
- 2.2 使用 Shimmer.fromColors 创建闪烁页面
- 2.3 更贴近实战:配合异步更新页面数据
- 3. 基于 skeletonizer 实现骨架化加载
- 安装 `skeletonizer` 依赖:
- 实战骨架加载界面
- F. 附录
- F1. shimmer 库源码分析
- ShimmerDirection 枚举
- Shimmer 组件:对外暴露的接口,渲染闪烁效果
- Shimmer的状态类_ShimmerState :用于控制动画的播放和停止
- 私有 _Shimmer 组件:用于实现Shimmer的渲染效果
- _ShimmerFilter私有的渲染对象:用于实现Shimmer的渲染效果
- F2. skeletonizer 库部分源码分析
1. 骨架化加载简介
在 Flutter 中,实现 UI骨架加载(Skeleton UI)可以通过使用一些内置的组件和库来创建简化的占位符用户界面。这有助于增强用户体验,因为用户可以立即看到页面正在加载,并且不会感到等待时间过长。
在 Flutter 中,你可以直接使用第三方库 shimmer
或者 skeletonizer
来实现 UI骨架加载 (Skeleton UI)。这两个库可以帮助你创建 占位符用户界面 ,以改善用户体验,尤其是在数据加载时。下面我将分别讲解如何使用这两个库来实现骨架加载。
pub.dev 上,一个流行度较高的骨架化加载器为 shimmer。本节介绍一下该骨架化加载器的用法。
2.1 shimmer 的安装
使用 shimmer
库:
- 添加
shimmer
依赖:
在你的 Flutter 项中运行以下命令:
flutter pub add shimmer
2.2 使用 Shimmer.fromColors 创建闪烁页面
使用 Shimmer.fromColors
来包装你的加载内容。
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Shimmer',
home: SkeletonLoadingScreen(), SkeletonLoadingScreen
);
}
}
class SkeletonLoadingScreen extends StatelessWidget {
const SkeletonLoadingScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Loading...'),
),
// 使用Shimmer.fromColors创建闪烁效果
body: Shimmer.fromColors(
baseColor: Colors.grey[500]!, // 基础颜色,闪烁效果的底色
highlightColor: Colors.grey[100]!, // 高亮颜色,闪烁效果的高亮部分颜色
child: ListView.builder( // 使用ListView.builder构建一个列表视图
itemCount: 10, // 模拟加载的项目数量,这里设置为10个
itemBuilder: (BuildContext context, int index) { // 列表项构建器,根据index创建每个列表项
return const ListTile( // 创建一个列表项
leading: CircleAvatar(), // 列表项左侧的头像占位符
title: Text('Loading...'),
subtitle: Text('Loading...'),
);
},
),
),
);
}
}
这个示例创建了一个,包含一个闪烁的加载屏幕的Flutter应用,用于模拟数据加载过程。闪烁效果是通过shimmer库的Shimmer.fromColors创建的,用于吸引用户的注意力,直到实际数据加载完毕。其运行后的效果如下:
2.3 更贴近实战:配合异步更新页面数据
上一节仅仅是对该库接口用法的介绍。实际中,我们也不能一直显示为这样的状态,而一般是有一个异步的数据请求,直到请求完成后,将页面骨骼显示为真实的数据页面。因此,下面的例子展示的是一个更加贴近实战的情况。(除了 SkeletonLoadingScreen 的部分保持不变)
class SkeletonLoadingScreen extends StatelessWidget {
const SkeletonLoadingScreen({super.key});
// _fetchData函数模拟了一个异步获取数据的请求
Future<List<String>> _fetchData() async {
await Future.delayed(const Duration(seconds: 3)); // 模拟网络请求延迟3秒
return List<String>.generate(
10, (index) => 'Item $index'); // 模拟获取的数据,生成一个包含10个字符串的列表
}
Widget build(BuildContext context) {
return FutureBuilder<List<String>>(
future: _fetchData(), // 异步获取数据
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
// 根据Future的状态(等待、完成或错误)构建不同的界面
return Scaffold(
appBar: AppBar(
// 如果数据正在加载,标题显示"Loading...",否则显示"Loaded"
title: Text(snapshot.connectionState == ConnectionState.waiting
? 'Loading...'
: 'Loaded'),
),
body: snapshot.connectionState == ConnectionState.waiting
? Shimmer.fromColors(
// 如果数据正在加载,显示闪烁的加载屏幕
baseColor: Colors.grey[300]!, // 闪烁效果的底色
highlightColor: Colors.grey[100]!, // 闪烁效果的高亮部分颜色
child: ListView.builder(
itemCount: 10, // 模拟加载的项目数量,这里设置为10个
itemBuilder: (BuildContext context, int index) {
// 列表项构建器,根据index创建每个列表项
return const ListTile(
leading: CircleAvatar(), // 列表项左侧的头像占位符
title: Text('Loading...'), // 列表项的标题文本
subtitle: Text('Loading...'), // 列表项的副标题文本
);
},
),
)
: snapshot.hasError
? Text('Error: ${snapshot.error}') // 如果加载出错,显示错误信息
: ListView.builder(
itemCount: snapshot.data!.length, // 加载完成后的项目数量
itemBuilder: (BuildContext context, int index) {
// 列表项构建器,根据index创建每个列表项
return ListTile(
leading: const CircleAvatar(), // 列表项左侧的头像占位符
title: Text(
snapshot.data![index]), // 列表项的标题文本,显示加载完成后的数据
subtitle:
const Text('Loaded'), // 列表项的副标题文本,显示"Loaded"
);
},
),
);
},
);
}
}
其效果如下:
可以看到,当我热重载应用后,先进入了页面骨骼阶段。直到 _fetchData
(请求加载数据)完成,显示为真实的页面数据。
pub.dev 上,另外一个流行度较高的骨架化加载器为 skeletonizer。本节介绍一下该骨架化加载器的用法。
安装 skeletonizer
依赖:
在你的 Flutter 项目的 pubspec.yaml
文件中,添加 skeletonizer
依赖:
flutter pub add skeletonizer
实战骨架加载界面
实际上 skeletonizer 库的官方示例中,是使用一个按钮手动切换数据加载后的。不过为了模拟实际情况,我还是使用了一个_futureData 函数模拟异步数据请求,实际上是延时2秒。在页面初始化状态时执行这个异步操作,模拟完成后使用真实数据。代码如下:
import 'package:flutter/material.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'dart:async';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Skeletonizer Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData.light(useMaterial3: true),
home: const SkeletonizerDemoPage(),
);
}
}
class SkeletonizerDemoPage extends StatefulWidget {
const SkeletonizerDemoPage({super.key});
State<SkeletonizerDemoPage> createState() => _SkeletonizerDemoPageState();
}
class _SkeletonizerDemoPageState extends State<SkeletonizerDemoPage> {
late Future<List<String>> _futureData;
void initState() {
super.initState();
_futureData = _fetchData();
}
Future<List<String>> _fetchData() async {
// 模拟网络延迟
await Future.delayed(const Duration(seconds: 2));
// 返回模拟数据
return List<String>.generate(6, (index) => 'Item number $index as title');
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Skeletonizer Demo'),
),
body: FutureBuilder<List<String>>(
future: _futureData,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Skeletonizer(
enabled: true,
child: ListView.builder(
itemCount: 6,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
return const Card(
child: ListTile(
title: Text('Loading...'),
subtitle: Text('Subtitle here'),
trailing: Icon(
Icons.ac_unit,
size: 32,
),
),
);
},
),
);
} else {
return ListView.builder(
itemCount: snapshot.data!.length,
padding: const EdgeInsets.all(16),
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text(snapshot.data![index]),
subtitle: const Text('Subtitle here'),
trailing: const Icon(
Icons.ac_unit,
size: 32,
),
),
);
},
);
}
},
),
);
}
}
其中,在 SkeletonizerDemoPage 页面脚手架的 body 中,使用了 FutureBuilder 组件,它是Flutter中用于处理异步操作的一个非常有用的组件。
FutureBuilder接受两个主要的参数:future
和 builder
。
-
future参数接受一个Future对象,这里是_futureData,它是在initState方法中初始化的,用于模拟异步获取数据的过程。
-
builder参数是一个返回组件的函数,它接受两个参数:BuildContext和AsyncSnapshot。BuildContext是当前组件的上下文,AsyncSnapshot包含了future的最新状态和数据。
在 builder
函数中,首先检查 snapshot
是否有数据。如果 snapshot.hasData
为 false
,说明 _futureData
(模拟异步请求数据)还没有完成,此时返回一个 Skeletonizer 组件,显示骨架屏。Skeletonizer 组件中的 ListView.builder 用于生成骨架屏的列表项。
如果 snapshot.hasData 为 true
,说明 _futureData
已经完成,此时返回一个 ListView.builder,显示真实的数据。 => 这里的 ListView.builder 用于生成包含真实数据的列表项,列表项的数量由 snapshot.data.length
决定,列表项的内容由 snapshot.data[index]
提供。
这段示例代码的运行效果如下:
ShimmerDirection 枚举
/// shimmer库
library shimmer;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
/// 定义所有支持的闪烁效果方向的枚举
///
/// * [ShimmerDirection.ltr] 从左到右
/// * [ShimmerDirection.rtl] 从右到左
/// * [ShimmerDirection.ttb] 从上到下
/// * [ShimmerDirection.btt] 从下到上
enum ShimmerDirection { ltr, rtl, ttb, btt }
Shimmer 组件:对外暴露的接口,渲染闪烁效果
/// 渲染闪烁效果的组件,覆盖在[child]组件树上。
///
/// [child] 定义闪烁效果融合的区域。可以从任何您喜欢的[Widget]构建[child],
/// 但为了获得精确的期望效果和更好的渲染性能,有一些注意事项:
///
/// * 使用静态的[Widget](即[StatelessWidget]的实例)。
/// * [Widget]应该是单色元素。您在这些[Widget]上设置的所有颜色都将被[gradient]的颜色覆盖。
/// * 闪烁效果仅影响[child]的不透明区域,透明区域仍然保持透明。
///
/// [period] 控制闪烁效果的速度。默认值为1500毫秒。
///
/// [direction] 控制闪烁效果的方向。默认值为[ShimmerDirection.ltr]。
///
/// [gradient] 控制闪烁效果的颜色。
///
/// [loop] 动画循环的次数,将值设置为`0`以使动画无限循环。
///
/// [enabled] 控制是否激活闪烁效果。当设置为false时,动画暂停。
///
///
/// ## 专业提示:
///
/// * [child]应由基本和简单的[Widget]构成,例如[Container]、[Row]和[Column],以避免副作用。
///
/// * 使用一个[Shimmer]来包装[Widget]列表,而不是多个[Shimmer]。
///
class Shimmer extends StatefulWidget {
final Widget child;
final Duration period;
final ShimmerDirection direction;
final Gradient gradient;
final int loop;
final bool enabled;
const Shimmer({
super.key,
required this.child,
required this.gradient,
this.direction = ShimmerDirection.ltr,
this.period = const Duration(milliseconds: 1500),
this.loop = 0,
this.enabled = true,
});
/// 一个便捷的构造函数,提供了一种简单方便的方法来创建一个[Shimmer],
/// 其[gradient]是由`baseColor`和`highlightColor`组成的[LinearGradient]。
Shimmer.fromColors({
super.key,
required this.child,
required Color baseColor,
required Color highlightColor,
this.period = const Duration(milliseconds: 1500),
this.direction = ShimmerDirection.ltr,
this.loop = 0,
this.enabled = true,
}) : gradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.centerRight,
colors: <Color>[
baseColor,
baseColor,
highlightColor,
baseColor,
baseColor
],
stops: const <double>[
0.0,
0.35,
0.5,
0.65,
1.0
]);
_ShimmerState createState() => _ShimmerState();
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Gradient>('gradient', gradient,
defaultValue: null));
properties.add(EnumProperty<ShimmerDirection>('direction', direction));
properties.add(
DiagnosticsProperty<Duration>('period', period, defaultValue: null));
properties
.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
properties.add(DiagnosticsProperty<int>('loop', loop, defaultValue: 0));
}
}
Shimmer的状态类_ShimmerState :用于控制动画的播放和停止
/// Shimmer的状态类,用于控制动画的播放和停止
class _ShimmerState extends State<Shimmer> with SingleTickerProviderStateMixin {
// AnimationController用于控制动画
late AnimationController _controller;
// 记录动画播放的次数
int _count = 0;
void initState() {
super.initState();
// 初始化AnimationController,设置vsync和动画持续时间
_controller = AnimationController(vsync: this, duration: widget.period)
// 添加状态监听器,当动画完成时,根据loop的值决定是否重复播放动画
..addStatusListener((AnimationStatus status) {
if (status != AnimationStatus.completed) {
return;
}
_count++;
if (widget.loop <= 0) {
_controller.repeat();
} else if (_count < widget.loop) {
_controller.forward(from: 0.0);
}
});
// 如果Shimmer启用,则开始播放动画
if (widget.enabled) {
_controller.forward();
}
}
void didUpdateWidget(Shimmer oldWidget) {
// 当Shimmer的状态更新时,根据enabled的值决定是否播放动画
if (widget.enabled) {
_controller.forward();
} else {
_controller.stop();
}
super.didUpdateWidget(oldWidget);
}
Widget build(BuildContext context) {
// 使用AnimatedBuilder来创建动画效果
return AnimatedBuilder(
animation: _controller,
child: widget.child,
builder: (BuildContext context, Widget? child) => _Shimmer(
child: child,
direction: widget.direction,
gradient: widget.gradient,
percent: _controller.value,
),
);
}
void dispose() {
// 当Shimmer被销毁时,需要清理AnimationController资源
_controller.dispose();
super.dispose();
}
}
私有 _Shimmer 组件:用于实现Shimmer的渲染效果
/// 一个私有的组件,用于实现Shimmer的渲染效果
class _Shimmer extends SingleChildRenderObjectWidget {
// 闪烁效果的进度,范围为0.0到1.0
final double percent;
// 闪烁效果的方向
final ShimmerDirection direction;
// 闪烁效果的颜色渐变
final Gradient gradient;
// 构造函数,接受child、percent、direction和gradient作为参数
const _Shimmer({
Widget? child,
required this.percent,
required this.direction,
required this.gradient,
}) : super(child: child);
// 创建一个新的_ShimmerFilter对象,用于渲染Shimmer效果
_ShimmerFilter createRenderObject(BuildContext context) {
return _ShimmerFilter(percent, direction, gradient);
}
// 更新_ShimmerFilter对象的属性
void updateRenderObject(BuildContext context, _ShimmerFilter shimmer) {
shimmer.percent = percent;
shimmer.gradient = gradient;
shimmer.direction = direction;
}
}
_ShimmerFilter私有的渲染对象:用于实现Shimmer的渲染效果
/// 一个私有的渲染对象,用于实现Shimmer的渲染效果
class _ShimmerFilter extends RenderProxyBox {
// 闪烁效果的方向
ShimmerDirection _direction;
// 闪烁效果的颜色渐变
Gradient _gradient;
// 闪烁效果的进度,范围为0.0到1.0
double _percent;
// 构造函数,接受percent、direction和gradient作为参数
_ShimmerFilter(this._percent, this._direction, this._gradient);
// 获取当前的ShaderMaskLayer
ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?;
// 如果child不为空,那么需要进行合成
bool get alwaysNeedsCompositing => child != null;
// 设置闪烁效果的进度,如果新值和旧值不同,那么需要重新绘制
set percent(double newValue) {
if (newValue == _percent) {
return;
}
_percent = newValue;
markNeedsPaint();
}
// 设置闪烁效果的颜色渐变,如果新值和旧值不同,那么需要重新绘制
set gradient(Gradient newValue) {
if (newValue == _gradient) {
return;
}
_gradient = newValue;
markNeedsPaint();
}
// 设置闪烁效果的方向,如果新值和旧值不同,那么需要重新布局
set direction(ShimmerDirection newDirection) {
if (newDirection == _direction) {
return;
}
_direction = newDirection;
markNeedsLayout();
}
// 绘制方法,根据方向和进度来绘制闪烁效果
void paint(PaintingContext context, Offset offset) {
if (child != null) {
assert(needsCompositing);
final double width = child!.size.width;
final double height = child!.size.height;
Rect rect;
double dx, dy;
if (_direction == ShimmerDirection.rtl) {
dx = _offset(width, -width, _percent);
dy = 0.0;
rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
} else if (_direction == ShimmerDirection.ttb) {
dx = 0.0;
dy = _offset(-height, height, _percent);
rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
} else if (_direction == ShimmerDirection.btt) {
dx = 0.0;
dy = _offset(height, -height, _percent);
rect = Rect.fromLTWH(dx, dy - height, width, 3 * height);
} else {
dx = _offset(-width, width, _percent);
dy = 0.0;
rect = Rect.fromLTWH(dx - width, dy, 3 * width, height);
}
layer ??= ShaderMaskLayer();
layer!
..shader = _gradient.createShader(rect)
..maskRect = offset & size
..blendMode = BlendMode.srcIn;
context.pushLayer(layer!, super.paint, offset);
} else {
layer = null;
}
}
// 计算偏移量的方法,根据起始位置、结束位置和进度来计算
double _offset(double start, double end, double percent) {
return start + (end - start) * percent;
}
}
在这个类中,_ShimmerFilter 是一个渲染对象,它继承自 RenderProxyBox,用于实现Shimmer 的渲染效果。paint
方法是绘制方法,根据方向和进度来绘制渲染效果。paint
方法根据方向和进度来绘制闪烁效果。
首先,根据 _direction
的值来计算 dx
和 dy
,然后创建一个 Rect 对象。接着,创建或获取一个 ShaderMaskLayer,并设置其 shader
、maskRect
和 blendMode
属性。最后,使用context.pushLayer
方法将这个层添加到渲染树中。
_offset
方法用于计算偏移量,它接受起始位置、结束位置和进度作为参数,然后根据这些参数来计算偏移量。
percent、gradient和direction 是属性的 setter 方法,当这些属性的值发生变化时,会调用markNeedsPaint
或 markNeedsLayout
方法来标记需要重新绘制或重新布局。
skeletonizer 模块骚味复杂一些。这里我仅仅看了 Skeletonizer类 以及部分相关的类。
/// Skeletonizer组件,用于绘制子组件的骨架
///
/// 如果[enabled]设置为false,则子组件将正常绘制
abstract class Skeletonizer extends StatefulWidget {
/// 需要绘制骨架的子组件
final Widget child;
/// 是否启用骨架绘制
final bool enabled;
/// 应用于骨架元素的绘制效果
final PaintingEffect? effect;
/// [TextElement]边框半径配置
final TextBoneBorderRadius? textBoneBorderRadius;
/// 是否忽略容器元素,只绘制依赖项
final bool? ignoreContainers;
/// 是否对齐多行文本骨架
final bool? justifyMultiLineText;
/// 容器元素的颜色,包括[Container]、[Card]、[DecoratedBox]等
///
/// 如果为null,则使用实际颜色
final Color? containersColor;
/// 是否忽略指针事件
///
/// 默认为true
final bool ignorePointers;
/// 默认构造函数
const Skeletonizer._({
super.key,
required this.child,
this.enabled = true,
this.effect,
this.textBoneBorderRadius,
this.ignoreContainers,
this.justifyMultiLineText,
this.containersColor,
this.ignorePointers = true,
});
/// 创建一个[Skeletonizer]组件
const factory Skeletonizer({
Key? key,
required Widget child,
bool enabled,
PaintingEffect? effect,
TextBoneBorderRadius? textBoneBorderRadius,
bool? ignoreContainers,
bool? justifyMultiLineText,
Color? containersColor,
bool ignorePointers,
}) = _Skeletonizer;
/// 创建一个可以在[CustomScrollView]中使用的[SliverSkeletonizer]组件
const factory Skeletonizer.sliver({
Key? key,
required Widget child,
bool enabled,
PaintingEffect? effect,
TextBoneBorderRadius? textBoneBorderRadius,
bool? ignoreContainers,
bool? justifyMultiLineText,
Color? containersColor,
bool ignorePointers,
}) = SliverSkeletonizer;
State<Skeletonizer> createState() => SkeletonizerState();
/// 依赖于最近的SkeletonizerScope(如果有的话)
static SkeletonizerScope? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<SkeletonizerScope>();
}
/// 依赖于最近的SkeletonizerScope(如果有的话),否则抛出异常
static SkeletonizerScope of(BuildContext context) {
final scope =
context.dependOnInheritedWidgetOfExactType<SkeletonizerScope>();
assert(() {
if (scope == null) {
throw FlutterError(
'Skeletonizer operation requested with a context that does not include a Skeletonizer.\n'
'The context used to push or pop routes from the Navigator must be that of a '
'widget that is a descendant of a Skeletonizer widget.',
);
}
return true;
}());
return scope!;
}
/// 将构建委托给[SkeletonizerState]
Widget build(BuildContext context, SkeletonizerBuildData data);
}
/// [Skeletonizer]组件的状态
class SkeletonizerState extends State<Skeletonizer>
with TickerProviderStateMixin<Skeletonizer> {
AnimationController? _animationController;
late bool _enabled = widget.enabled;
SkeletonizerConfigData? _config;
double get _animationValue => _animationController?.value ?? 0.0;
PaintingEffect? get _effect => _config?.effect;
Brightness _brightness = Brightness.light;
TextDirection _textDirection = TextDirection.ltr;
void didChangeDependencies() {
super.didChangeDependencies();
_setupEffect();
}
void _setupEffect() {
_brightness = Theme.of(context).brightness;
_textDirection = Directionality.of(context);
final isDarkMode = _brightness == Brightness.dark;
var resolvedConfig = SkeletonizerConfig.maybeOf(context) ??
(isDarkMode
? const SkeletonizerConfigData.dark()
: const SkeletonizerConfigData.light());
resolvedConfig = resolvedConfig.copyWith(
effect: widget.effect,
textBorderRadius: widget.textBoneBorderRadius,
ignoreContainers: widget.ignoreContainers,
justifyMultiLineText: widget.justifyMultiLineText,
containersColor: widget.containersColor,
);
if (resolvedConfig != _config) {
_config = resolvedConfig;
_stopAnimation();
if (widget.enabled) {
_startAnimation();
}
}
}
void _stopAnimation() {
_animationController
?..removeListener(_onShimmerChange)
..stop(canceled: true)
..dispose();
_animationController = null;
}
void _startAnimation() {
assert(_effect != null);
if (_effect!.duration.inMilliseconds != 0) {
_animationController = AnimationController.unbounded(vsync: this)
..addListener(_onShimmerChange)
..repeat(
reverse: _effect!.reverse,
min: _effect!.lowerBound,
max: _effect!.upperBound,
period: _effect!.duration,
);
}
}
void didUpdateWidget(covariant Skeletonizer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.enabled != widget.enabled) {
_enabled = widget.enabled;
if (!_enabled) {
_animationController?.reset();
_animationController?.stop(canceled: true);
} else {
_startAnimation();
}
}
_setupEffect();
}
void dispose() {
_animationController?.removeListener(_onShimmerChange);
_animationController?.dispose();
super.dispose();
}
void _onShimmerChange() {
if (mounted && widget.enabled) {
setState(() {
// 更新骨架绘制。
});
}
}
Widget build(BuildContext context) => widget.build(
context,
SkeletonizerBuildData(
enabled: _enabled,
config: _config!,
brightness: _brightness,
textDirection: _textDirection,
animationValue: _animationValue,
ignorePointers: widget.ignorePointers,
),
);
}
class _Skeletonizer extends Skeletonizer {
// 构造函数,接收一些参数并传递给父类
const _Skeletonizer({
required super.child,
super.key,
super.enabled = true,
super.effect,
super.textBoneBorderRadius,
super.ignoreContainers,
super.justifyMultiLineText,
super.containersColor,
super.ignorePointers,
}) : super._();
// 重写build方法,返回一个SkeletonizerScope组件
// 如果data.enabled为true,即启用骨架绘制,则使用SkeletonizerRenderObjectWidget来绘制骨架
// 否则,直接返回子组件
Widget build(BuildContext context, SkeletonizerBuildData data) {
return SkeletonizerScope(
enabled: data.enabled,
child: data.enabled
? SkeletonizerRenderObjectWidget(data: data, child: child)
: child,
);
}
}
/// 可以在[CustomScrollView]中使用的[Skeletonizer]组件
class SliverSkeletonizer extends Skeletonizer {
/// 创建一个[SliverSkeletonizer]组件
const SliverSkeletonizer({
required super.child,
super.key,
super.enabled = true,
super.effect,
super.textBoneBorderRadius,
super.ignoreContainers,
super.justifyMultiLineText,
super.containersColor,
super.ignorePointers,
}) : super._();
Widget build(BuildContext context, SkeletonizerBuildData data) {
return SkeletonizerScope(
enabled: data.enabled,
child: data.enabled
? SliverSkeletonizerRenderObjectWidget(data: data, child: child)
: child,
);
}
}
/// 传递给[SkeletonizerRenderObjectWidget]的数据
class SkeletonizerBuildData {
/// 默认构造函数
const SkeletonizerBuildData({
required this.enabled,
required this.config,
required this.brightness,
required this.textDirection,
required this.animationValue,
required this.ignorePointers,
});
/// 是否启用骨架绘制
final bool enabled;
/// 骨架绘制的配置
final SkeletonizerConfigData config;
/// 主题的亮度
final Brightness brightness;
/// 主题的文本方向
final TextDirection textDirection;
/// 动画值
final double animationValue;
/// 是否忽略指针事件
///
/// 默认为true
final bool ignorePointers;
bool operator ==(Object other) =>
identical(this, other) ||
other is SkeletonizerBuildData &&
runtimeType == other.runtimeType &&
enabled == other.enabled &&
config == other.config &&
brightness == other.brightness &&
textDirection == other.textDirection &&
animationValue == other.animationValue &&
ignorePointers == other.ignorePointers;
int get hashCode =>
enabled.hashCode ^
config.hashCode ^
brightness.hashCode ^
textDirection.hashCode ^
animationValue.hashCode ^
ignorePointers.hashCode;
}
/// 提供骨架绘制激活信息
/// 给下级组件
class SkeletonizerScope extends InheritedWidget {
/// 默认构造函数
const SkeletonizerScope(
{super.key, required super.child, required this.enabled});
/// 是否启用骨架绘制
final bool enabled;
bool updateShouldNotify(covariant SkeletonizerScope oldWidget) {
return enabled != oldWidget.enabled;
}
}