flutter笔记:骨架化加载器

flutter笔记 骨架化加载器



【介绍】:本文介绍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 项中运行以下命令:

csharp 复制代码
flutter pub add shimmer

2.2 使用 Shimmer.fromColors 创建闪烁页面

使用 Shimmer.fromColors 来包装你的加载内容。

scala 复制代码
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 的部分保持不变)

less 复制代码
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 依赖:

csharp 复制代码
flutter pub add skeletonizer

实战骨架加载界面

实际上 skeletonizer 库的官方示例中,是使用一个按钮手动切换数据加载后的。不过为了模拟实际情况,我还是使用了一个_futureData 函数模拟异步数据请求,实际上是延时2秒。在页面初始化状态时执行这个异步操作,模拟完成后使用真实数据。代码如下:

php 复制代码
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 接受两个主要的参数: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 枚举

dart 复制代码
/// 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 组件:对外暴露的接口,渲染闪烁效果

dart 复制代码
/// 渲染闪烁效果的组件,覆盖在[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 :用于控制动画的播放和停止

scala 复制代码
/// 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的渲染效果

scala 复制代码
/// 一个私有的组件,用于实现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的渲染效果

ini 复制代码
/// 一个私有的渲染对象,用于实现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 的值来计算 dxdy,然后创建一个 Rect 对象。接着,创建或获取一个 ShaderMaskLayer ,并设置其 shadermaskRectblendMode 属性。最后,使用context.pushLayer 方法将这个层添加到渲染树中。

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

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

F2. skeletonizer 库部分源码分析

skeletonizer 模块骚味复杂一些。这里我仅仅看了 Skeletonizer类 以及部分相关的类。

dart 复制代码
/// 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);
}
scss 复制代码
/// [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,
        ),
      );
}
scala 复制代码
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,
    );
  }
}
scala 复制代码
/// 可以在[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,
    );
  }
}
dart 复制代码
/// 传递给[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;
}
scala 复制代码
/// 提供骨架绘制激活信息
/// 给下级组件
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;
  }
}
相关推荐
FinelyYang5 小时前
UniApp + LiveKit:Android 端为什么要用原生做屏幕共享,以及我们怎么落地的
android·uni-app
蓝速科技5 小时前
蓝速科技信创落地:平衡安全合规与项目成本的实战方案
android·运维·人工智能·科技·安全·电脑·鸿蒙
又见情义8 小时前
RK3568 Android 13 SDK 开机自动开启共享热点实践
android
mmsx9 小时前
return false 不是拦截:一次按键触发两个动作的事件冒泡陷阱
android·bug
DU_YULIN9 小时前
Camera HAL ThreadManager概述
android·camera hal
淡淡的香烟10 小时前
Android中的MQTT通信原理解析
android·物联网
淡淡的香烟10 小时前
Android Camera开发详解
android·音视频
mmsx11 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp
xcyxiner12 小时前
flutter wsl2安装记录(使用宿主机虚拟机)
前端·flutter
YF021112 小时前
如何进入Android工程模式
android