flutter笔记骨架化加载器
1. 骨架化加载简介
在 Flutter 中,实现 UI骨架加载(Skeleton UI)可以通过使用一些内置的组件和库来创建简化的占位符用户界面。这有助于增强用户体验,因为用户可以立即看到页面正在加载,并且不会感到等待时间过长。
在 Flutter 中,你可以直接使用第三方库 shimmer
或者 skeletonizer
来实现 UI骨架加载 (Skeleton UI)。这两个库可以帮助你创建 占位符用户界面 ,以改善用户体验,尤其是在数据加载时。下面我将分别讲解如何使用这两个库来实现骨架加载。
2. 基于 shimmer 实现骨架化加载
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}); @override Widget build(BuildContext context) { return const MaterialApp( title: 'Shimmer', home: SkeletonLoadingScreen(), SkeletonLoadingScreen ); } } class SkeletonLoadingScreen extends StatelessWidget { const SkeletonLoadingScreen({super.key}); @override 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个字符串的列表 } @override 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
(请求加载数据)完成,显示为真实的页面数据。
3. 基于 skeletonizer 实现骨架化加载
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}); @override 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}); @override State<SkeletonizerDemoPage> createState() => _SkeletonizerDemoPageState(); } class _SkeletonizerDemoPageState extends State<SkeletonizerDemoPage> { late Future<List<String>> _futureData; @override 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'); } @override 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]
提供。
这段示例代码的运行效果如下:
F. 附录
F1. shimmer 库源码分析
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]。 /// @immutable 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 ]); @override _ShimmerState createState() => _ShimmerState(); @override 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; @override 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(); } } @override void didUpdateWidget(Shimmer oldWidget) { // 当Shimmer的状态更新时,根据enabled的值决定是否播放动画 if (widget.enabled) { _controller.forward(); } else { _controller.stop(); } super.didUpdateWidget(oldWidget); } @override 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, ), ); } @override void dispose() { // 当Shimmer被销毁时,需要清理AnimationController资源 _controller.dispose(); super.dispose(); } }
私有 _Shimmer 组件:用于实现Shimmer的渲染效果
/// 一个私有的组件,用于实现Shimmer的渲染效果 @immutable 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效果 @override _ShimmerFilter createRenderObject(BuildContext context) { return _ShimmerFilter(percent, direction, gradient); } // 更新_ShimmerFilter对象的属性 @override 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 @override ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?; // 如果child不为空,那么需要进行合成 @override 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(); } // 绘制方法,根据方向和进度来绘制闪烁效果 @override 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
方法来标记需要重新绘制或重新布局。
F2. skeletonizer 库部分源码分析
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; @override 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; @override 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, ); } } @override 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(); } @override void dispose() { _animationController?.removeListener(_onShimmerChange); _animationController?.dispose(); super.dispose(); } void _onShimmerChange() { if (mounted && widget.enabled) { setState(() { // 更新骨架绘制。 }); } } @override 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来绘制骨架 // 否则,直接返回子组件 @override 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._(); @override 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; @override 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; @override 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; @override bool updateShouldNotify(covariant SkeletonizerScope oldWidget) { return enabled != oldWidget.enabled; } }