Flutter 小技巧之不一样的思路实现炫酷 3D 翻页折叠动画

news2025/7/20 11:04:30

今天聊一个比较有意思的 Flutter 动画实现,如果需要实现一个如下图的 3D 折叠动画效果,你会选择通过什么方式?

相信可能很多人第一想法就是:在 Dart 里通过矩阵变换配合 Canvas 实现

因为这个效果其实也算「常见」,在目前的小说阅读器场景里,类似的翻页效果基本都是通过这个思路完成,而这个思路以前我也「折腾」过不少,比如 《炫酷的 3D 卡片和帅气的 360° 展示效果》 和 用纯代码实现立体 Dash 和 3D 掘金 Logo ,就是在 Dart 里利用矩阵变换实现的视觉 3D 效果。

但是今天通过一个叫 riveo_page_curl 的项目,提供了不一样的实现方式,那就是通过自定义 Fragment Shaders 实现动画 ,使用自定义 shaders 可以直接使用 GLSL 语言来进行编程,最终达到通过 GPU 渲染出更丰富图形效果。

解释这个项目之前,我们先聊聊 Fragment Shader ,Flutter 在 3.7 开始提供 Fragment Shader API ,顾名思义,它是一个作用于片段的着色器,也就是通过 Fragment Shader API ,开发者可以直接介入到 Flutter 渲染管道的渲染流程中。

那么直接使用 Fragment Shader 而不使用 Dart 矩阵变换的好处是什么?简单来说就是可以减少 CPU 的耗时,直接通过图形语言(GLSL)直接给 GPU 发送指令,从性能上无疑可以得到提升,并且实现会更简洁。

不过加载着色器这个行为的开销可能会比较大,所以必须在运行时将它编译为适当的特定于平台的着色器。

当然,在 Flutter 里使用 Fragment Shader 也是有条件限制的,例如一般都需要引入 #include <flutter/runtime_effect.glsl> 这个头文件,因为在编写着色器代码时,我们都需要知道当前片段的局部坐标的值,而 flutter/runtime_effect.glsl 里就提供了 FlutterFragCoord().xy; 来支持访问局部坐标,而这并不是标准 GLSL 的 API 。

另外, Fragment Shader 只支持 .frag 格式文件, 不支持顶点着色文件 .vert ,同时还有以下限制:

  • 不支持 UBO 和 SSBO
  • sampler2D 是唯一受支持的 sampler 类型
  • texture 仅支持( sampler 和 uv)的两个参数版本
  • 不能声明额外的可变输入
  • 不支持无符号整数和布尔值

所以如果需要搬运一些已有的 GLSL 效果,例如 shadertoy 上的代码时,那么一些必要的「代码改造」还是逃不掉的,例如下方代码是一段渐变动画的着色器:

void mainImage( out vec4 fragColor, in vec2 fragCoord ){
		float strength = 0.4;
    float t = iTime/3.0;
    
    vec3 col = vec3(0);
    vec2 fC = fragCoord;

    for(int i = -1; i <= 1; i++) {
        for(int j = -1; j <= 1; j++) {

            fC = fragCoord+vec2(i,j)/3.0;
            vec2 pos = fC/iResolution.xy;
            pos.y /= iResolution.x/iResolution.y;
            pos = 4.0*(vec2(0.5) - pos);
            for(float k = 1.0; k < 7.0; k+=1.0){ 
                pos.x += strength * sin(2.0*t+k*1.5 * pos.y)+t*0.5;
                pos.y += strength * cos(2.0*t+k*1.5 * pos.x);
            }
            col += 0.5 + 0.5*cos(iTime+pos.xyx+vec3(0,2,4));
        }
    }
    col /= 9.0;
    col = pow(col, vec3(0.4545));
    fragColor = vec4(col,1.0);
}

