Shaders 是我们知道存在却不常亲手用的东西。但它们恰恰是让界面“活起来”的秘密武器:流动的背景、玻璃质感的表面、像素级的失真,还有仿佛在呼吸的动画。为了便于照搬落地,我给出一个可直接复制的 Flutter 屏幕示例,改一改就能用在你的项目里。这样你不是在“看” Shaders,而是在真正“用”它。你会发现,少量代码就能解锁由 GPU 驱动的视觉效果,对“普通 UI”的认知也会被刷新。
快速概览:本文将涵盖的内容
- FragmentProgram是什么以及何时使用它。
- 编写一个小型的片段着色器(GLSL)及其存放位置。
- 使用 Dart 中的FragmentProgram和FragmentShader进行加载与使用。
- 经济高效地更新Uniforms并重用着色器对象。
- 调试、常见陷阱、以及 CI/资源管理方面的建议。
- 带截图的最终示例:如何实现它?
- 安全发布的核对清单。
为什么要使用 Shaders?(简短回答)
把像素处理交给 GPU,会直接带来这些好处:
- 低成本地运行各种逐像素效果(如模糊、扭曲、光照)。
- 生成难以通过基于组件(Widget-based)绘图实现的流畅 60/120fps 视觉效果。
- 将视觉逻辑集中在一个单独的着色器中,让 GPU 可以进行大规模并行执行。
在实际项目里,我用小型 Shaders 替换了不少依赖 CPU 的动画和开销较大的Canvas循环:帧率更稳更顺,同时减轻了 CPU 争用,尤其对中端设备很关键。
核心心智模型:FragmentProgram → FragmentShader → Paint.shader
理解这三个关键概念是使用 Shaders 的基础:
- FragmentProgram:你加载的已编译着色器资源(Asset)。可以将其理解为着色器的二进制文件。
- FragmentShader:程序的一个配置实例,携带着它的 Uniforms(即每次绘制时传入的参数)。你可以从一个
FragmentProgram创建出多个FragmentShader实例。 - Paint.shader:
FragmentShader是通过Paint.shader在绘制画布时使用的(也可以通过ShaderMask、CustomPainter等使用)。
总结:只需加载一次Program,然后重复使用它,并在每帧更新Shader 实例上的 Uniforms。
1)编写一个微小的着色器 (GLSL) —shaders/wave.frag
首先,创建一个着色器文件。一个使用 Flutter 运行时辅助函数的最小化示例如下:
c
体验AI代码助手
代码解读
复制代码
// shaders/wave.frag // 引入坐标映射的辅助函数(可选) #include "flutter/runtime_effect.glsl"; uniform float u_width; uniform float u_height; uniform float u_time; // 秒 half4 main(vec2 fragCoord) { vec2 uv = fragCoord / vec2(u_width, u_height); float wave = 0.5 + 0.5 * sin(uv.x * 12.0 + u_time * 2.0); vec3 base = vec3(0.12, 0.6, 0.9); vec3 color = mix(base * 0.8, base, wave); return half4(color, 1.0); }
📝 Shaders 使用要点和配置说明
📤 Uniforms(着色器参数)
- 着色器会接收到一些Uniforms参数 (
u_width,u_height,u_time),这些参数将由Dart 代码设置和传入。
📦 GLSL 导入与工具链
- 根据你使用的 Flutter 工具链,可能需要添加
#include "flutter/runtime_effect.glsl"来引入坐标辅助函数。 - (关于确切的引用路径,请查阅官方文档或示例。)
📂 资源配置的关键区别
- 务必将着色器文件添加到你的
pubspec.yaml文件的shaders:下方,而不是assets:下方。
yaml
体验AI代码助手
代码解读
复制代码
shaders: - shaders/wave.frag
🚨 2) 在 Dart 中加载和使用 Shaders
🛣️ 着色器路径的配置(关键注意事项)
将着色器路径放在
pubspec.yaml的shaders:部分,可以确保 Flutter 的构建系统将它们编译成FragmentProgram所期望的格式。忽略这一步是导致“它无法运行”的最常见错误。
🖌️ 易于复制粘贴的CustomPainter示例
以下是一个可以直接复制粘贴使用的CustomPainter示例,演示了如何在 Dart 中加载并使用着色器:
dart
体验AI代码助手
代码解读
复制代码
import 'dart:ui' as ui; import 'package:flutter/material.dart'; class WavePainter extends CustomPainter { final ui.FragmentShader shader; final double time; WavePainter({ required this.shader, required this.time }); @override void paint(Canvas canvas, Size size) { // 按着色器中声明的顺序设置 uniforms(采样器跳过) shader.setFloat(0, size.width); // u_width 宽度 shader.setFloat(1, size.height); // u_height 高度 shader.setFloat(2, time); // u_time 时间 final paint = Paint()..shader = shader; canvas.drawRect(Offset.zero & size, paint); } @override bool shouldRepaint(covariant WavePainter old) => time != old.time; }
如何准备和连接着色器:
dart
体验AI代码助手
代码解读
复制代码
// 在你的 StatefulWidget 中的某处 ui.FragmentProgram? _program; ui.FragmentShader? _shader; double _time = 0.0; late final Ticker _ticker; @override void initState() { super.initState(); _loadShader(); _ticker = Ticker((elapsed) { setState(() => _time = elapsed.inMilliseconds / 1000.0); })..start(); } Future<void> _loadShader() async { _program = await ui.FragmentProgram.fromAsset('shaders/wave.frag'); // 创建 FragmentShader 实例——跨帧复用(更新 uniforms 更快) _shader = _program!.fragmentShader(); } @override void dispose() { _ticker.dispose(); super.dispose(); }
🔑 关键要点总结
- FragmentProgram.fromAsset是异步的;只需加载一次(例如,在应用启动或屏幕挂载时)。
- 使用
fragmentShader()创建一个FragmentShader实例。应该重用该实例,并每帧调用setFloat来更新 Uniforms,而不是每帧都重新创建新的 Shader 对象。 setFloat(index, value)根据指定的索引(索引顺序遵循着色器中的 Uniform 声明,跳过 Samplers)设置浮点型 Uniform。
3) Uniforms、Samplers 和纹理
- 浮点型 / 向量 (
vec2/vec3/vec4):通过重复调用setFloat来设置。对于向量类型,需按顺序设置每个分量。 - 图像采样器 (Image samplers):使用
setImageSampler(在FragmentShader上)将纹理绑定到采样器 Uniform——这对于处理捕获的图像或纹理的特效非常有用。尽可能重用纹理以避免内存分配。
4) 着色器的存放位置和构建方式
- 在
pubspec.yaml中使用shaders:标签,确保 Flutter 通过impellerc工具链将其编译成FragmentProgram所需的运行时格式。 - 如果错误地放在
assets:下,加载器可能会失败并给出误导性的错误。务必在本地和持续集成(CI)环境中测试构建。 - 在调试模式下,着色器编辑通常支持热重载(工具链会重新编译),迭代周期很快——但始终要在Profile/Release 版本上进行健全性检查。
5) 性能最佳实践(实用建议)
- 重用对象:重复创建
FragmentProgram和FragmentShader的开销很大;应该重用它们,每帧只更新Uniforms。 - 最小化 Uniform 更新:仅打包每帧会发生变化的数据(例如,时间、触摸坐标)。
- 避免大纹理:大型图像采样器会占用内存和纹理上传时间;尽可能进行降采样。
- 绘制优化:使用
shouldRepaint和状态检查来避免不必要的重绘(这是经典的CustomPainter规范)。 - 测试设备:在中端设备上进行测试——高端硬件上的描述性基准测试可能具有误导性。
- 性能分析:在Profile 模式(而非 Debug 模式)下进行分析,以查看真实的 GPU/CPU 行为。Flutter 文档中指出了不同模式之间的差异。
6) 调试和常见陷阱
“资源不包含有效的着色器数据” (Asset does not contain valid shader data):
- 通常是因为着色器未被包含在
shaders:下或工具链未对其进行编译;检查构建日志和pubspec。(这个错误非常常见且容易令人困惑。)
- 通常是因为着色器未被包含在
Uniform 顺序问题:
setFloat的整数索引取决于着色器中 Uniform 的声明顺序(跳过 Samplers)。如果值看起来不对,请检查你的索引映射。热重载异常:着色器在 Debug 模式下会重新编译,但请务必确认其在 Profile/Release 模式下的行为。
平台差异:GPU 驱动程序和操作系统版本可能会影响着色器能力。测试你所支持的 Android 和 iOS 设备。
7) 可访问性和用户体验 (UX) 考虑
着色器是视觉效果——不要将关键内容隐藏在效果之中:
- 始终为重要信息提供文本等效内容。
- 避免使用纯粹由着色器驱动的颜色对比度来传达状态。
- 如果着色器包含动画,请提供一种让用户减少动态效果的方式(尊重系统“减少动态效果”的偏好设置)。
8) 测试和持续集成 (CI) 提示
- 在 CI 中包含
shaders:路径,并运行一个构建步骤来验证FragmentProgram.fromAsset能否加载每个已编译的着色器(一个小型冒烟测试)。尽早捕获“未编译”的问题。 - 检查大小影响:Shaders 会增加微小的二进制数据块;在 CI 中跟踪应用大小。
- 视觉回归:截取关键帧快照(例如,使用 Golden Tests)以检测视觉效果上的回归。
🖼️ 最终示例
着色器可以实时通过数学方式生成波浪、渐变、扭曲、涟漪和有机运动等效果。
着色器应用区域:
- 在你生成的截图中,最上方的区域——那个色彩鲜艳、波浪起伏、充满流动感的背景(位于“Total Balance”的上方)——正是使用Fragment Shader实现的部分。
中部和底部区域:非着色器实现(刻意为之):
- 中部和底部区域并非基于着色器实现的——这是出于设计目的。
步骤 1:添加着色器文件
- 创建文件:
shaders/wave_header.frag
c
体验AI代码助手
代码解读
复制代码
// shaders/wave_header.frag #include <flutter/runtime_effect.glsl> uniform float u_width; uniform float u_height; uniform float u_time; half4 main(vec2 fragCoord) { vec2 uv = fragCoord / vec2(u_width, u_height); // 基础配色 vec3 c1 = vec3(0.09, 0.15, 0.36); vec3 c2 = vec3(0.26, 0.20, 0.70); vec3 c3 = vec3(0.05, 0.60, 0.85); // 分层波浪 float w1 = sin(uv.x * 6.0 + u_time * 0.9); float w2 = sin(uv.x * 10.0 - u_time * 1.3 + 2.0); float waveMix = (w1 + w2) * 0.25 + uv.y; vec3 color = mix(c2, c3, smoothstep(0.0, 1.0, waveMix)); color = mix(c1, color, 0.8); return half4(color, 1.0); }
2. 在pubspec.yaml中注册着色器
yaml
体验AI代码助手
代码解读
复制代码
flutter: uses-material-design: true shaders: - shaders/wave_header.frag
2. 注册着色器(pubspec.yaml)
注意:该文件不应放在
assets:下——它必须放在shaders:下方。
3. Flutter 屏幕代码 (lib/main.dart)
dart
体验AI代码助手
代码解读
复制代码
import 'dart:ui' as ui; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Fintech Shader Demo', theme: ThemeData(useMaterial3: true), home: const FintechHomeScreen(), ); } } class FintechHomeScreen extends StatefulWidget { const FintechHomeScreen({super.key}); @override State<FintechHomeScreen> createState() => _FintechHomeScreenState(); } class _FintechHomeScreenState extends State<FintechHomeScreen> with SingleTickerProviderStateMixin { ui.FragmentProgram? _program; ui.FragmentShader? _shader; late final AnimationController _controller; @override void initState() { super.initState(); _loadShader(); _controller = AnimationController.unbounded(vsync: this) ..repeat(period: const Duration(seconds: 10)); } Future<void> _loadShader() async { final program = await ui.FragmentProgram.fromAsset('shaders/wave_header.frag'); setState(() { _program = program; _shader = program.fragmentShader(); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( bottomNavigationBar: NavigationBar( selectedIndex: 0, destinations: const [ NavigationDestination(icon: Icon(Icons.home), label: 'Home'), NavigationDestination(icon: Icon(Icons.sync_alt), label: 'Transfer'), NavigationDestination(icon: Icon(Icons.credit_card), label: 'Cards'), NavigationDestination(icon: Icon(Icons.more_horiz), label: 'More'), ], ), body: Column( children: [ SizedBox( height: 260, child: (_program == null || _shader == null) ? const _HeaderFallback() : AnimatedBuilder( animation: _controller, builder: (context, _) { return CustomPaint( painter: _HeaderShaderPainter( shader: _shader!, time: _controller.lastElapsedDuration?.inMilliseconds .toDouble() ?? 0.0, ), child: const _HeaderContent(), ); }, ), ), Expanded( child: ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), children: const [ _AccountsCard(), SizedBox(height: 16), _QuickTransferCard(), ], ), ), ], ), ); } } /// 着色器加载时的简易渐变回退 class _HeaderFallback extends StatelessWidget { const _HeaderFallback(); @override Widget build(BuildContext context) { return Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end:
Alignment.bottomRight, colors: [ Color(0xFF141E30), Color(0xFF243B55), ], ), ), child: const _HeaderContent(), ); } } /// 着色器之上的前景 UI class _HeaderContent extends StatelessWidget { const _HeaderContent(); @override Widget build(BuildContext context) { return SafeArea( bottom: false, child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text('Total Balance', style: Theme.of(context) .textTheme .labelLarge ?.copyWith(color: Colors.white70)), const SizedBox(height: 8), Text('$6,280.50', style: Theme.of(context).textTheme.displaySmall?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, )), ], ), ), ); } } /// 实际绘制着色器的自定义画笔 class _HeaderShaderPainter extends CustomPainter { final ui.FragmentShader shader; final double time; _HeaderShaderPainter({required this.shader, required this.time}); @override void paint(Canvas canvas, Size size) { shader.setFloat(0, size.width); // u_width shader.setFloat(1, size.height); // u_height shader.setFloat(2, time / 1000.0); // u_time(秒) final paint = Paint()..shader = shader; canvas.drawRect(Offset.zero & size, paint); } @override bool shouldRepaint(covariant _HeaderShaderPainter oldDelegate) => oldDelegate.time != time; } // ===== 前景卡片 ===================================================== class _AccountsCard extends StatelessWidget { const _AccountsCard(); @override Widget build(BuildContext context) { return Card( elevation: 2, shape:
https://avg.163.com/topic/detail/8121231
https://avg.163.com/topic/detail/8121250
https://avg.163.com/topic/detail/8103525
https://avg.163.com/topic/detail/8121276
https://avg.163.com/topic/detail/8104256
https://avg.163.com/topic/detail/8121300
https://avg.163.com/topic/detail/8104887
https://avg.163.com/topic/detail/8103532
https://avg.163.com/topic/detail/8105566
https://avg.163.com/topic/detail/8104264
https://avg.163.com/topic/detail/8106223
https://avg.163.com/topic/detail/8121212
https://avg.163.com/topic/detail/8121220
https://avg.163.com/topic/detail/8121235
https://avg.163.com/topic/detail/8103530
https://avg.163.com/topic/detail/8121260
https://avg.163.com/topic/detail/8104258
https://avg.163.com/topic/detail/8121280
https://avg.163.com/topic/detail/8104901
https://avg.163.com/topic/detail/8121239
https://avg.163.com/topic/detail/8105575
https://avg.163.com/topic/detail/8121254
https://avg.163.com/topic/detail/8121210
https://avg.163.com/topic/detail/8121278
https://avg.163.com/topic/detail/8121233
https://avg.163.com/topic/detail/8121301
https://avg.163.com/topic/detail/8121257
https://avg.163.com/topic/detail/8121282
https://avg.163.com/topic/detail/8121294
https://avg.163.com/topic/detail/8103521
https://avg.163.com/topic/detail/8104252
https://avg.163.com/topic/detail/8104897
https://avg.163.com/topic/detail/8105571
https://avg.163.com/topic/detail/8106235
https://avg.163.com/topic/detail/8121219
https://avg.163.com/topic/detail/8121229
https://avg.163.com/topic/detail/8121255
https://avg.163.com/topic/detail/8121277
https://avg.163.com/topic/detail/8121297
https://avg.163.com/topic/detail/8103519
https://avg.163.com/topic/detail/8103515
https://avg.163.com/topic/detail/8104263
https://avg.163.com/topic/detail/8103516
https://avg.163.com/topic/detail/8121230
https://avg.163.com/topic/detail/8104260
https://avg.163.com/topic/detail/8121249
https://avg.163.com/topic/detail/8104882
https://avg.163.com/topic/detail/8121273
https://avg.163.com/topic/detail/8105565
https://avg.163.com/topic/detail/8121291
https://avg.163.com/topic/detail/8106219
https://avg.163.com/topic/detail/8104265
https://avg.163.com/topic/detail/8121218
https://avg.163.com/topic/detail/8104885
https://avg.163.com/topic/detail/8121238
https://avg.163.com/topic/detail/8105564
https://avg.163.com/topic/detail/8121261
https://avg.163.com/topic/detail/8121217
https://avg.163.com/topic/detail/8121274
https://avg.163.com/topic/detail/8121236
https://avg.163.com/topic/detail/8121252
https://avg.163.com/topic/detail/8121281
https://avg.163.com/topic/detail/8121302
https://avg.163.com/topic/detail/8103527
https://avg.163.com/topic/detail/8104249
https://avg.163.com/topic/detail/8121216
https://avg.163.com/topic/detail/8121228
https://avg.163.com/topic/detail/8103523
https://avg.163.com/topic/detail/8121248
https://avg.163.com/topic/detail/8104246
https://avg.163.com/topic/detail/8121271
https://avg.163.com/topic/detail/8104878
https://avg.163.com/topic/detail/8121289
https://avg.163.com/topic/detail/8105570
https://avg.163.com/topic/detail/8103513
https://avg.163.com/topic/detail/8104259
https://avg.163.com/topic/detail/8104876
https://avg.163.com/topic/detail/8103520
https://avg.163.com/topic/detail/8104250
https://avg.163.com/topic/detail/8105561
https://avg.163.com/topic/detail/8106224
https://avg.163.com/topic/detail/8104889
https://avg.163.com/topic/detail/8105574
https://avg.163.com/topic/detail/8106245
https://avg.163.com/topic/detail/8121213
https://avg.163.com/topic/detail/8106227
https://avg.163.com/topic/detail/8121214
https://avg.163.com/topic/detail/8121234
https://avg.163.com/topic/detail/8121258
https://avg.163.com/topic/detail/8121279
https://avg.163.com/topic/detail/8121292
https://avg.163.com/topic/detail/8121232
https://avg.163.com/topic/detail/8121211
https://avg.163.com/topic/detail/8121259
https://avg.163.com/topic/detail/8121237
https://avg.163.com/topic/detail/8121272
https://avg.163.com/topic/detail/8121256
https://avg.163.com/topic/detail/8121299
https://avg.163.com/topic/detail/8121283
https://avg.163.com/topic/detail/8121298
https://avg.163.com/topic/detail/8103510
https://avg.163.com/topic/detail/8104253
https://avg.163.com/topic/detail/8104877
https://avg.163.com/topic/detail/8105562
https://avg.163.com/topic/detail/8106231
https://avg.163.com/topic/detail/8121227
https://avg.163.com/topic/detail/8121251
https://avg.163.com/topic/detail/8121275
https://avg.163.com/topic/detail/8103509
https://avg.163.com/topic/detail/8104251
https://avg.163.com/topic/detail/8104883
https://avg.163.com/topic/detail/8105569
https://avg.163.com/topic/detail/8106243
https://avg.163.com/topic/detail/8121209
https://avg.163.com/topic/detail/8121225
https://avg.163.com/topic/detail/8121247
https://avg.163.com/topic/detail/8121269
https://avg.163.com/topic/detail/8121296
https://avg.163.com/topic/detail/8103506
https://avg.163.com/topic/detail/8104255
https://avg.163.com/topic/detail/8104880
https://avg.163.com/topic/detail/8105559
https://avg.163.com/topic/detail/8106247
https://avg.163.com/topic/detail/8121208
https://avg.163.com/topic/detail/8121226
https://avg.163.com/topic/detail/8121253
https://avg.163.com/topic/detail/8121270
https://avg.163.com/topic/detail/8121290
https://avg.163.com/topic/detail/8121167
https://avg.163.com/topic/detail/8121170
https://avg.163.com/topic/detail/8121173
https://avg.163.com/topic/detail/8121180
https://avg.163.com/topic/detail/8121148
https://avg.163.com/topic/detail/8121153
https://avg.163.com/topic/detail/8121158
https://avg.163.com/topic/detail/8121162
https://avg.163.com/topic/detail/8121130
https://avg.163.com/topic/detail/8121133
https://avg.163.com/topic/detail/8121137
https://avg.163.com/topic/detail/8121139
https://avg.163.com/topic/detail/8121112
https://avg.163.com/topic/detail/8121116
https://avg.163.com/topic/detail/8121120
https://avg.163.com/topic/detail/8121124
https://avg.163.com/topic/detail/8117422
https://avg.163.com/topic/detail/8117429
https://avg.163.com/topic/detail/8117437
https://avg.163.com/topic/detail/8117874
https://avg.163.com/topic/detail/8116330
https://avg.163.com/topic/detail/8116332
https://avg.163.com/topic/detail/8116464
https://avg.163.com/topic/detail/8113729
https://avg.163.com/topic/detail/8113257
https://avg.163.com/topic/detail/8113303
https://avg.163.com/topic/detail/8113328
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Accounts', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 12), _AccountRow(label: 'Checking', last4: '1234', amount: '$2,150.75'), const SizedBox(height: 8), _AccountRow(label: 'Savings', last4: '5678', amount: '$4,129.75'), ], ), ), ); } } class _AccountRow extends StatelessWidget { final String label; final String last4; final String amount; const _AccountRow({ required this.label, required this.last4, required this.amount, }); @override Widget build(BuildContext context) { return Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: Theme.of(context) .textTheme .bodyLarge ?.copyWith(fontWeight: FontWeight.w500)), Text('•••• $last4', style: Theme.of(context).textTheme.bodySmall), ], ), const Spacer(), Text(amount, style: Theme.of(context) .textTheme .bodyLarge ?.copyWith(fontWeight: FontWeight.w600)), ], ); } } class _QuickTransferCard extends StatelessWidget { const _QuickTransferCard(); @override Widget build(BuildContext context) { return Card( elevation: 1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Quick Transfer', style: Theme.of(context) .textTheme .titleMedium ?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 12), Row( children: [ _RoundAction(icon: Icons.north_east, label: 'Send'), const SizedBox(width: 16), _RoundAction(icon: Icons.south_west, label: 'Request'), ], ), ], ), ), ); } } class _RoundAction extends StatelessWidget { final IconData icon; final String label; const _RoundAction({required this.icon, required this.label}); @override Widget build(BuildContext context) { return Column( children: [ Container( width: 52, height: 52, decoration: BoxDecoration( color: Theme.of(context).colorScheme.primary.withOpacity(0.08), shape: BoxShape.circle, ), child: Icon(icon, size: 24, color: Theme.of(context).colorScheme.primary), ), const SizedBox(height: 4), Text(label, style: Theme.of(context).textTheme.bodySmall), ], ); } }
🎯 核心原理(超简述)
- GLSL 着色器:负责绘制动画波浪背景。
- 加载:
FragmentProgram.fromAsset进行加载,fragmentShader()创建一个可重用的着色器实例。 - 数据传递:
_HeaderShaderPainter设置三个 Uniforms:宽度、高度和时间。 - 前景:前景元素(余额文本、卡片、按钮、底部导航)是正常的 Flutter UI。
✅ 着色器发布前的简短核对清单
- 着色器文件声明在
pubspec.yaml的shaders:下。 - 在Profile/Release 版本中构建,并在目标设备上测试。
- 重用
FragmentProgram和FragmentShader;每帧只更新 Uniforms。 - 如果动画是关键部分,添加回退视觉效果或减少动态效果的选项。
- 添加一个 CI 冒烟测试,确保可以加载/实例化每个片段程序。
🚀 实践指南:应该如何做
将着色器写成小型的 GLSL 片段程序,在pubspec.yaml的shaders:下注册它们,然后使用FragmentProgram.fromAsset加载一次,创建FragmentShader实例,接着每帧通过setFloat(以及用于纹理的setImageSampler)来更新 Uniforms。
务必重用着色器对象,在Profile/Release 版本中进行性能分析,并纳入 CI 检查,以避免着色器编译/加载问题在运行时给你带来意外。