Flutter自定义路径布局:从原理到实战实现环绕排列
1. 项目概述当布局不再局限于行与列在 Flutter 的世界里我们习惯了Row、Column、Stack和GridView这些“规矩”的布局组件。它们像乐高积木一样能搭建出绝大多数规整的界面。但你是否想过如果界面元素需要沿着一条曲线、一个圆形甚至一个任意自定义的路径来排列该怎么办比如一个音乐播放器的控制按钮环绕着专辑封面旋转排布或者一个社交应用的动态头像沿着波浪线散落分布。这就是“自定义路径布局”要解决的问题它让 UI 设计突破了矩形网格的束缚走向更富创意和表现力的领域。而“Vibe Coding”则是一种近年来在开发者社区特别是前端和移动端领域兴起的工作哲学。它强调的是一种流畅、直觉驱动的编码状态追求在编码过程中达到“心流”快速将创意转化为可运行的代码。它不特指某一种技术而更像是一种高效、愉悦的工作方法。将 Vibe Coding 与 Flutter 自定义路径布局结合意味着我们不再需要为复杂的数学计算和繁琐的控件定位而陷入僵局而是通过理解核心原理利用 Flutter 强大的画布能力以一种更直接、更富创造性的方式来实现动态、有机的界面效果。这篇文章就是一次 Vibe Coding 的实战演练。我们将彻底抛开对复杂第三方库的依赖从零开始深入 Flutter 的CustomPaint与CustomMultiChildLayout核心手把手带你实现一个完全可控的自定义路径布局管理器。无论你是想为你的应用增添一抹独特的视觉亮点还是单纯对 Flutter 的底层渲染机制感到好奇这篇内容都将为你提供从理论到实践的完整路径。2. 核心思路拆解路径布局的三大支柱实现一个自定义路径布局听起来复杂但拆解开来核心离不开三件事路径定义、子节点定位和布局约束传递。理解这三者就掌握了自定义布局的钥匙。2.1 路径的数学表达与参数化路径本质上是一系列点的集合以及连接这些点的规则。在 Flutter 中我们通常使用Path类来定义。但对于布局来说仅仅有一个Path对象是不够的。我们需要一个参数化的描述使得路径上的每一个点都能通过一个参数比如t取值范围通常是0.0到1.0来获取。为什么需要参数化想象一下我们要在一条贝塞尔曲线上均匀放置5个子组件。如果没有参数化我们很难精确计算出每个组件应该放在曲线上的哪个位置。参数化之后问题就简化了我们只需要在t 0.0, 0.25, 0.5, 0.75, 1.0这五个位置通过路径对象获取对应的坐标点即可。Flutter 的Path类提供了PathMetrics和PathMetric来帮助我们进行参数化操作。PathMetric可以获取路径的长度以及根据长度比例即我们的参数t来获取路径上的切线和位置。这是我们实现精准定位的数学基础。注意Path对象可能包含多个子路径例如调用多次moveTo。PathMetric会将它们作为一个连续的路径来处理这对于布局计算通常是方便的但如果你需要处理不连续的路径段则需要分别获取每个子路径的PathMetric。2.2 子组件在路径上的定位策略确定了路径和参数下一步就是决定子组件如何“放置”在路径的对应点上。这里有几个关键策略中心对齐这是最常用的策略。将子组件的中心点对准路径上的目标点。实现简单视觉效果均衡。切线方向旋转为了让组件“贴合”路径的走向比如让一个箭头图标始终沿着曲线方向我们需要获取路径在该点的切线角度。PathMetric的getTangentForOffset方法可以返回一个Tangent对象其中包含了该点的位置和角度。我们可以将此角度应用于子组件的变换Transform.rotate。法线方向偏移有时我们可能希望组件不是放在路径上而是放在路径的某一侧比如在一条水平线的上下方交错排列。这就需要计算路径的法线方向与切线垂直的方向并沿法线进行偏移。在自定义布局中我们需要在performLayout方法里为每一个子组件计算它最终的Offset位置和可选的旋转角度然后调用layoutChild和positionChild来完成布局。2.3 自定义布局器的约束传递与尺寸计算Flutter 的布局系统是自上而下传递约束BoxConstraints自下而上确定尺寸的。我们自定义的布局器作为一个父组件需要承担两个核心职责向子组件传递约束父组件收到来自其父级的约束比如最大宽度300px。它需要决定给每个子组件什么样的约束。对于路径布局一个常见的策略是不改变子组件自身的尺寸。即我们将父组件收到的宽松约束BoxConstraints.loose(Size)直接传递给每个子组件让子组件根据自己的内容决定自己的大小。这样能最大程度保持子组件原本的样式。确定自身尺寸所有子组件布局完成后父组件需要根据子组件的位置和自身逻辑决定自己最终占据多大空间。对于路径布局一个合理的自身尺寸是能够包围所有子组件的最小矩形。我们可以遍历所有子组件的位置和尺寸计算出一个能包含它们的Rect这个Rect的size就是布局器自身的理想尺寸。然后我们需要确保这个尺寸符合最初从父级接收到的约束如果不符则需要进行调整例如按比例缩放整个布局。3. 实战构建一个环绕路径布局组件理论说得再多不如一行代码。接下来我们将从零开始构建一个名为CircularPathLayout的布局组件它可以将子组件均匀地排列在一个圆形路径上并支持可选的旋转对齐。3.1 创建自定义布局器类首先我们创建一个继承自MultiChildRenderObjectWidget的 Widget。这个 Widget 是布局的配置部分它负责创建和管理对应的 RenderObject。import package:flutter/rendering.dart; import package:flutter/widgets.dart; class CircularPathLayout extends MultiChildRenderObjectWidget { final double radius; final bool rotateToTangent; CircularPathLayout({ Key? key, required this.radius, this.rotateToTangent false, required ListWidget children, }) : super(key: key, children: children); override RenderObject createRenderObject(BuildContext context) { return RenderCircularPathLayout( radius: radius, rotateToTangent: rotateToTangent, ); } override void updateRenderObject( BuildContext context, RenderCircularPathLayout renderObject, ) { renderObject ..radius radius ..rotateToTangent rotateToTangent; } }这个 Widget 很简单它接收两个配置参数radius圆的半径和rotateToTangent子组件是否旋转以对齐切线并持有一组子组件。createRenderObject和updateRenderObject是标准写法用于创建和更新真正的布局渲染对象。3.2 实现 RenderObject 与布局逻辑核心中的核心是RenderCircularPathLayout这个类。它继承自RenderBox并混入了ContainerRenderObjectMixin、RenderBoxContainerDefaultsMixin这是实现多子组件布局的常见模式。class RenderCircularPathLayout extends RenderBox with ContainerRenderObjectMixinRenderBox, MultiChildLayoutParentData, RenderBoxContainerDefaultsMixinRenderBox, MultiChildLayoutParentData { double _radius; bool _rotateToTangent; RenderCircularPathLayout({ required double radius, bool rotateToTangent false, ListRenderBox? children, }) : _radius radius, _rotateToTangent rotateToTangent { addAll(children); } double get radius _radius; set radius(double value) { if (_radius value) return; _radius value; markNeedsLayout(); // 半径改变需要重新布局 } bool get rotateToTangent _rotateToTangent; set rotateToTangent(bool value) { if (_rotateToTangent value) return; _rotateToTangent value; markNeedsLayout(); // 旋转策略改变需要重新布局 } }接下来就是重写performLayout方法。这是布局发生的地方。override void performLayout() { // 1. 确定自身尺寸一个能包围整个圆的方框 final double diameter _radius * 2; size constraints.constrain(Size(diameter, diameter)); // 如果没有子节点布局结束 if (firstChild null) return; final int childCount count; final double angleStep (2 * math.pi) / childCount; // 每个子组件间隔的角度 RenderBox? child firstChild; int index 0; while (child ! null) { // 2. 为子组件布局传递宽松约束允许子组件自定大小 child.layout(constraints.loosen(), parentUsesSize: true); final MultiChildLayoutParentData childParentData child.parentData! as MultiChildLayoutParentData; // 3. 计算当前子组件在圆上的位置角度 final double angle angleStep * index; final double x _radius _radius * math.cos(angle); final double y _radius _radius * math.sin(angle); // 4. 计算子组件的偏移量使其中心点位于计算的位置 final Offset childOffset Offset( x - child.size.width / 2, y - child.size.height / 2, ); childParentData.offset childOffset; // 5. 可选计算旋转角度 if (_rotateToTangent) { // 圆上某点的切线方向与该点的半径方向垂直。 // 在标准坐标系中角度angle的半径方向向量是(cos(angle), sin(angle)) // 切线方向向量是(-sin(angle), cos(angle))其角度为 angle pi/2 childParentData.rotation angle math.pi / 2; } else { childParentData.rotation 0.0; } // 移动到下一个子组件 child childParentData.nextSibling; index; } }这里有几个关键点自身尺寸我们简单地将自身尺寸设定为能包围整个圆的正方形并用外部约束constraints.constrain进行修正确保不超出父组件给的范围。子组件约束使用constraints.loosen()创建一个宽松约束告诉子组件“我的空间很大你想多大就多大但别超过我给的最大范围”。这保留了子组件的固有尺寸。位置计算使用三角函数math.cos和math.sin计算圆上点的坐标。注意我们将圆心放在了(_radius, _radius)点这样整个圆就在我们确定的size范围内了。偏移调整计算出的(x, y)是目标点的坐标而childParentData.offset是子组件左上角的位置。因此需要减去子组件宽高的一半来实现中心点对齐。旋转计算当需要旋转时我们计算了每个位置点的切线角度angle math.pi/2并将其存储在parentData中供后续绘制使用。实操心得在performLayout中child.layout的parentUsesSize参数必须设为true因为我们需要读取child.size来计算偏移量。如果设为false在访问child.size时会抛出异常。这是自定义布局时一个非常容易踩的坑。3.3 处理旋转的绘制逻辑布局计算出了位置和旋转角度但旋转的视觉效果需要在绘制阶段实现。我们需要重写paint方法。override void paint(PaintingContext context, Offset offset) { RenderBox? child firstChild; while (child ! null) { final MultiChildLayoutParentData childParentData child.parentData! as MultiChildLayoutParentData; final Offset childOffset offset childParentData.offset; if (childParentData.rotation ! 0.0) { // 需要旋转绘制 context.pushTransform( needsCompositing, // 通常为true启用图层合成以实现平滑变换 childParentData.offset offset, // 变换的原点子组件中心 Matrix4.rotationZ(childParentData.rotation), (PaintingContext context, Offset offset) { context.paintChild(child!, offset); }, ); } else { // 无需旋转直接绘制 context.paintChild(child, childOffset); } child childParentData.nextSibling; } } // 由于我们进行了旋转变换需要确保点击测试区域正确 override bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { return defaultHitTestChildren(result, position: position); }这里使用了PaintingContext.pushTransform来创建一个图层在该图层上应用旋转变换后再绘制子组件。变换的原点设置在了子组件的中心点这样旋转起来才自然。needsCompositing参数通常设为true它会让 Flutter 使用硬件加速的图层来执行这个变换性能更好。3.4 定义 ParentData 存储额外信息我们注意到在MultiChildLayoutParentData中并没有rotation字段。我们需要扩展它。class CircularPathLayoutParentData extends MultiChildLayoutParentData { double rotation 0.0; } // 然后修改 RenderCircularPathLayout指定其子组件使用这个 ParentData class RenderCircularPathLayout extends RenderBox with ContainerRenderObjectMixinRenderBox, CircularPathLayoutParentData, // 修改这里 RenderBoxContainerDefaultsMixinRenderBox, CircularPathLayoutParentData { // 修改这里 override void setupParentData(RenderBox child) { if (child.parentData is! CircularPathLayoutParentData) { child.parentData CircularPathLayoutParentData(); } } // ... 其余代码不变但需要将 MultiChildLayoutParentData 类型替换为 CircularPathLayoutParentData }通过setupParentData方法我们确保了每个子组件都使用我们自定义的CircularPathLayoutParentData这样就能安全地存储和使用rotation信息了。4. 使用示例与效果验证现在我们的CircularPathLayout已经完成了。让我们创建一个简单的页面来使用它。import package:flutter/material.dart; import circular_path_layout.dart; // 假设上面的代码保存在这个文件 class CircularPathLayoutDemo extends StatelessWidget { const CircularPathLayoutDemo({super.key}); override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text(环绕路径布局示例)), body: Center( child: Container( width: 300, height: 300, color: Colors.grey[200], child: CircularPathLayout( radius: 100, rotateToTangent: true, // 试试改为 false 看看效果 children: List.generate(6, (index) { return Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.blueAccent, borderRadius: BorderRadius.circular(20), ), child: Icon( Icons.star, color: Colors.white, size: 24, ), ); }), ), ), ), ); } }运行这个示例你会看到6个蓝色的圆形按钮均匀分布在一个半径为100的逻辑圆上并且每个按钮的图标都像指针一样指向圆心因为旋转对齐了切线。这就是自定义路径布局的魅力——用相对简单的数学和 Flutter 核心机制创造出独特的视觉排列。5. 从圆形到任意路径通用化探索圆形路径只是一个开始。基于我们上面建立的框架将其通用化以支持任意Path并不困难。核心思路是将radius参数替换为Path参数。在performLayout中使用PathMetric来替代三角函数计算。通过path.computeMetrics()获取PathMetric列表。对于第i个子组件计算其在总路径长度上的比例位置t i / childCount。使用pathMetric.getTangentForOffset(pathMetric.length * t)获取该点的Tangent对象。Tangent.position就是目标点坐标Tangent.angle就是切线角度。自身尺寸计算可以通过path.getBounds()获取路径的边界矩形并以此为基础计算布局器的尺寸同时考虑子组件的大小因为子组件可能超出路径边界。这将会是一个更强大的CustomPathLayout组件。实现它你将彻底掌握 Flutter 中自定义路径布局的精髓。6. 性能考量与常见陷阱自定义布局尤其是涉及复杂计算和变换时性能是需要关注的重点。布局缓存如果路径和子组件数量不变每次布局计算的结果理论上是一样的。可以考虑将计算好的位置和角度缓存起来在radius、rotateToTangent等属性未改变时直接使用缓存避免重复计算。在markNeedsLayout被调用时清空缓存。合成图层我们在paint中使用了pushTransform。这会创建一个新的合成图层。过多的合成图层会增加 GPU 的负担。如果子组件很多且都需要旋转需要考虑性能影响。对于静态或较少更新的布局这通常不是问题。parentUsesSize的正确使用这是新手最容易出错的地方。务必清楚你是否需要在布局阶段读取子组件的大小。如果需要就必须设为true。边界框Hit Test我们的hitTestChildren使用了默认实现它能够正确处理经过变换后的点击区域。这是因为defaultHitTestChildren会考虑parentData.offset。对于更复杂的变换如非均匀缩放可能需要自定义点击测试逻辑。一个典型的排查案例你发现子组件没有显示或者位置完全不对。第一步检查performLayout中是否调用了child.layout。第二步检查childParentData.offset是否被正确赋值。第三步在paint方法中打印childOffset看计算出的坐标是否在屏幕可见范围内。第四步检查父级 Container 的尺寸是否足够大能容纳下你的布局器。有时布局器计算出的size是正确的但它的父容器却给了它一个0x0的约束导致其被压缩得看不见。实现自定义布局是一个深入理解 Flutter 渲染管线的好机会。它让你从被动的组件使用者转变为界面规则的制定者。当你能够随心所欲地控制每一个像素的排列时那种创造力和掌控感正是 Vibe Coding 所追求的最佳状态。

相关新闻

ESP32 Wi-Fi开发全解析:从STA/AP模式到HTTP客户端实战

ESP32 Wi-Fi开发全解析:从STA/AP模式到HTTP客户端实战

1. 项目概述:为什么ESP32的Wi-Fi功能如此重要?如果你玩过Arduino,可能会觉得联网是个麻烦事,得外接个Wi-Fi模块,接线、配置协议栈,一堆事儿。但ESP32的出现,直接把这事儿给“包圆”了。它内置了…

2026/9/29 12:12:30 阅读更多 →
网络基础入门:从OSI模型到防火墙配置的实战指南

网络基础入门:从OSI模型到防火墙配置的实战指南

1. 从零开始:为什么我们需要理解网络基础? 如果你刚接触网络,看到“LAN”、“WAN”、“防火墙”这些词,可能会觉得它们属于机房里的工程师,离自己很远。但事实上,我们每天都在和它们打交道。你手机连上家里…

2026/10/4 18:31:47 阅读更多 →
Fable5实战:从零构建AI驱动的互动叙事游戏《死线求生》

Fable5实战:从零构建AI驱动的互动叙事游戏《死线求生》

1. 项目概述:当“死线求生”遇上Fable5,一场属于普通人的创作冒险最近在AI创作圈子里,Fable5这个名字的热度可以说是直线飙升。从“claude fable5 怎么用”到“opus5和fable5”的对比,再到各种关于“fable5使用”的教程&#xff0…

2026/10/7 10:48:39 阅读更多 →

最新新闻

SpringBoot集成Hyperledger Fabric实现DID去中心化身份认证

SpringBoot集成Hyperledger Fabric实现DID去中心化身份认证

简介:本资源是一套面向本科毕业设计的分布式身份认证系统用户端实现,基于Hyperledger Fabric区块链构建可信身份管理体系,适用于信息安全、区块链开发与Java后端方向的学习者与毕设开发者。项目采用SpringBoot框架搭建,完整覆盖用…

2026/10/9 6:02:59 阅读更多 →
Linux进程间通信从原理到实战:共享内存与信号量完整指南

Linux进程间通信从原理到实战:共享内存与信号量完整指南

凡是常年跟Linux多进程程序打交道的人,早晚都会碰到一个绕不开的话题:进程间通信(IPC)。你可能已经见过进程间通信这个词无数次了,但真正在代码里用起来,尤其是要在性能、可靠性、复杂度三者之间做取舍时&a…

2026/10/9 6:02:59 阅读更多 →
旅游景点方面级情感分析实战:从语料构建到BERT模型调优

旅游景点方面级情感分析实战:从语料构建到BERT模型调优

简介:面向计算机相关专业学生完成毕业设计或课程设计,这份资源围绕旅游景点评论的方面级别情感分析任务,给出从语料库、模型训练到Django Web展示的完整源码方案。项目后端使用Django框架,涵盖数据库与ORM设计、评论文本预处理、情…

2026/10/9 6:02:59 阅读更多 →
时间序列预测实战:基于PyTorch统一框架对比LSTM、Transformer与自定义模型

时间序列预测实战:基于PyTorch统一框架对比LSTM、Transformer与自定义模型

简介:面向计算机相关专业学生和毕业设计开发者,资源以ETTh1电力负荷数据集为对象,提供了LSTM、Transformers以及自定义线性模型三种时间序列预测实现,用户可通过调整模型名称、序列长度等超参数对比不同架构的预测效果&#xff0c…

2026/10/9 6:02:59 阅读更多 →
AI写作全流程拆解:诘问、协议、生成三环节打造内容创作SOP

AI写作全流程拆解:诘问、协议、生成三环节打造内容创作SOP

当我的工作台同时贴上三张便签——“为什么必须写这个”“按什么规则写”“生成完谁来审”——我突然意识到,过去半年反复打磨的AI辅助创作流程,本质上是一套由“诘问、协议、生成”拼起来的流水线。我把它整理成《元创力》纪实录的第六卷,主…

2026/10/9 6:02:59 阅读更多 →
OKL4微内核源码深度拆解:从IPC到用户态驱动设计

OKL4微内核源码深度拆解:从IPC到用户态驱动设计

简介:OKL4 1.4.1.1 是微内核领域早期颇具代表性的发行版,适合操作系统课程学习者、嵌入式系统开发者以及想深入理解内核机理的工程师。资源以 tar.gz 压缩格式打包,整体约 58.71MB,解开后即可按目录查看完整源码结构。目前已有 94…

2026/10/9 6:01:59 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 13:34:55 阅读更多 →