而在 Flutter 里,就需要转化为如下代码所示:

  • 首先就是必不可少的 flutter/runtime_effect.glsl
  • 其次定义 main() 函数
  • 然后我们需要将 mainImage 里定义的 out vec4 fragColor; 移到全局声明
  • 因为在 GLSL 里 iResolution 用于表示画布像素高宽,iTime 是程序运行的时间,而这里通过 uniform 定义 resolutioniTime 直接用于接受 Dart 端的输入,其余逻辑不变
  • 对应 fragCoord 可以在 Flutter 里通过 FlutterFragCoord 获取坐标
#version 460 core
#include <flutter/runtime_effect.glsl>

out vec4 fragColor;

uniform vec2 resolution;
uniform float iTime;

void main(){
    float strength = 0.25;
    float t = iTime/8.0;
    vec3 col = vec3(0);
    vec2 pos = FlutterFragCoord().xy/resolution.xy;
    pos = 4.0*(vec2(0.5) - pos);
    for(float k = 1.0; k < 7.0; k+=1.0){
        pos.x += strength * sin(2.0*t+k*1.5 * pos.y)+t*0.5;
        pos.y += strength * cos(2.0*t+k*1.5 * pos.x);
    }
    col += 0.5 + 0.5*cos(iTime+pos.xyx+vec3(0,2,4));
    col = pow(col, vec3(0.4545));
    fragColor = vec4(col,1.0);
}

第一行 #version 460 core 指定所用的 OpenGL 语言版本。

可以看到转换一段 GLSL 代码并不特别麻烦,主要是坐标和输入参数的变化,而通过这些已有的片段着色器,却可以给我们提供极其丰富的渲染效果,如下代码所示:

  • pubspec.yaml 里引入上面的 shaders 代码

  • 通过 ShaderBuilder 加载对应 'shaders/warp.frag' 文件,获得 FragmentShader

  • 利用 FragmentShadersetFloat 传递数据

  • 通过 Paint()..shader 添加着色器进行绘制,就可以完成渲染

flutter:
  shaders:
    - shaders/warp.frag

·············
  
  late Ticker _ticker;

  Duration _elapsed = Duration.zero;

  
  void initState() {
    super.initState();
    _ticker = createTicker((elapsed) {
      setState(() {
        _elapsed = elapsed;
      });
    });
    _ticker.start();
  }

  
  Widget build(BuildContext context) => ShaderBuilder(
        assetKey: 'shaders/warp.frag',
        (BuildContext context, FragmentShader shader, _) => Scaffold(
          appBar: AppBar(
            title: const Text('Warp')
          ),
          body: CustomPaint(
            size: MediaQuery.of(context).size,
            painter: ShaderCustomPainter(shader, _elapsed) 
          ),
        ),
      );

class ShaderCustomPainter extends CustomPainter {
  final FragmentShader shader;
  final Duration currentTime;

