当前位置: 首页 > news >正文

flutter笔记:骨架化加载器

flutter笔记
骨架化加载器

- 文章信息 - 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. 骨架化加载简介

Flutter 中,实现 UI骨架加载Skeleton UI)可以通过使用一些内置的组件和库来创建简化的占位符用户界面。这有助于增强用户体验,因为用户可以立即看到页面正在加载,并且不会感到等待时间过长。

Flutter 中,你可以直接使用第三方库 shimmer 或者 skeletonizer 来实现 UI骨架加载Skeleton UI)。这两个库可以帮助你创建 占位符用户界面 ,以改善用户体验,尤其是在数据加载时。下面我将分别讲解如何使用这两个库来实现骨架加载。

2. 基于 shimmer 实现骨架化加载

pub.dev 上,一个流行度较高的骨架化加载器为 shimmer。本节介绍一下该骨架化加载器的用法。

2.1 shimmer 的安装

使用 shimmer 库:

  1. 添加 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 (请求加载数据)完成,显示为真实的页面数据。

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});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接受两个主要的参数:futurebuilder

  • future参数接受一个Future对象,这里是_futureData,它是在initState方法中初始化的,用于模拟异步获取数据的过程。

  • builder参数是一个返回组件的函数,它接受两个参数:BuildContext和AsyncSnapshot。BuildContext是当前组件的上下文,AsyncSnapshot包含了future的最新状态和数据。

builder 函数中,首先检查 snapshot 是否有数据。如果 snapshot.hasDatafalse,说明 _futureData (模拟异步请求数据)还没有完成,此时返回一个 Skeletonizer 组件,显示骨架屏。Skeletonizer 组件中的 ListView.builder 用于生成骨架屏的列表项。

如果 snapshot.hasDatatrue,说明 _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]。
///

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.0final 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.0double _percent;// 构造函数,接受percent、direction和gradient作为参数_ShimmerFilter(this._percent, this._direction, this._gradient);// 获取当前的ShaderMaskLayerShaderMaskLayer? 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 的值来计算 dxdy,然后创建一个 Rect 对象。接着,创建或获取一个 ShaderMaskLayer,并设置其 shadermaskRectblendMode 属性。最后,使用context.pushLayer 方法将这个层添加到渲染树中。

_offset 方法用于计算偏移量,它接受起始位置、结束位置和进度作为参数,然后根据这些参数来计算偏移量。

percent、gradient和direction 是属性的 setter 方法,当这些属性的值发生变化时,会调用markNeedsPaintmarkNeedsLayout 方法来标记需要重新绘制或重新布局。

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;/// 是否忽略指针事件////// 默认为truefinal 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;/// 是否忽略指针事件////// 默认为truefinal 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;}
}

相关文章:

flutter笔记:骨架化加载器

flutter笔记 骨架化加载器 - 文章信息 - Author: Jack Lee (jcLee95) Visit me at: https://jclee95.blog.csdn.netEmail: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.csdn.net/qq_28550263/article/details/134224135 【介绍】&#xff1a;本文介…...

关于视频封装格式和视频编码格式的简介

文章目录 简介视频封装格式&#xff08;Video Container Format&#xff09;视频编码格式&#xff08;Video Compression Format&#xff09;两者关系总结webm 格式简介webm视频编码格式webm音频编码格式webm总结 简介 视频封装格式&#xff08;Video Container Format&#x…...

npm发布自己的包

npm发布自己的包 1. 首先在npm官网注册一个自己的账户(有账号的可以直接登录) 注册地址 2. 创建一个自己的项目(如果已有自己的项目, 跳过这一步) npm init -y3. 确认自己的npm下载源, 只能使用npm官方的地址 npm config get registry修改地址源 npm config set registr…...

【漏洞复现】weblogic-10.3.6-‘wls-wsat‘-XMLDecoder反序列化(CVE-2017-10271)

感谢互联网提供分享知识与智慧&#xff0c;在法治的社会里&#xff0c;请遵守有关法律法规 文章目录 1.1、漏洞描述1.2、漏洞等级1.3、影响版本1.4、漏洞复现1、基础环境2、漏洞扫描nacsweblogicScanner3、漏洞验证 说明内容漏洞编号CVE-2017-10271漏洞名称Weblogic < 10.3.…...

CRM中的销售机会管理是什么?三个步骤帮你创建销售渠道

企业销售业务中&#xff0c;有个名词叫做“机会管理”&#xff0c;有效的机会管理可以帮助销售人员准确地抓住潜在客户群体&#xff0c;并将其转化为真正的客户、持续带来收入。CRM客户管理系统也是销售机会管理的一个重要工具&#xff0c;帮助销售人员与正确的人建立起关系&am…...

X(原Twitter)怎么发推文最有效?技巧分享

