很久没有更新过小技巧系列,今天简单介绍一个非常好用的骨架屏框架 skeletonizer ,它主要是通过将你现有的布局自动简化为简单的骨架,并添加动画效果来实现加载过程,而使用成本则是简单的添加一个 Skeletonizer 作为 parent :

Skeletonizer(enabled: _loading,child: ListView.builder(itemCount: 7,itemBuilder: (context, index) {return Card(child: ListTile(title: Text('Item number $index as title'),subtitle: const Text('Subtitle here'),trailing: const Icon(Icons.ac_unit),),);},),
)

=

当然,在实际使用场景中,一般情况在列表返回之前我们是没有数据的,所以可以在加载过程中,通过 skeletonizer 提供的 BoneMock 来组装一个你需要长度的数据列表:

 final fakeUsers = List.filled(7, User(name: BoneMock.name,jobTitle: BoneMock.words(2),email: BoneMock.email,createdAt: BoneMock.date, ),);final users = _loading ? fakeUsers : realUsers;return Skeletonizer(enabled: _loading,child: UserList(users: users),);

那 skeletonizer 是如何做到这个自动转换控件为骨架屏的呢?核心就是在绘制 child 时,通过自定义 context 来替换默认 PaintingContext

在 skeletonizer 内部,它的 RenderSkeletonizer 是一个 RenderProxyBox 实现,作为一个 RenderProxyBox 的子类,它在布局阶段表现得像一个透明代理,但在绘制阶段会接管控制权,决定是绘制真实的子节点还是绘制骨架。

简单来说,skeletonizer 就是通过自定义 PaintingContext 来拦截处理 child 的渲染 ,这里我们先简单看看它的核心代码的作用:

  • render_skeletonizer.dart:

    • 它是 RenderObject 的实现,也就是实际负责渲染的对象, RenderSkeletonizerRenderSliverSkeletonizer 的核心就是 override paint 方法,当 Skeletonizer 被激活时,它们不会像平常一样绘制 child,而是创建一个自定义的 SkeletonizerPaintingContext 来接管绘制工作
  • skeletonizer_painting_context.dart:

    • 骨架屏效果的关键,继承自 PaintingContext,但是提供了一个自定义的 Canvas 对象 SkeletonizerCanvas,这个自定义的 Canvas 会拦截所有来自 child 的绘制,然后用骨架的样式来替代它们
  • uniting_painting_context.dart:

    • 在 paint 里对应 Skeleton.unite 的特殊实现,它提供了一个名为 UnitingCanvas 的特殊 Canvas,当 child 在这个 Canvas 上绘制时,它不会真的去绘制每个元素,而是计算所有绘制操作的区域,并将它们合并成一个大的矩形(unitedRect),最终这个合并后的大矩形会被统一渲染成一个骨架块
  • /effects/\*.dart:

    • 这个目录主要用于定义骨架屏的视觉动画效果,其中 painting_effect.dart 定义了所有效果必须遵守的抽象基类 PaintingEffect,主要是通过构建 Paint 来构建动画,默认的对应实现有:
      • shimmer_effect.dart: 实现了最常见的“微光”或“闪烁”效果,通过一个滑动的 LinearGradient (线性渐变) 来实现
      • pulse_effect.dart: 实现了“脉冲”效果,在两种颜色之间来回渐变
      • sold_color_effect.dart: 纯色效果,没有动画