  ShaderCustomPainter(this.shader, this.currentTime);

  
  void paint(Canvas canvas, Size size) {
    shader.setFloat(0, size.width);
    shader.setFloat(1, size.height);
    shader.setFloat(2, currentTime.inMilliseconds.toDouble() / 1000.0);
    final Paint paint = Paint()..shader = shader;
    canvas.drawRect(Offset.zero & size, paint);
  }

  
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

这里唯一需要解释的就是 shader.setFloat 流程,因为它其实是通过索引来对应到我们在 .frag 文件里的变量,简单来说:

这里我们在 GLSL 里定义了 uniform vec2 resolution;uniform float iTime; ,那么 vec2 resolution 就占据了索引 0 和 1 ,float iTime 就占据了索引 2 。

大概理解就是,vec2 就是两个 float 类型的值保存在了一起的意思,所以先声明的 vec2 resolution 就占据了 索引 0 和 1 ,举个例子,如下图所示,此时的 vec2 和 vec3 分了就占据了 0-4 的索引。

而通过 uniform 在 GLSL 着色器中定义值,然后在 Dart 中就可以通过 setFloat 的索引来传递对应数据过去,从而形成了数据交互的完整闭环。

这里的渐变动画在 Flutter 的完整代码可以参考 Github https://github.com/tbuczkowski/flutter_shaders 里的 warp.frag ,

同时针对前面整个渐变动画,作者在仓库内还提供了对应纯 Dart 代码实现一样效果的对比,通过数据可以看到,利用着色器的实现在性能上得到了巨大的提升。

image-20231031175152699

那么回过头来, riveo_page_curl 的项目里的折叠着色器如下所示,除了一堆不懂的矩阵变化,如 scale 缩放、translate 平移和 project 投影转换之外,就是各种看不明白的三角函数计算,简单的核心就是在矩阵变化时计算弯曲部分的弧度,以及增加阴影投影来提高视觉效果。

#include <flutter/runtime_effect.glsl>

uniform vec2 resolution;
uniform float pointer;
uniform float origin;
uniform vec4 container;
uniform float cornerRadius;
uniform sampler2D image;

const float r = 150.0;
const float scaleFactor = 0.2;

#define PI 3.14159265359
#define TRANSPARENT vec4(0.0, 0.0, 0.0, 0.0)

mat3 translate(vec2 p) {
    return mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, p.x, p.y, 1.0);
}

mat3 scale(vec2 s, vec2 p) {
    return translate(p) * mat3(s.x, 0.0, 0.0, 0.0, s.y, 0.0, 0.0, 0.0, 1.0) * translate(-p);
}

vec2 project(vec2 p, mat3 m) {
    return (inverse(m) * vec3(p, 1.0)).xy;
}

struct Paint {
    vec4 color;
    bool stroke;
    float strokeWidth;
    int blendMode;
};

struct Context {
    vec4 color;
    vec2 p;
    vec2 resolution;
};


bool inRect(vec2 p, vec4 rct) {
    bool inRct = p.x > rct.x && p.x < rct.z && p.y > rct.y && p.y < rct.w;
    if (!inRct) {
        return false;
    }
    // Top left corner
    if (p.x < rct.x + cornerRadius && p.y < rct.y + cornerRadius) {
        return length(p - vec2(rct.x + cornerRadius, rct.y + cornerRadius)) < cornerRadius;
    }
    // Top right corner
    if (p.x > rct.z - cornerRadius && p.y < rct.y + cornerRadius) {
        return length(p - vec2(rct.z - cornerRadius, rct.y + cornerRadius)) < cornerRadius;
    }
    // Bottom left corner
    if (p.x < rct.x + cornerRadius && p.y > rct.w - cornerRadius) {
        return length(p - vec2(rct.x + cornerRadius, rct.w - cornerRadius)) < cornerRadius;
    }
    // Bottom right corner
    if (p.x > rct.z - cornerRadius && p.y > rct.w - cornerRadius) {
        return length(p - vec2(rct.z - cornerRadius, rct.w - cornerRadius)) < cornerRadius;
    }
    return true;
}

out vec4 fragColor;