随着人们对于TikTok和INS 等社交媒体平台的热情不断高涨&#xff0c;可能渐渐忽视了X&#xff08;原Twitter&#xff09;这一不可或缺的海外社交媒体巨头。尽管 X&#xff08;原Twitter&#xff09;并未放弃其以280个字符的推文为核心的社交模式&#xff0c;但是众多流行文化和…...

Ionic 模块组件的理解

1 Ionic4.x 文件分析 1.1 app.module.ts 分析 Ionic 是一个基于 Angular 的移动应用开发框架&#xff0c;能帮助开发者使用 Web 技术&#xff08;HTML5、CSS3、JavaScript&#xff09;创建跨平台的应用程序。在 Ionic 应用程序中&#xff0c;app.module.ts 文件是整个应用程序的…...

sql:1对多获取最新一条数据

假设A表为table_a&#xff0c;B表为table_b&#xff0c;它们之间通过主键ID关联。我们可以利用窗口函数ROW_NUMBER()来获取B表中每条A记录对应的最新一条B记录。以下是SQL语句&#xff1a; SELECT a.*,b.* FROM table_a a LEFT JOIN (SELECT *,ROW_NUMBER() OVER (PARTITION B…...

CDN加速技术:降低企业云服务成本的有效利用

在当今数字化时代&#xff0c;云服务已经成为企业运营的不可或缺的一部分。然而&#xff0c;与此同时&#xff0c;云服务的需求也在不断增长&#xff0c;使企业不得不应对更大的数据传输和负载。这就引出了一个关键问题&#xff1a;如何有效降低企业云服务成本&#xff0c;同时…...

设计模式——享元模式(Flyweight Pattern)+ Spring相关源码

文章目录 一、享元模式定义二、例子2.1 菜鸟教程例子2.1.1 定义被缓存对象2.1.2 定义ShapeFactory 2.2 JDK源码——Integer2.3 JDK源码——DriverManager2.4 Spring源码——HandlerMethodArgumentResolverComposite除此之外BeanFactory获取bean其实也是一种享元模式的应用。 三…...

vue3中el-tree设置默认选中节点和展开节点

1.el-tree设置 node-key&#xff0c;current-node-key&#xff0c;default-expanded-keys&#xff0c;highlight-current&#xff1a; <el-tree ref"taskTree" :data"ptreeData" node-key"key" :current-node-key"currentKey" …...

软件测试需求分析是什么?为什么需要进行测试需求分析?

在软件开发中&#xff0c;软件测试是确保软件质量的重要环节之一。而软件测试需求分析作为软件测试的前置工作&#xff0c;对于保证软件测试的顺利进行具有重要意义。软件测试需求分析是指对软件测试的需求进行细致的分析和规划&#xff0c;以明确测试的目标、任务和范围&#…...

GreenPlum简介

简介 Greenplum是一家总部位于**美国加利福尼亚州&#xff0c;为全球大型企业用户提供新型企业级数据仓库(EDW)、企业级数据云(EDC)和商务智能(BI)提供解决方案和咨询服务的公司&#xff0c;在全球已有&#xff1a;纳斯达克&#xff0c;纽约证券交易所&#xff0c;Skype. FOX&…...

HTML和CSS入门学习

目录 一.HTML 二.CSS 1.CSS作用&#xff1a;美化页面 2.CSS语法 【1】CSS语法规范 【2】如何插入样式表 3.CSS选择器 4.CSS设置样式属性--设置html各种标签的属性 【1】文本属性--设置整段文字的样式 【2】字体属性--设置单个字的样式 【3】链接属性--设置链接的样式…...

轻量封装WebGPU渲染系统示例<17>- 使用GPU Compute之元胞自动机(源码)

注&#xff1a; 此示例通过渲染实体的渲染过程控制来实现。此实现方式繁琐&#xff0c;这里用于说明相关用法。 更简洁的实现请见: 轻量封装WebGPU渲染系统示例&#xff1c;19&#xff1e;- 使用GPU Compute材质多pass元胞自动机(源码)-CSDN博客 当前示例源码github地址: ht…...

fmx windows 下 制作无边框窗口最小化最大化并鼠标可拖移窗口

