• 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
    
    • 1

    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...'), 
                );
              },
            ),
          ),
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    这个示例创建了一个,包含一个闪烁的加载屏幕的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"
                            );
                          },
                        ),
            );
          },
        );
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    其效果如下:

    在这里插入图片描述

    可以看到,当我热重载应用后,先进入了页面骨骼阶段。直到 _fetchData (请求加载数据)完成,显示为真实的页面数据。

    3. 基于 skeletonizer 实现骨架化加载

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

    安装 skeletonizer 依赖:

    在你的 Flutter 项目的 pubspec.yaml 文件中,添加 skeletonizer 依赖:

    flutter pub add skeletonizer
    
    • 1

    实战骨架加载界面

    实际上 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,
                        ),
                      ),
                    );
                  },
                );
              }
            },
          ),
        );
      }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    其中,在 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 }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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));
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    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();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    私有 _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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    _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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90

    在这个类中,_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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    /// [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,
            ),
          );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    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,
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    /// 可以在[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,
        );
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    
    /// 传递给[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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    /// 提供骨架绘制激活信息
    /// 给下级组件
    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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    企业微信下班后能收到通知吗?不接收消息怎么设置?
    Python进行excel处理-01
    【数字测图原理与方法】绪论与测量基本知识
    C++ 课本习题(程序设计题)
    LeetCode·139.单词拆分·递归·记忆化搜索·字典树
    Java核心技术卷Ⅰ-第一章Java程序设计概述
    Python 基于 selenium 实现不同商城的商品价格差异分析系统
    手机在网状态-手机在网状态查询-手机在网站状态接口
    CTF-SSH私钥泄露【简单易懂】
    【Verilog基础】关于芯片中信号串扰的理解
  • 原文地址:https://blog.csdn.net/qq_28550263/article/details/134224135