Flutter 小技巧之有趣的 UI 骨架屏框架 skeletonizer

很久没有更新过小技巧系列,今天简单介绍一个非常好用的骨架屏框架 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.mfbz.cn/a/2818.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ面试精讲 Day 26:RabbitMQ监控体系建设

【RabbitMQ面试精讲 Day 26】RabbitMQ监控体系建设 在“RabbitMQ面试精讲”系列的第26天,我们将聚焦于RabbitMQ监控体系建设这一关键运维主题。作为消息中间件的核心组件,RabbitMQ一旦出现消息积压、节点宕机或资源耗尽等问题,将直接影响系统…

强化学习中的重要性采样:跨分布复用样本的核心技术

在强化学习中,智能体需与环境交互采集样本(轨迹、状态 - 动作对)以更新策略。但 “样本分布必须与目标策略分布一致” 的同策略限制,会导致采样效率低下(每次策略更新都需重新采样)。此时,** 重…

SWMM排水管网水力、水质建模及海绵城市与水环境中的应用

一:SWMM软件及水力建模基础 1.1软件模块结构 1.2建模基础数据的分类及获取方法概述 1.3软件基本功能介绍 1.4 SWMM相较于其他商业软件的优缺点二:管网水质建模基础 2.1数据需求分析 各种SWMM对象的数据需求以及含义 2.2基础数据整理 2.3基础数据的输入 各…

MySQL 50 道经典练习题及答案

目录 一、数据表设计与初始化 1. 数据表结构说明 2. 建表语句 3. 插入测试数据 二、练习题及答案 1. 查询 "01" 课程比 "02" 课程成绩高的学生的信息及课程分数 2. 查询同时存在 "01" 课程和 "02" 课程的情况 3. 查询存在 &qu…

MyCAT分库分表

MyCAT分库分表 前言: 很难评价的软件 尝试通过修改配置文件做到分库分表 你会发现一些很离谱的BUG 或者是主从分离的时候 你也会发现 莫名其妙的BUG ‍ 创建基础环境192.168.3.145192.168.3.159192.168.3.163MyCAT MySQLMySQLMySQL --更改root密码alter user rootlo…

C++开发/Qt开发:单例模式介绍与应用

单例模式是软件设计模式中最简单也是最常用的一种创建型设计模式。它的核心目标是确保一个类在整个应用程序生命周期中只有一个实例,并提供一个全局访问点。笔者白话版理解:你创建了一个类,如果你希望这个类对象在工程中应用时只创建一次&…

学习设计模式《二十三》——桥接模式

一、基础概念 桥接模式的本质是【分离抽象和实现】。 桥接模式的定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。 认识桥接模式序号认识桥接模式说明1什么是桥接通俗点说就是在不同的东西之间搭一个桥,让它们能够连接起来&a…

HTML+CSS:浮动详解

在HTMLCSS布局中,浮动(float) 是一种经典的布局技术,用于控制元素在页面中的排列方式。它最初设计用于实现文字环绕图片的效果,后来被广泛用于复杂布局,但随着Flexbox和Grid的兴起,其使用场景有…

PAT 1074 Reversing Linked List

题目的意思给出一个链表,让我们每隔K个进行一次反转,如果不足K个的,就不进行。 对于链表反转的题目,我第一时间想出来的是,原地进行逆置,不断的变化指针,但这样很麻烦,没有想出来&am…

python学习DAY46打卡

DAY 46 通道注意力(SE注意力) 内容: 不同CNN层的特征图:不同通道的特征图什么是注意力:注意力家族,类似于动物园,都是不同的模块,好不好试了才知道。通道注意力:模型的定义和插入的位置通道注意…

猫头虎AI分享|字节开源了一款具备长期记忆能力的多模态智能体:M3-Agent 下载、安装、配置、部署教程

猫头虎AI分享|字节开源了一款具备长期记忆能力的多模态智能体:M3-Agent 大家好,我是猫头虎 🦉🐯,今天给大家带来一个超硬核的开源 AI 项目分享:M3-Agent。这是一款由字节开源的、多模态智能体框…

应用缓存不止是Redis!——亿级流量系统架构设计系列

在当今互联网架构中,缓存技术犹如系统的"加速器",通过将热点数据存储在高速介质中,显著降低数据库负载并提升响应速度。无论是CPU的L1/L2/L3缓存,还是分布式系统中的Redis集群,缓存无处不在。本文将深入探讨…