移动开发UI组件【免费下载链接】TextureSmooth asynchronous user interfaces for iOS apps.项目地址https://gitcode.com/gh_mirrors/te/Texture点击查看免费下载导读在 TextureAsyncDisplayKit中Layout Transition API 是专为让一切动画都变得简单而设计的布局动画体系——它甚至能让你把一整组视图平滑地变换为另一组完全不同的视图。本文以官方文档 layout-transition-api.md 为主线完整讲解transitionLayoutWithAnimation:与transitionLayoutWithSizeRange:animated:两个核心入口、animateLayoutTransition:自定义动画钩子、ASContextTransitioning上下文对象并结合仓库源码ASDisplayNodeLayout.mm、ASLayoutTransition.mm、ASContextTransitioning.h剖析其底层实现。读完你将掌握如何让节点响应内部状态变化在布局间自动插拔子节点并做动画如何响应旋转与尺寸变化以及何时该重写哪个回调。前置条件必须开启 Automatic Subnode ManagementASM官方文档明确指出使用 Layout Transition API 必须开启 Automatic Subnode ManagementASM。详见 automatic-subnode-mgmt.md。开启方式是在节点初始化时设置automaticallyManagesSubnodes YES。开启后你不再需要手动调用addSubnode:或removeFromSupernode节点的存在与否完全由layoutSpecThatFits:返回的 layout spec 决定- (instancetype)init { self [super init]; if (self) { self.automaticallyManagesSubnodes YES; // 只需创建子节点并持有强引用不再 addSubnode: } return self; }override init() { super.init() automaticallyManagesSubnodes true }需要注意的是ASM 开启后绝不应对该节点再调用addSubnode:/removeFromSupernode否则可能触发 A flattened layout must consist exclusively of node sublayouts 异常。也正因如此layoutSpecThatFits:必须能正确描述有这些元素 / 没有这些元素时的 UI 形态——这正是 Layout Transition API 判断插入、移除、移动的依据automatic-subnode-mgmt.md 中给出了基于数据是否返回而条件组合 children 的完整示例。Layout Transition API 的核心思想使用这套 API 时你只需要指定期望的新布局Texture 会负责完成以下工作自动计算新旧布局之间的差异自动插入新出现的元素在过渡结束后自动移除不再需要的元素自动更新仍存在元素的位置。同时API 还提供易于使用的钩子允许你完全自定义新元素进入时的起始位置、被移除元素的结束位置。文档原文的表述是designed to make all animations with Texture easy — even transforming an entire set of views into a completely different set of views!设计目标甚至包含把整组视图变换成完全不同的一组视图。在 ASDisplayNode.h 的ASDisplayNode(ASLayoutTransitioning)分类中这套 API 由以下成员构成成员作用transitionLayoutWithAnimation:使当前约束尺寸下的布局失效并重算触发过渡transitionLayoutWithSizeRange:animated:以新的ASSizeRange重算布局并触发过渡animateLayoutTransition:自定义动画的回调钩子新节点已插入didCompleteLayoutTransition:过渡完成后的清理回调默认执行移除cancelLayoutTransition取消所有进行中的布局过渡可在任意线程调用defaultLayoutTransitionDuration/Delay/Options默认过渡动画的时长默认 0.2s、延迟默认 0.0、选项场景一响应内部状态在两个布局之间切换动画官方文档用一个注册表单场景说明核心用法容器节点SignupNode内包含两个可编辑文本字段节点nameField与ageField和一个按钮节点通过属性fieldState决定layoutSpecThatFits:返回哪个字段。其内部 layout spec 如下- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { FieldNode *field; if (self.fieldState SignupNodeName) { field self.nameField; } else { field self.ageField; } ASStackLayoutSpec *stack [[ASStackLayoutSpec alloc] init]; [stack setChildren:[field, self.buttonNode]]; UIEdgeInsets insets UIEdgeInsetsMake(15.0, 15.0, 15.0, 15.0); return [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:stack]; }override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) - ASLayoutSpec { let fieldNode: FieldNode if self.fieldState .signupNodeName { fieldNode self.nameField } else { fieldNode self.ageField } let stack ASStackLayoutSpec() stack.children [fieldNode, buttonNode] let insets UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) return ASInsetLayoutSpec(insets: insets, child: stack) }当用户点击下一步时更新fieldState并调用transitionLayoutWithAnimation:触发过渡self.signupNode.fieldState SignupNodeAge; [self.signupNode transitionLayoutWithAnimation:YES];self.signupNode.fieldState .signupNodeName self.signupNode.transitionLayout(withAnimation: true, shouldMeasureAsync: true)该方法会使当前已计算的布局失效并用栈中现在包含ageField的新布局重新计算。默认行为的局限需要自定义动画块文档特别说明API 的默认实现会重算新布局并用其 sublayouts 去设置子节点的尺寸和位置但这个过程不带动画。文档同时指出未来版本可能会内置默认布局间动画并欢迎社区反馈。因此在注册表单这类场景中必须自己实现动画块。注意仓库源码中的默认animateLayoutTransition:实现ASDisplayNodeLayout.mm其实已经提供了一套淡入淡出 移动 尺寸变化的默认动画基于defaultLayoutTransitionDuration、defaultLayoutTransitionDelay、defaultLayoutTransitionOptions三个属性时长默认 0.2s文档中默认无动画的表述针对的是布局间元素替换的通用场景具体行为以你重写或调用 super 的实现为准。自定义动画重写 animateLayoutTransition:animateLayoutTransition:在transitionLayoutWithAnimation:完成新布局计算后调用。在实现中我们可以根据触发动画前设置的fieldState执行特定动画。下面是一个在SignupNode中重写该方法的完整示例- (void)animateLayoutTransition:(idASContextTransitioning)context { if (self.fieldState SignupNodeName) { CGRect initialNameFrame [context initialFrameForNode:self.ageField]; initialNameFrame.origin.x initialNameFrame.size.width; self.nameField.frame initialNameFrame; self.nameField.alpha 0.0; CGRect finalAgeFrame [context finalFrameForNode:self.nameField]; finalAgeFrame.origin.x - finalAgeFrame.size.width; [UIView animateWithDuration:0.4 animations:^{ self.nameField.frame [context finalFrameForNode:self.nameField]; self.nameField.alpha 1.0; self.ageField.frame finalAgeFrame; self.ageField.alpha 0.0; } completion:^(BOOL finished) { [context completeTransition:finished]; }]; } else { CGRect initialAgeFrame [context initialFrameForNode:self.nameField]; initialAgeFrame.origin.x initialAgeFrame.size.width; self.ageField.frame initialAgeFrame; self.ageField.alpha 0.0; CGRect finalNameFrame [context finalFrameForNode:self.ageField]; finalNameFrame.origin.x - finalNameFrame.size.width; [UIView animateWithDuration:0.4 animations:^{ self.ageField.frame [context finalFrameForNode:self.ageField]; self.ageField.alpha 1.0; self.nameField.frame finalNameFrame; self.nameField.alpha 0.0; } completion:^(BOOL finished) { [context completeTransition:finished]; }]; } }override func animateLayoutTransition(_ context: ASContextTransitioning) { if fieldState .signupNodeName { let initialNameFrame context.initialFrame(for: ageField) nameField.frame initialNameFrame nameField.alpha 0 var finalAgeFrame context.finalFrame(for: nameField) finalAgeFrame.origin.x - finalAgeFrame.size.width UIView.animate(withDuration: 0.4, animations: { self.nameField.frame context.finalFrame(for: self.nameField) self.nameField.alpha 1 self.ageField.frame finalAgeFrame self.ageField.alpha 0 }, completion: { finished in context.completeTransition(finished) }) } else { var initialAgeFrame context.initialFrame(for: nameField) initialAgeFrame.origin.x initialAgeFrame.size.width ageField.frame initialAgeFrame ageField.alpha 0 var finalNameFrame context.finalFrame(for: ageField) finalNameFrame.origin.x - finalNameFrame.size.width UIView.animate(withDuration: 0.4, animations: { self.ageField.frame context.finalFrame(for: self.ageField) self.ageField.alpha 1 self.nameField.frame finalNameFrame self.nameField.alpha 0 }, completion: { finished in context.completeTransition(finished) }) } }上下文对象 ASContextTransitioning过渡前后的完整信息回调中传入的context实现了 ASContextTransitioning 协议包含判断过渡前后节点状态所需的全部信息。协议提供的方法如下对应 ASContextTransitioning.h方法说明isAnimated本次过渡是否带动画layoutForKey:获取 fromASTransitionContextFromLayoutKey或 toASTransitionContextToLayoutKey的原始ASLayout对象constrainedSizeForKey:获取过渡前 / 过渡后的约束尺寸ASSizeRangesubnodesForKey:获取过渡前 / 过渡后布局中的子节点数组insertedSubnodes本次过渡中新插入的子节点removedSubnodes本次过渡中将被移除的子节点initialFrameForNode:节点在过渡开始前的 frame若过渡前不在层级中则返回CGRectNullfinalFrameForNode:节点在过渡完成时的 frame若过渡后已不在层级中则返回CGRectNullcompleteTransition:必须在动画结束后调用通知过渡完成在SignupNode示例中我们正是用initialFrameForNode:/finalFrameForNode:获取每个字段的 frame从而把它们从画面外移入 / 移出。关键点务必调用 completeTransition:在动画完成后必须对 context 调用completeTransition:——它会执行必要的内部步骤使新计算出的布局正式成为当前的calculatedLayout。从源码看默认实现会在动画 completion 中调用它ASDisplayNodeLayout.mm自定义实现时必须自己补上这一调用。隐式的子节点插入与移除在整个过渡过程中你没有调用过任何addSubnode:或removeFromSupernode。这是因为 Layout Transition API 会分析新旧布局之间节点层级的差异通过 ASM隐式地完成节点插入与移除插入发生在你的animateLayoutTransition:被调用之前——因此这是你手动调整层级例如重新排序的好时机移除发生在didCompleteLayoutTransition:中——也就是你调用completeTransition:之后。如果你需要手动执行删除可以重写didCompleteLayoutTransition:并执行自定义操作。注意这会覆盖默认行为官方建议要么调用super要么遍历 context 的removedSubnodesgetter 完成清理。默认实现ASDisplayNodeLayout.mm正是通过_pendingLayoutTransition调用applySubnodeRemovals来移除不再需要的节点。无动画调用与 isAnimated向transitionLayoutWithAnimation:传NO仍然会走完你的animateLayoutTransition:与didCompleteLayoutTransition:实现只是context的isAnimated属性为NO。如何处理这种情形完全由你决定。一个提供默认行为的简单方式是调用super- (void)animateLayoutTransition:(idASContextTransitioning)context { if ([context isAnimated]) { // perform animation } else { [super animateLayoutTransition:context]; } }override func animateLayoutTransition(_ context: ASContextTransitioning) { if context.isAnimated() { } else { super.animateLayoutTransition(context) } }默认实现的非动画分支会直接执行_layoutSublayouts并调用completeTransition:YESASDisplayNodeLayout.mm即无动画直接落地新布局。场景二响应 constrainedSize 变化当你想响应节点自身 bounds 的变化并动画重算布局时应调用transitionLayoutWithSizeRange:animated:。它与transitionLayoutWithAnimation:类似但如果传入的ASSizeRange与当前constrainedSizeForCalculatedLayout相等则不会触发动画源码层面是直接 noop见 ASDisplayNode.h 的注释。这非常适合响应旋转事件和控制器尺寸变化- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(idUIViewControllerTransitionCoordinator)coordinator { [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; [coordinator animateAlongsideTransition:^(idUIViewControllerTransitionCoordinatorContext _Nonnull context) { [self.node transitionLayoutWithSizeRange:ASSizeRangeMake(size, size) animated:YES]; } completion:nil]; }override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) coordinator.animate(alongsideTransition: { context in self.node.transitionLayout(with: ASSizeRange(min: size, max: size), animated: true, shouldMeasureAsync: true) }) }从源码理解过渡的内部流程transitionLayoutWithSizeRange:animated:shouldMeasureAsync:measurementCompletion:的实现位于 ASDisplayNodeLayout.mm其流程大致如下前置校验必须在主线程调用ASDisplayNodeAssertMainThread若constrainedSize.max宽度或高度小于等于 0 则直接忽略若节点正处于子节点的布局过渡中ASHierarchyStateIncludesLayoutPending则断言并返回。失效计算布局调用invalidateCalculatedLayout——源码注释明确说明这是节点的动画版 setNeedsLayout避免后续layoutThatFits:返回过期布局。分配过渡 ID每次新过渡生成transitionID后续新过渡会基于该 ID 判定并取消旧过渡同时将所有子节点置为ASHierarchyStateLayoutPending并记录pendingTransitionID。测量阶段以传入的 constrained size 执行完整布局创建calculateLayoutThatFits:。shouldMeasureAsync为YES时保证在后台线程测量ASPerformBlockOnBackgroundThread否则在调用线程测量。整个测量带ASLayoutElementContext携带 transitionID上下文。主线程提交更新_calculatedDisplayNodeLayout为新布局构造ASLayoutTransition持有 previousLayout 与 pendingLayout见 ASLayoutTransition.h与_ASTransitionContext持有 animated 标记、layout delegate 与 completion delegate见 _ASTransitionContext.h。回调测量完成触发_layoutTransitionMeasurementDidFinish然后执行可选的measurementCompletion官方注释它总是在主线程、在animateLayoutTransition:之前被调用。立即应用插入与移动调用applySubnodeInsertionsAndMoves把新节点真正挂进层级以便后续动画能作用于它们。触发动画调用animateLayoutTransition:context进入你的自定义动画实现。差异计算新旧布局如何对比ASLayoutTransition的calculateSubnodeOperationsIfNeededASLayoutTransition.mm负责计算插入、删除与移动若编译了 IGListDiffKit使用线性复杂度 O(mn) 的IGListDiff对 previousLayout 与 pendingLayout 的 sublayouts 做 diff否则使用内置的 NSArrayDiffing 的asdk_diffWithArray:得到 insertions、deletions 与 moves结果保存在_insertedSubnodes、_removedSubnodes、_insertedSubnodePositions、_subnodeMoves中随后applySubnodeInsertionsAndMoves按目标索引顺序插入/重排节点applySubnodeRemovals通过_removeFromSupernodeIfEqualTo:安全移除节点仅当子节点仍属于该父节点时才移除见 ASLayoutTransition.mm。此外ASLayoutTransition的isSynchronous会 BFS 遍历整个布局栈只要存在一个canLayoutAsynchronous NO的 layout element就判定该过渡无法异步执行ASLayoutTransition.mm从而把子节点插入/移除跳转到主线程执行。取消进行中的过渡cancelLayoutTransition可在任意线程调用ASDisplayNodeLayout.mm它清除当前 transitionID并让所有子节点退出 layout pending 状态。内部通过asdisplaynode_iscancelled_block_t在测量前后反复检查 transitionID一旦被新过渡取代就立即中止旧过渡的工作。官方示例工程ASDKLayoutTransition官方在仓库中提供了专门演示该 API 的示例工程 examples/ASDKLayoutTransition。其核心代码在 ViewController.mTransitionNode开启automaticallyManagesSubnodes YES并把defaultLayoutTransitionDuration设为1.0演示如何调参默认过渡动画点击按钮切换enabled状态并调用transitionLayoutWithAnimation:shouldMeasureAsync:measurementCompletion:源码第 79-81 行layoutSpecThatFits:根据enabled在textNodeOne与textNodeTwo之间二选一放入水平栈文件顶部用#define USE_CUSTOM_LAYOUT_TRANSITION 0开关自定义动画实现开启后重写animateLayoutTransition:通过removedSubnodes、insertedSubnodes、subnodesForKey:ASTransitionContextToLayoutKey与initialFrameForNode:/finalFrameForNode:实现两个文本节点左右滑入滑出、按钮跟随finalFrameForNode:移动、父节点随布局尺寸变化自动 resized 的完整动画并在 completion 中调用completeTransition:。这是把本文所有概念落到真实可运行代码的最佳参考建议配合文档阅读。要点速查使用前必须先开启Automatic Subnode ManagementautomaticallyManagesSubnodes YES且禁用 ASM 后不得再手动addSubnode:。transitionLayoutWithAnimation:负责当前约束尺寸下重新布局transitionLayoutWithSizeRange:animated:负责以新约束尺寸重新布局两者都必须在主线程调用。自定义动画重写animateLayoutTransition:新节点在回调前已插入旧节点在completeTransition:后才被移除。务必在动画完成后调用[context completeTransition:finished]否则新布局不会成为calculatedLayout。传NO时仍会走过渡回调可通过[context isAnimated]区分并建议调用super提供默认非动画落地行为。过渡期间的一切子节点增删都由 ASM 布局差异计算自动完成无需手动管理层级。赞分享移动开发UI组件【免费下载链接】TextureSmooth asynchronous user interfaces for iOS apps.项目地址https://gitcode.com/gh_mirrors/te/Texture点击查看免费下载相关推荐react-native-reanimated 布局过渡Layout Transitions完全指南从预置过渡到自定义动画react native reanimated 布局过渡Layout Transitions完全指南从预置过渡到自定义动画 布局动画Layout Ani移动开发前端ToolJet 移动端布局Mobile Layout完全指南从 600px 断点到双布局画布ToolJet 移动端布局Mobile Layout完全指南从 600px 断点到双布局画布 Mobile layout 是 ToolJet 应用构建器中低代码后端前端AI 应用MCP 服务Reanimated 布局动画实战用 entering 进入动画与 layout 布局过渡打造丝滑列表Layout Animations 教程Reanimated 布局动画实战用 entering 进入动画与 layout 布局过渡打造丝滑列表Layout Animations 教程 本文基于移动开发前端上一篇用免费的开源 yuzu 模拟器在 PC 上玩 Switch 游戏30 分钟就能完成首次启动下一篇让 AI 的置信度可信赖SemIf 如何用按工作负载温度校准把概率 ECE 降低 67%创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
Bangumi 追番客户端:从零跑通本地安装的完整指南 Bangumi 追番客户端:从零跑通本地安装的完整指南 【免费下载链接】Bangumi :electron: An unofficial https://bgm.tv ui first app client for Android and iOS, built with React Native. 一个无广告、以爱好为驱动、不以盈利为目的、专门做 ACG 的类似豆瓣的追番… · 2026/9/26 10:29:20
LinuxKit 项目中的 go-yaml v2 使用指南:YAML 解析、编码与实战 操作系统云原生容器运行时 【免费下载链接】linuxkit A toolkit for building secure, portable and lean operating systems for containers 项目地址: https://gitcode.com/gh_mirrors/li/linuxkit 点击查看 免费下载 LinuxKit 是用于构建安全、便携、精简的容器… · 2026/9/26 10:29:20
五子棋AI自博弈推理加速116倍:C++与GPU优化实战 1. 从一局五子棋说起:为什么要死磕推理速度五子棋这东西,规则简单到用一张餐巾纸就能讲明白,但真要让 AI 通过自博弈把棋力练出来,计算量一点都不“简单”。我最初用 Python 写了个能跑的自博弈框架,逻辑上没毛病&… · 2026/9/26 10:29:20
小白程序员必看:收藏这份大模型实战指南,轻松构建数字员工系统! 本文介绍了企业Agent系统的四层架构,重点阐述了本体层作为“虚拟办公室”的作用,包括行业语义、公司制度、业务逻辑和Action四类内容,以及如何用七个元素组织案件信息。文章还讨论了本体层的选型与施工,以及Agent如何利用本体层高… · 2026/9/26 11:08:42
LeetCode:合并两个有序链表 题目:解题思路:1.建立一个虚拟头结点ListNode dummy,声明cur 是我们用来拼接链表的“指针尾巴”,并取空节点的指针地址赋值给cur;2.通过判断两个链表不为空,进行循环比较;3.通过循环比较两链表的值… · 2026/9/26 11:08:42
AUTOSAR CP通信栈基础:ComStack_Types核心类型解析与工程实践 1. 从一次编译报错说起:ComStack_Types到底管什么第一次接触AUTOSAR CP通信栈的人,十有八九会在某个时刻被一个看似莫名其妙的编译错误拦住。我印象很深的一次,是在集成一个CAN通信模块时,代码里引用了PduInfoType,头文… · 2026/9/26 11:08:42
代码直接变论文!MSRA同款Agent库开源,读Repo一键生成初稿: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/9/26 11:08:42
【AUTOSAR】 Classic Platform COM 模块--从入门到放弃 【AUTOSAR】 Classic Platform COM 模块–从入门到放弃 这份材料主要依据 AUTOSAR_CP_SWS_COM.pdf(R23-11)整理,另外参考了 RTE(AUTOSAR_CP_SWS_RTE.pdf)、PDU Router(AUTOSAR_CP_TPS_ECUConfiguration.pdf… · 2026/9/26 11:08:36
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46