所以,整个骨架屏的渲染流程如上图所示,可以总结为:

  • 启用 Skeletonizer:

    • Skeletonizer(enabled: true, child: ...) 被构建时,它会启动一个动画控制器(AnimationController),并根据配置选择一个 PaintingEffect (例如 ShimmerEffect)
  • 创建 RenderObject:

    • Skeletonizer 会创建一个 RenderSkeletonizer (或 RenderSliverSkeletonizer) 对象,这个 RenderObject 会将自己标记为 isRepaintBoundary = true,这意味着它会创建一个独立的绘制层 (Layer)
  • 接管绘制上下文:

    • paint 阶段,RenderSkeletonizer 不会像普通 RenderObject 那样直接调用 super.paint 来绘制 child,相反它会创建一个 SkeletonizerPaintingContext 实例,用于拦截绘制
  • 拦截绘制指令:

    • SkeletonizerPaintingContext 内部包含一个 SkeletonizerCanvas,当 Flutter 引擎尝试绘制 child 时(比如 TextContainerIcon 等),所有对 canvas 的操作(如 drawParagraph, drawRect, drawImage)都会被 SkeletonizerCanvas 拦截
  • 替换为骨架样式:

    • SkeletonizerCanvas 会根据拦截到的绘制指令的类型和位置,绘制出相应的骨架形态,并实现一些系列绘制方法,比如:
      • 文本 (drawParagraph): 它会计算出文本的每一行在哪里,然后用一系列矩形来代替真实的文字,矩形的圆角、是否对齐等:
      • 矩形/圆角矩形 (drawRect/drawRRect): 它会检查这个矩形是否被标记为“叶子节点”(比如一个没有子节点的 Container 或被 Skeleton.leaf 包裹的 Widget),如果是,它就会使用从 PaintingEffect (如 ShimmerEffect) 创建的 shaderPaint (带有闪烁效果的画笔) 来填充这个区域,如果不是,它可能会根据配置绘制一个纯色背景,或者干脆忽略它:
      • ······
  • 应用动画效果:

    • 所有用于绘制骨架的 shaderPaint 都来自于当前的 PaintingEffectSkeletonizerAnimationController 会不断更新动画值 (animationValue),PaintingEffect 根据这个值来创建每一帧的 Paint 对象,对于 ShimmerEffect 来说,这就表现为一个不断移动的渐变,从而产生了微光流动的效果:

而在使用使用中,skeletonizer 也提供了丰富的可配置细节,例如:

  • skeleton.dart: 提供了一系列控制场景:

    • Skeleton.ignore: 忽略某个子 Widget,不对其进行骨架化

      Card(child: ListTile(title: Text('The title goes here'),subtitle: Text('Subtitle here'),trailing: Skeleton.ignore( // the icon will not be skeletonizedchild: Icon(Icons.ac_unit, size: 40),),),
      )
      

    • Skeleton.leaf : 容器标记为叶子控件,直接还用 shader paint 绘制

      Skeleton.leaf(child : Card(child: ListTile(title: Text('The title goes here'),subtitle: Text('Subtitle here'),trailing: Icon(Icons.ac_unit, size: 40),),)
      )
      

    • Skeleton.keep: 在骨架化时,保持某个子 Widget 的原始样貌

      Card(child: ListTile(title: Text('The title goes here'),subtitle: Text('Subtitle here'),trailing: Skeleton.keep( // the icon will be painted as ischild: Icon(Icons.ac_unit, size: 40),),),
      )
      

    • Skeleton.replace: 在骨架化时,用一个替代的 Widget (比如一个简单的灰色方块) 来显示,比如遇到需要 Image 空间的场景

          Card(child: ListTile(title: Text('The title goes here'),subtitle: Text('Subtitle here'),trailing: Skeleton.replace( // the icon will be replaced when skeletonizer is enabledwidth: 50, // the width of the replacementheight: 50, // the height of the replacementreplacement: // defaults to a DecoratedBoxchild: Icon(Icons.ac_unit, size: 40),),),);
      

    • Skeleton.unite: 将多个子 Widget 合并成一个大的骨架块

      Card(child: ListTile(title: Text('Item number 1 as title'),subtitle: Text('Subtitle here'),trailing: Skeleton.unite(child: Row(mainAxisSize: MainAxisSize.min,children: [Icon(Icons.ac_unit, size: 32),SizedBox(width: 8),Icon(Icons.access_alarm, size: 32),],),),),
      )
      

    作用场景
    Skeleton.ignore完全跳过骨架化在加载时也需原样显示的 Logo 或品牌元素
    Skeleton.leaf将容器标记为终端骨骼将一个 Card 组件显示为一整个实心骨架块
    Skeleton.keep保持自身,骨架化子孙保持一个带特殊边框的容器,但骨架化其内部的文本和图标
    Skeleton.shade为自定义绘制应用效果骨架化一个使用 CustomPainter 绘制的图表或图形
    Skeleton.replace在骨架化时替换组件处理 Image.network,用一个占位方块替换加载中的网络图片
    Skeleton.unite将多个骨骼合并为一个将一行紧邻的多个 Icon 合并成一个连续的长条形骨架
    Skeleton.ignorePointers禁用指针事件防止用户点击处于加载状态的按钮或列表项
  • bone.dart: 支持通过 Skeletonizer.zone 场景,手动自定义提供了一系列预设的“骨骼”Widget,用于手动搭建骨架屏布局,支持:

    • Bone.text()
    • Bone.multiText()
    • Bone.circle()
    • Bone.square()
    • Bone.icon()
    • Bone.button()
    • Bone.iconButton()
    Skeletonizer.zone(child: Card(child: ListTile(leading: Bone.circle(size: 48),  title: Bone.text(words: 2),subtitle: Bone.text(),trailing: Bone.icon(), ),),);
    

  • effects/*.dart, 主要用于定义了骨架屏的视觉动画效果,其中 painting_effect.dart 定义了抽象基类 PaintingEffect

    • shimmer_effect.dart: 实现了最常见的“微光”或“闪烁”效果,通过一个滑动的 LinearGradient (线性渐变) 来实现

    • pulse_effect.dart: 实现了“脉冲”效果,在两种颜色之间来回渐变

    • sold_color_effect.dart: 纯色效果,没有动画

当然,在一些复杂嵌套场景,或者某些特殊控件,比如 SwitchListTile ,还有比如 RoundedSuperellipseBorder 这样的自定义边框形状 等,框架在便利和处理时会无法处理对应的状态或者复现形状,这也算是它的局限性。

但是瑕不掩瑜,除了需要处理的 fake 数据部分,整体使用还是相当便捷,skeletonizer 的自动化能力可以极大地减少样板代码,并保证 UI 占位的一致性,这也是它值的推荐的原因。

那么,你会在你的应用里使用骨架屏吗?

参考链接

  • https://github.com/Milad-Akarie/skeletonizer

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot的宠物用品系统【2026最新】

作者:计算机学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等,“文末源码”。 专栏推荐:前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏:…

MongoDB 分片集群修改管理员密码

记得关注一下博主,博主每天都会更新IT技术,让你有意想不到的小收获哦^_^ 文章目录*记得关注一下博主,博主每天都会更新IT技术,让你有意想不到的小收获哦^_^*一、注释MongoDB分片集群认证参数(三台主机都要操作&#xf…

C++函数重载与引用详解

一、函数重载:同名函数的 “差异化生存”​1. 概念定义​函数重载(Function Overloading)是 C 的重要特性,指在同一作用域内,允许存在多个同名函数,但要求这些函数的参数列表必须不同。(参数个数…

2025-08-17 李沐深度学习16——目标检测

文章目录1 介绍1.1 实际应用1.2 边界框1.3 数据集2 锚框2.1 什么是锚框2.2 交并比2.3 分配标签2.4 非极大值抑制3 经典目标检测网络3.1 R-CNN3.1.1 R-CNN (原始版本)3.1.2 Fast R-CNN3.1.3 Faster R-CNN3.1.4 Mask R-CNN3.2 单阶段检测器:SSD 和 YOLO3.2.1 SSD (Sin…

Bluedroid vs NimBLE

🔹 对比:Bluedroid vs NimBLE 1. 协议栈体积 & 内存占用 Bluedroid:体积大,RAM 占用也大(几十 KB 到上百 KB)。NimBLE:轻量级,内存占用大概是 Bluedroid 的一半甚至更少。 &…

(纯新手教学)计算机视觉(opencv)实战八——四种边缘检测详解:Sobel、Scharr、Laplacian、Canny

边缘检测详解:Sobel、Scharr、Laplacian、Canny边缘检测是图像处理和计算机视觉中的重要步骤,主要用于发现图像中亮度变化剧烈的区域,即物体的轮廓、边界或纹理特征。OpenCV 提供了多种常用的边缘检测算子,本教程将通过四种方法带…

PyTorch 环境配置

目录一、安装 CUDA二、安装 PyTorch1. 创建虚拟环境2. 安装 PyTorch三、在 PyCharm 上创建一个 PyTorch 项目参考文章: 【2025年最新PyTorch环境配置保姆级教程(附安装包)】 【超详细 CUDA 安装与卸载教程(图文教程)】…

鸿蒙中冷启动分析:Launch分析

启动的分类(热身环节) 启动动类型触发条件系统开销 & 速度主要优化方向冷启动应用进程不存在(首次启动或进程被杀后启动)最高,需创建进程、加载资源、初始化所有组件主要优化目标,减少主线程任务&…

告别盲目排查,PolarDB+DAS Agent智能运维新突破

1.概述 周五下午6点正准备下班,数据库CPU突然爆满,业务告警响成一片,DBA却要手动翻查CPU/内存/负载等多个监控指标,还要查询是否有新增慢SQL,死锁等问题?” 这可能是数据库DBA最闹心的场景了,…

Linux------《零基础到联网:CentOS 7 在 VMware Workstation 中的全流程安装与 NAT 网络配置实战》

(一)Linux的发行版Centos安装与配置 下载Linux发行版本Centos:centos-7-isos-x86_64安装包下载_开源镜像站-阿里云点击CentOS-7-x86_64-DVD-2009.torrent ,CentOS-7-x86_64-DVD-2009.torrent是官方提供的 BT 种子文件(176.1 KB&a…

iOS App 混淆工具实战,教育培训类 App 的安全保护方案

随着在线教育、企业培训、知识付费平台的兴起,越来越多的 iOS 应用需要保护自己的课程资源和核心逻辑。然而,教育类 App 面临的最大风险并非传统的外挂或刷分,而是 视频盗链、题库数据泄露、源码逻辑被二次利用。 在这种场景下,合…

RabbitMQ:SpringAMQP Topic Exchange(主题交换机)

目录一、案例需求二、基础配置三、代码实现TopicExchange与DirectExchange类似,区别在于RoutingKey可以是多个单次的列表,并且以.分割。 Queue与Exchange指定BindingKey时可以使用通配符: #:代指0个或多个单词。*:代…

(纯新手教学)计算机视觉(opencv)实战六——图像形态学(腐蚀、膨胀、开运算、闭运算、梯度、顶帽、黑帽)

图像形态学在图像处理中,形态学(Morphology) 是一种基于图像中物体形状的处理方法,通常用于二值图像和灰度图像。它通过腐蚀、膨胀等基本操作,结合开运算、闭运算、梯度运算、顶帽、黑帽等派生操作,来实现去…

学习嵌入式第三十五天

文章目录网络(续上)1.函数接口2.相关功能实现1.TCP连接2.UDP习题网络(续上) 1.函数接口 sendto 原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, sockle…

为什么给数据表加了索引,写入速度反而变慢了

为数据表增加索引后之所以会导致写入(包括插入、更新、删除)操作的速度变慢,其根本原因在于索引本质上是一个独立的、需要与主表数据保持实时同步的“数据结构”。这一机制的核心逻辑涵盖五个方面:因为索引本质上是一个“独立的数…

.NET Core 中采用独立数据库的SAAS(多租户)方法

介绍多租户是指一种软件架构,其中软件的单个实例在服务器上运行并为多个租户提供服务。在基于 SAAS 的平台中,租户是指使用该平台开展业务运营的客户。每个租户都拥有独立的数据、用户帐户和配置设置,并且与其他租户隔离。多租户允许有效利用…

运维日常工作100条

这是一份非常详细和实用的“运维日常工作100条”清单。它涵盖了从日常巡检、变更管理、故障处理到安全、优化和文档等运维工作的方方面面,可以作为运维工程师的日常工作指南和检查清单。 运维日常工作100条 一、日常巡检与监控 (20条) 检查核心监控大盘:查看整体业务健康状态…

OpenHarmony子系统介绍

OpenHarmony子系统OpenHarmony子系统1. AI业务子系统2. 方舟运行时子系统3. ArkUI框架子系统4. DFX子系统5. DeviceProfile子系统6. XTS子系统7. 上传下载子系统8. 主题框架子系统9. 事件通知子系统10. 位置服务子系统11. 元能力子系统12. 全局资源调度子系统13. 全球化子系统1…

博士招生 | 英国谢菲尔德大学 招收计算机博士

内容源自“图灵学术博研社”gongzhonghao学校简介谢菲尔德大学(The University of Sheffield)是英国久负盛名的公立研究型大学,也是罗素集团成员之一。在 2026 年 QS 世界大学排名中,谢菲尔德大学位列第92位,其中计算机…

如何理解面向过程和面向对象,举例说明一下?

面向过程和面向对象是两种不同的编程思想,核心区别在于解决问题的视角不同:前者关注 “步骤和过程”,后者关注 “对象和交互”。面向过程的核心思想是把问题拆解成一系列步骤,通过函数实现每个步骤,然后按顺序调用这些…