void main() {
    vec2 xy = FlutterFragCoord().xy;
    vec2 center = resolution * 0.5;
    float dx = origin - pointer;
    float x = container.z - dx;
    float d = xy.x - x;

    if (d > r) {
        fragColor = TRANSPARENT;
        if (inRect(xy, container)) {
            fragColor.a = mix(0.5, 0.0, (d-r)/r);
        }
    }

    else
    if (d > 0.0) {
        float theta = asin(d / r);
        float d1 = theta * r;
        float d2 = (3.14159265 - theta) * r;

        vec2 s = vec2(1.0 + (1.0 - sin(3.14159265/2.0 + theta)) * 0.1);
        mat3 transform = scale(s, center);
        vec2 uv = project(xy, transform);
        vec2 p1 = vec2(x + d1, uv.y);

        s = vec2(1.1 + sin(3.14159265/2.0 + theta) * 0.1);
        transform = scale(s, center);
        uv = project(xy, transform);
        vec2 p2 = vec2(x + d2, uv.y);

        if (inRect(p2, container)) {
            fragColor = texture(image, p2 / resolution);
        } else if (inRect(p1, container)) {
            fragColor = texture(image, p1 / resolution);
            fragColor.rgb *= pow(clamp((r - d) / r, 0.0, 1.0), 0.2);
        } else if (inRect(xy, container)) {
            fragColor = vec4(0.0, 0.0, 0.0, 0.5);
        }
    }
    else {
        vec2 s = vec2(1.2);
        mat3 transform = scale(s, center);
        vec2 uv = project(xy, transform);

        vec2 p = vec2(x + abs(d) + 3.14159265 * r, uv.y);
        if (inRect(p, container)) {
            fragColor = texture(image, p / resolution);
        } else {
            fragColor = texture(image, xy / resolution);
        }
    }

}

其实我知道大家并不关心它的实现逻辑,更多是如何使用,这里有个关键信息就是 uniform sampler2D image ,通过引入 sampler2D ,我们就可以在 Dart 通过 setImageSampler(0, image); ui.Image 传递到 GLSL 里,这样就可以对 Flutter 控件实现上述的折叠动画逻辑。

