flutter笔记:骨架化加载器

news2024/11/28 6:05:44
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.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 的值来计算 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;

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1187061.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

windows服务器限制特定ip访问指定端口(服务器ip白名单)

有些时候我们需要限制特定的ip白名单来访问服务器的指定端口&#xff0c;来防止dos攻击或其他危险访问&#xff0c;我们可以通过防火墙来实现这一需求&#xff0c;下面一起看一下&#xff1a; 1.首先开启防火墙 ps:开启防火墙可能有些端口会无法访问&#xff0c;比如80、443等…

【JavaEE】HTTPS协议(对称加密、非对称加密、中间人攻击、证书)

一、什么是HTTPS&#xff1f; 1.1 什么是HTTPS&#xff1f; HTTPS 也是一个应用层协议. 是在 HTTP 协议的基础上引入了一个加密层 1.2 为什么引入HTTPS&#xff1f; HTTP 协议内容都是按照文本的方式明文传输的. 这就导致在传输过程中出现一些被篡改的情况 比如臭名昭著的…

Java --- Mybatis的动态sql标签

一、if标签 <select id"queryEmpByCondition" resultType"User">select * from t_user where 11<if test"username ! null and username ! ">and username #{username}</if></select> if&#xff1a;根据标签中的test…

用Powershell实现:删除所有不是与.json文件重名的.jpg文件

# 指定要搜索的目录路径 $directoryPath "C:\path\to\your\directory"# 获取该目录下的所有.jpg和.json文件 $jpgFiles Get-ChildItem -Path $directoryPath -Filter *.jpg $jsonFiles Get-ChildItem -Path $directoryPath -Filter *.json | Select-Object -Expan…

2022最新版-李宏毅机器学习深度学习课程-P34 自注意力机制类别总结

在课程的transformer视频中&#xff0c;李老师详细介绍了部分self-attention内容&#xff0c;但是self-attention其实还有各种各样的变化形式&#xff1a; 一、Self-attention运算存在的问题 在self-attention中&#xff0c;假设输入序列&#xff08;query&#xff09;长度是N…

【论文阅读】PSDF Fusion:用于动态 3D 数据融合和场景重建的概率符号距离函数

【论文阅读】PSDF Fusion&#xff1a;用于动态 3D 数据融合和场景重建的概率符号距离函数 Abstract1 Introduction3 Overview3.1 Hybrid Data Structure3.2 3D Representations3.3 Pipeline 4 PSDF Fusion and Surface Reconstruction4.1 PSDF Fusion4.2 Inlier Ratio Evaluati…

Apache Storm 2.5.0 单机安装与配置

1、下载storm 2.5.0 2、需要安装python3&#xff0c;并且设置python3的环境变量 3、修改storm.yaml配置 storm.zookeeper.servers:- "node4" # - "server2" # # nimbus.seeds: ["host1", "host2", "host3"] # nimbus…

中标麒麟操作系统网络应用服务部署与实现

文章目录 一、麒麟操作系统概述与网络应用服务部署背景1.1 中标麒麟操作系统介绍1.2 网络应用服务部署的意义和背景 二、网络应用服务部署方案设计2.1 方案设计原则与目标2.2 硬件与软件需求分析2.3 网络应用服务拓扑结构设计与配置 三、中标麒麟操作系统的安装与配置3.1 安装准…

CSDN中: Markdown编辑器使用说明

Markdown编辑器使用说明 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一…

Spring-循环依赖简述

什么是循环依赖 // A依赖了B class A {public B b; } ​ // B依赖了A class B {public A a; } ​ // 循环依赖 A a new A(); B b new B(); a.b b; b.a a; 对象之间的相互依赖很正常&#xff0c;但是在Spring中由于对象创建要经过Bean的生命周期&#xff0c;所以就有了循环…

【MySQL数据库】 六

本文主要介绍了数据库原理中数据库索引和事务相关概念. 一.索引 在查询表的时候,最基本的方式就是遍历表,一条一条筛选 . 因此,就可以给这个表建立索引,来提高查找的速度 比如,按照id建立索引 在数据库上额外搞一个空间维护一些id 相关的信息, id:1 表的某个位置 id:2 …

第22章_数据库的设计规范

文章目录 范式的概念三范式范式一范式二范式三 反范式总结 范式的概念 为了建立冗余较小、结构合理的数据库&#xff0c;设计数据库时必须遵循一定的规则。在关系型数据库中这种规则就称为范式。范式是符合某一种设计要求的总结。要想设计一个结构合理的关系型数据库&#xff…

LLM 大模型向量数据库技术架构浅析

▼最近直播超级多&#xff0c;预约保你有收获 近期直播&#xff1a;《LLM 大模型向量数据库技术架构剖析和应用案例实战》 —1— AI 智能时代&#xff0c;开发者需要一个真正的向量数据库吗&#xff1f; 答案很简单&#xff0c;这取决于开发者的应用场景。举个例子&#xff0c;…

【C++】多态(重写)的实现过程及其原理【核心知识点精讲】(22)

前言 大家好吖&#xff0c;欢迎来到 YY 滴C系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 目录 一.基础知识介绍1&#xff09;虚函数&a…

It Is All About Data: A Survey on the Efects of Data on Adversarial Robustness

It Is All About Data: A Survey on the Effects of Data on Adversarial Robustness----《这一切都关乎数据&#xff1a;关于数据对对抗性鲁棒性影响的调查》 摘要 对抗性示例是攻击者故意设计的机器学习模型的输入&#xff0c;目的是迷惑模型&#xff0c;使其犯错误。这些例…

Java对象的深拷贝

什么是深拷贝 在Java中&#xff0c;对象的深拷贝是指创建一个新的对象&#xff0c;并复制原始对象的所有字段和属性&#xff0c;包括嵌套对象。深拷贝确保原始对象和拷贝对象是完全独立的&#xff0c;对其中一个对象的修改不会影响另一个对象。 深拷贝需要注意的点 在Java中…

震裕科技-300953 三季报分析(20231108)

震裕科技-300953 基本情况 公司名称&#xff1a;宁波震裕科技股份有限公司 A股简称&#xff1a;震裕科技 成立日期&#xff1a;1994-10-18 上市日期&#xff1a;2021-03-18 所属行业&#xff1a;专用设备制造业 周期性&#xff1a;0 主营业务&#xff1a;精密级进冲压模具及下游…

【hcie-cloud】【5】华为云Stack规划设计之华为云Stack标准化配置、缩略语【下】

文章目录 前言、华为云Stack交付综述为云Stack标准组网华为云Stack标准化配置华为云Stack配置概览华为云Stack云服务全视图华为云Stack部署方案节点类型说明华为云Stack云服务组件部署场景管理节点部署原则云平台管理规格华为云Stack IaaS场景&高阶场景起步必选部署组件x86…

Python高级语法----Python多线程与多进程

文章目录 多线程多进程注意事项多线程与多进程是提高程序性能的两种常见方法。在深入代码之前,让我们先用一个简单的比喻来理解它们。 想象你在一家餐厅里工作。如果你是一个服务员,同时负责多个桌子的顾客,这就类似于“多线程”——同一个人(程序)同时进行多项任务(线程…

RflySim | 滤波器设计实验二

本讲是关于无人机滤波器&#xff0c;其中包括无人机滤波器简介、测量原理、线性互补滤波器设计、线性互补滤波器参数分析、卡尔曼滤波器设计等。 滤波器设计实验2 卡尔曼滤波器是一种递推线性最小方差估计算法&#xff0c;它的最优估计需满足以下三个条件&#xff1a; 1&#…