1,最顶端 放一个rectangle 置顶 ,此区域后面实现鼠标拖动 移动窗口,可在上面放置最大,最小,关闭按钮 2,窗口边框模式 设置 none 3,rectangel mousemove事件 uses Winapi.Windows,Winapi.Messages,FMX.Platform.Winprocedure TfrmMain.Rectangle1MouseMove(Sender: TObje…...

【Python】11 Conda常用命令

Conda简介 Conda是一个开源的软件包管理系统和环境管理器&#xff0c;用于安装和管理不同语言的软件包&#xff0c;如Python、R等。它可以创建独立的环境&#xff0c;每个环境都可以安装特定版本的软件包和依赖项&#xff0c;而不必担心与其他环境冲突。Conda还可以轻松地在不…...

5G边缘计算网关 是什么?

5G边缘计算网关&#xff1a;智能设备的云端控制与数据采集 随着物联网技术的不断发展&#xff0c;5G边缘计算网关正在成为智能设备领域的一种重要技术。这种智能网关具备强大的功能&#xff0c;可以完成本地计算、消息通信、数据缓存等任务&#xff0c;同时支持云端远程配置和…...

mac电脑系统清理软件CleanMyMac X2024破解版下载

基本上&#xff0c;不管是win版还是Mac版的电脑&#xff0c;其装机必备就是一款电脑系统清理软件&#xff0c;就比如Mac&#xff0c;目前在市面上&#xff0c;电脑系统清理软件是非常多的。 对于不熟悉系统的用户来说&#xff0c;使用一些小众工具&#xff0c;往往很多用户都不…...

19 款Agent产品工具合集

原文&#xff1a;19 款Agent产品工具合集 什么是Agent? 你告诉GPT完成一项任务&#xff0c;它就会完成一项任务。 如果你不想为GPT提出所有任务怎么办&#xff1f;如果你想让GPT自己思考怎么办&#xff1f; 想象一下&#xff0c;你创建了一个AI&#xff0c;你可以给它一个…...

[尚硅谷React笔记]——第8章 扩展

目录&#xff1a; 扩展1_setState扩展2_lazyLoad扩展3_stateHook扩展4_EffectHook扩展5_RefHook扩展6_Fragment扩展7_Context扩展8_PureComponent扩展9_renderProps扩展10_ErrorBoundary组件通信方式总结 1.扩展1_setState setState更新状态的2种写法 setState(stateChange…...

卷积神经网络中 6 种经典卷积操作

深度学习的模型大致可以分为两类&#xff0c;一类是卷积神经网络&#xff0c;另外一类循环神经网络&#xff0c;在计算机视觉领域应用最多的就是卷积神经网络&#xff08;CNN&#xff09;。CNN在图像分类、对象检测、语义分割等经典的视觉任务中表现出色&#xff0c;因此也早就…...

下拉列表框Spinner

在XML文件中的创建 <Spinnerandroid:id"id/spinner"android:layout_width"wrap_content"android:layout_height"wrap_content"/> 在Java文件中的设置 //获取Spinner对象 Spinner spinnerfindViewById(R.id.spinner); //创建数组…...

C++高级功能笔记

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 例如&#xff1a;…...

PTE SST和RL模板

目录 事实证明&#xff0c;SST分值占比很小&#xff0c;不是很需要好好练 SST的模板&#xff1a; RL模板&#xff1a; 给你一个模版供参考&#xff1a; RA技巧 为什么说日本人团结 This lecture mainly talked about the importance of words and the sound of words and…...

2023年03月 Python(三级)真题解析#中国电子学会#全国青少年软件编程等级考试

Python等级考试(1~6级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 十进制数111转换成二进制数是?( ) A: 111 B: 1111011 C: 101111 D: 1101111 答案:D 十进制转二进制,采用除二倒取余数,直到商为0为止。 第2题 某班有36人,王老师想给每位…...

Mysql数据库 10.SQL语言 储存过程 中 流程控制

存储过程中的流程控制 在存储过程中支持流程控制语句用于实现逻辑的控制 一、分支语句 语法&#xff1a;if-then-else 1.单分支语句 语法 if conditions then ——SQL end if; if conditions then——SQLend if; ——如果参数a的值为1&#xff0c;则添加一条班级信息 …...

测试用例的设计方法(全):错误推测方法及因果图方法

目录 错误推测方法 一. 方法简介 因果图方法 一. 方法简介 二. 实战演习 错误推测方法 一. 方法简介 1. 定义&#xff1a;基于经验和直觉推测程序中所有可能存在的各种错误, 从而有针对性的设计测试用例的方法。 2. 错误推测方法的基本思想&#xff1a; 列举出程序中…...

折叠旗舰新战局:华为先行,OPPO接棒

乌云中的曙光&#xff0c;总能带给人希望。 全球智能手机出货量已经连续八个季度下滑&#xff0c;行业里的乌云挥之不散。不过&#xff0c;也能看到高端市场逆势上涨&#xff0c;散发光亮。个中逻辑在于&#xff0c;当前换机周期已经达到了34个月&#xff0c;只有创新产品才能…...

ESP使用webserver实现本地控制

因为使用云服务有时候不可靠&#xff0c;那么离线控制就很重要。本文使用webserver实现本地网页控制。这样不需要再单独开发APP&#xff0c;有浏览器就可以控制。本文所有测试是靠ESP32。8266未测试。使用USE_8266控制。 核心代码如下&#xff1a; html.h #pragma onceconst…...