对应在 Dart 层,就是除了 ShaderBuilder 之外,还可以通过 flutter_shaders 的 AnimatedSampler 来实现更简洁的 shaderimagecanvas 的配合,其中 AnimatedSampler 的最大作用,就是将整个 child 通过 PictureRecorder 进行截图,转化成 ui.Image 传递给 GLSL,完成 UI 传递交互效果。

  Widget _buildAnimatedCard(BuildContext context, Widget? child) {
    return ShaderBuilder(
      (context, shader, _) {
        return AnimatedSampler(
          (image, size, canvas) {
            _configureShader(shader, size, image);
            _drawShaderRect(shader, size, canvas);
          },
          child: Padding(
            padding: EdgeInsets.symmetric(vertical: cornerRadius),
            child: widget.child,
          ),
        );
      },
      assetKey: 'shaders/page_curl.frag',
    );
    
    void _configureShader(FragmentShader shader, Size size, ui.Image image) {
    shader
      ..setFloat(0, size.width) // resolution
      ..setFloat(1, size.height) // resolution
      ..setFloat(2, _animationController.value) // pointer
      ..setFloat(3, 0) // origin
      ..setFloat(4, 0) // inner container
      ..setFloat(5, 0) // inner container
      ..setFloat(6, size.width) // inner container
      ..setFloat(7, size.height) // inner container
      ..setFloat(8, cornerRadius) // cornerRadius
      ..setImageSampler(0, image); // image
  }

  void _drawShaderRect(FragmentShader shader, Size size, Canvas canvas) {
    canvas.drawRect(
      Rect.fromCenter(
        center: Offset(size.width / 2, size.height / 2),
        width: size.width,
        height: size.height,
      ),
      Paint()..shader = shader,
    );
  }
    

完整项目可见:https://github.com/Rahiche/riveo_page_curl

所以可以看到,相比起在 Dart 层实现这样的 3D 翻页折叠,利用 FragmentShader 实现的代码会更简洁,并且性能体验上会更优于纯 Dart 实现,最重要的是,类似 ShaderToy 里的一些着色器代码,通过简单的移植适配,就可以在直接被运用到 Flutter 里,这对于 Flutter 在游戏场景的实现来无疑说非常友好。

最后,Flutter 3.10 之后, Flutter Web 同样支持了 fragment shaders,所以着色器在 Flutter 的实现目前已经相对成熟,那么如果是之前的我通过 Flutter 实现的《霓虹灯文本的「故障」效果的实现》的逻辑转换成 fragment shaders 来完成,是不是性能和代码简洁程度也会更高?

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

el-table样式

1、实现效果&#xff0c;外部框是蓝绿色边框&#xff0c;深色背景&#xff0c;里面的表格首先设置透明色&#xff0c;然后应用自定义斑马纹。 2、代码 template代码&#xff0c;其中样式frameBordStyle是深色背景框&#xff0c;不负责表格样式&#xff0c;表格样式由tableStyl…

RCurl库做爬虫效率怎么样

RCurl库是一个非常强大的网络爬虫工具&#xff0c;它提供了许多功能&#xff0c;例如从服务器下载文件、保持连接、上传文件、采用二进制格式读取、句柄重定向、密码认证等等。使用RCurl库进行网络爬虫可以方便地获取网站上的数据&#xff0c;并进行数据分析和挖掘。在使用RCur…

2023-10-30 LeetCode每日一题(H 指数 II)

2023-10-30每日一题 一、题目编号 275. H 指数 II二、题目链接 点击跳转到题目位置 三、题目描述 给你一个整数数组 citations &#xff0c;其中 citations[i] 表示研究者的第 i 篇论文被引用的次数&#xff0c;citations 已经按照 升序排列 。计算并返回该研究者的 h 指数…

LiveNVR监控流媒体Onvif/RTSP常见问题-概览负载信息具体表示什么实时展示取流中、播放中、录像中等使用数目说明

LiveNVR常见问题-概览负载信息具体表示什么实时展示取流中、播放中、录像中等使用数目说明 1、 Onvif/RTSP等直播流拉转服务2、负载信息2、负载信息说明 1、 Onvif/RTSP等直播流拉转服务 可接入传统监控行业里面的高清网络摄像机的RTSP直播流&#xff0c;及 RTMP、HTTP-FLV、M3…

全新彩虹知识付费商城源码-知识付费小程序V3.4/支持二级分类+多级分销(免授权)

源码简介&#xff1a; 全新彩虹知识付费商城源码&#xff0c;作为知识付费小程序V3.4&#xff0c;它支持二级分类&#xff0c;而且有多级分销&#xff0c;秒杀&#xff0c;砍价&#xff0c;团购的功能。它是免授权的。 彩虹知识付费商城初创体验版是一款功能丰富的电商平台&a…

uni-app项目 前端加前缀(包括页面及静态资源)

具体步骤 参考&#xff1a; https://www.jianshu.com/p/f1fe5daa3a3c manifest.json 文件## 位置如下&#xff1a; "h5" : {"title" : "物业公告","router" : {"mode" : "history", //采用history模式URL的路…

Go Metrics SDK Tag 校验性能优化实践

背景 Metrics SDK 是与字节内场时序数据库 ByteTSD 配套的用户指标打点 SDK&#xff0c;在字节内数十万服务中集成&#xff0c;应用广泛&#xff0c;因此 SDK 的性能优化是个重要和持续性的话题。本文主要以 Go Metrics SDK 为例&#xff0c;讲述对打点 API 的 hot-path 优化的…

VCR库编写爬虫程序

VCR库编写一个爬虫程序来爬取图片&#xff0c;代码如下所示&#xff1a; require vcr require open-uri ​ # 使用VCR库录制一次HTTP请求 VCR.use_cassette(vip_crawler) do# 设置主机和端口proxy_host proxy_port # 创建一个HTT理对象proxy URI.parse("http://#{prox…

免费好用的DNS在线工具,DNS记录、域名被墙、Whois轻松查询

在互联网的世界中&#xff0c;DNS是一种非常重要的存在&#xff0c;它们是域名与IP地址之间的桥梁&#xff0c;用于实现用户访问网站。我们在搭建网站时&#xff0c;经常会用到一些DNS工具&#xff0c;以便了解网站的DSN情况。DNS记录查询工具、域名被墙检测工具以及Whois域名信…

综合场景搭建|一张图展示多源空间数据,搭建丰富场景

「四维轻云」是一款轻量化的地理空间数据管理云平台&#xff0c;支持地理空间数据的在线管理、编辑以及分享。平台提供了项目管理、数据上传、场景搭建、发布分享、素材库等功能模块&#xff0c;支持团队多用户在线协作管理。 四维轻云平台是以项目的形式进行数据管理&#xf…

asp.net企业费用报销管理信息系统VS开发sqlserver数据库web结构c#编程Microsoft Visual Studio

一、源码特点 asp.net 企业费用报销管理信息系统是一套完善的web设计管理系统&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为vs2010&#xff0c;数据库为sqlserver2008&#xff0c;使 用c#语言开发 asp.net企业费用报销系统…

构件基本概念

1.构件的概念 构件是一种组装单元&#xff0c;它可以可以是一个封装的对象&#xff0c;可以是软件包&#xff0c;也可以是一个子系统&#xff0c;甚至是一些文档。它具有规范的接口规约和显式的语境依赖&#xff0c;软件构件可以独立的部署并由第三方任意组装。中间件是构件的…

XUbuntu22.04之解决桌面突然放大,屏幕跟着鼠标移动问题(一百九十)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

centos7安装mysql5.7

工具&#xff1a;VM16 centos7 1.1.Mysql安装 1.1.1 下载wget命令 root权限下进行&#xff1a; yum -y install wget 1.1.2 在线下载mysql安装包 wget https://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm 1.1.3 安装MySQL rpm -ivh mysql57-community-r…

外观数列00

题目链接 外观数列 题目描述 注意点 1 < n < 30countAndSay(n) 是对 countAndSay(n-1) 的描述&#xff0c;然后转换成另一个数字字符串 解答思路 从第一个数1开始不断遍历推出下一个外观数列&#xff0c;下一个外观数列是由当前数列中连续数字的数量该数字组成&…

孩子写作业需要买台灯吗?专家推荐的读写作业台灯分享

我认为孩子写作业的时候还是很有必要备上一盏台灯的。原本现在的孩子学习压力就大&#xff0c;相信大家也不难发现&#xff0c;很多孩子每天放学回家后都要写作业到很晚&#xff0c;再加上已经长时间的学习用眼&#xff0c;如果这个时候光线不适合是很容易加重眼睛的负担的。 而…

Netty通信框架

Netty框架的底层是NIO&#xff0c;NIO&#xff1a;non-blocking io 非阻塞IO 一个线程可以处理多个通道&#xff0c;减少线程创建数量&#xff1b; 读写非阻塞&#xff0c;节约资源&#xff1a;没有可读&#xff0f;可写数据时&#xff0c;不会发生阻塞导致线程资源的浪费 一…

2023年11月4日(星期六)骑行香杆箐

2023年11月4日 (星期六) 骑行香杆箐&#xff0c;早8:30到9:00&#xff0c; 郊野公园西门&#xff0c;9:30准时出发 【因迟到者&#xff0c;骑行速度快者&#xff0c;可自行追赶偶遇。】 偶遇地点:大观公园门口集合 &#xff0c;家住东&#xff0c;南&#xff0c;北的骑友在下列…

高性能消息中间件 - Kafka3.x(四)

文章目录 高性能消息中间件 - Kafka3.x&#xff08;四&#xff09;Kafka监控-Kafka eagle&#xff08;EFAK 2.1.0版本&#xff09;⭐修改每个kafka的配置信息启动MySQL在mysql中创建名为ke的数据库开始安装并启动kafka-eagle Kafka的Kraft模式&#xff08;新版Kafka特性&#x…

【Redis】redis的过期策略如何实现有关定时器的补充

文章目录 redis的过期策略如何实现关于定时器的补充基于优先级队列/堆实现的定时器基于时间轮实现的定时器 redis的过期策略如何实现 注意&#xff1a;不能直接遍历所有的key来判断当前key是否过期&#xff0c;这样子效率非常低&#xff0c;redis整体策略是&#xff1a;定期删…