:让截图从取景框飞上晾衣绳的 Core Animation 动画实现全解析)
Tendedero 源码剖析五让截图从取景框飞上晾衣绳的 Core Animation 动画实现全解析【免费下载链接】tendederoScreenshots, hung out to dry. A tiny native macOS app that hangs every screenshot on a line at the top of your screen.项目地址: https://gitcode.com/gh_mirrors/te/tendederoTendedero 是一款免费开源的 macOS 截图管理工具每拍一张截图它都会从取景位置飞到屏幕顶部的晾衣绳上。本文带你剖析这个飞行动画的完整实现——它没有用 SwiftUI 的声明式动画而是用 Core Animation 的CALayer 一个 120Hz 的手动时钟逐帧驱动是理解 macOS 上自定义几何变形动画的优秀范例。一、起飞点从哪来读取截图区域飞行动画的前提是知道截图是在屏幕的哪里拍的。答案藏在 macOS 的截图文件本身系统会把截取区域以扩展属性的形式写进 PNG 文件里。captureRect(of:)函数通过getxattr读取com.apple.metadata:kMDItemScreenCaptureGlobalRect这个键拿到 4 个数值x、y、宽、高再做一次坐标翻转macOS 元数据用屏幕左上角为原点而 AppKit 用左下角就得到了截图区域源码位置CaptureFlight.swift#L7-L24新截图则由 ScreenshotWatcher.swift 通过DispatchSource.makeFileSystemObjectSource监听截图文件夹发现AppDelegate收到通知后判断能不能飞若能从文件读出取景区域 → 走飞行动画读不到比如截图来自其他工具→ 直接掉到绳子上决策入口在 AppDelegate.swift#L202-L231 的hangCapture与fly两个方法先让晾衣绳滑出并布局延时 0.03 秒等 SwiftUI 完成排版再量出卡片目标位置才正式起飞。二、为什么用整块全屏 CALayer 窗口做飞行卡片要穿越屏幕顶部那条半透明的晾衣绳面板LinePanel.swift如果只在面板内部做动画卡片飞出面板边界就会被裁掉。所以CaptureFlight干脆创建了一块覆盖整块屏幕、完全透明、不响应鼠标的NSPanelwindow.ignoresMouseEvents true // 鼠标直接穿过去 window.level NSWindow.Level(rawValue: NSWindow.Level.floating.rawValue 1) // 浮在晾衣绳之上在这块飞行画布上它手工搭了一棵 5 层的CALayer树CaptureFlight.swift#L99-L131图层作用container卡片容器负责位移、旋转、阴影glass毛玻璃底板透明度由 0 渐显photo截图本体resizeAspectFill填充edge用CAGradientLayer 描边mask做的高光边框clip顶部那个小夹子用 4 段金属渐变绘制关键设计是**飞行即变形**截图在飞行过程中同步缩放、旋转、长出圆角、玻璃边框和夹子逐渐浮现——落地那一刻飞行窗口里的卡片和绳子上真正的卡片已经完全同构之后只需交叉淡化观众看不到任何切换。三、0.65 秒的飞行120Hz 手动时钟逐帧插值这是全文最核心的部分。CaptureFlight不用任何系统的隐式/显式动画 API而是自己当导演120Hz 计时器Timer(timeInterval: 1.0 / 120.0)加进主 RunLoop 的.common模式每次tick()用CACurrentMediaTime()计算进度kCaptureFlight.swift#L134-L143缓动曲线用三次缓动easeInOutCubic把线性进度k变成慢—快—慢的节奏每一帧在CATransaction.setDisableActions(true)里直接写几何值关掉隐式动画确保所见即所算每一帧的插值逻辑非常值得抄进你的工具箱CaptureFlight.swift#L186-L219位置lerp线性插值起点和终点的中心与顶边再叠加sin(.pi * k) * 30——一个正弦拱形让卡片沿 30 点高的抛物线掠过屏幕而不是干巴巴的直线尺寸与圆角宽高从截图原始比例lerp到卡片尺寸圆角从 0 长到 16配件浮现玻璃、边框、夹子的透明度走另一条更晚的缓动smooth(k, 0.35, 1)smoothstep即飞行 35% 之后才开始穿衣旋转按卡片随机的tilt值从 0 度旋到位注意源码里那句注释——SwiftUI 顺时针为正Core Animation 是反的角度要取负 小结这套手动时钟 逐帧插值 关闭隐式动画的模式本质上是把 Core Animation 降级成了一个可控的 2D 变换器。适合任何需要精确同步多个属性、或者中途要改变形的复杂动画。四、落地的瞬间交叉淡化交接k走到 1 后飞行窗口并不马上消失NSAnimationContext.runAnimationGroup({ ctx in ctx.duration 0.16 window.animator().alphaValue 0 })真正挂在绳子上的 SwiftUI 卡片PeggedView在同一位置淡入飞行窗口淡出盖在它上面0.16 秒内完成无缝交接CaptureFlight.swift#L145-L163。落地后 PeggedView.swift#L123-L126 还会给卡片一个 2.2 度的小晃模拟被夹子接住的物理惯性。五、反向复用让卡片掉下去CaptureFlight不是单向的。把falling true同一个类就变成了脱落动画CaptureFlight.swift#L69-L79卡片从绳上垂直下落 520 点倾斜角继续加大透明度用e raw³的三次缓入同步衰减0.55 秒完成——和飞行共用同一套图层结构和窗口机制是一个引擎、两种叙事的漂亮复用。之后 Line.swift 里的微风系统每 7~16 秒随机来一阵风卡片用interpolatingSpring弹簧回摆接手让绳子和照片在动画结束后依然有生命感。六、复盘这套实现的三个可复用思路飞行画布独立成窗动画要跨越界面边界时用全屏透明NSPanel兜底别让容器裁剪你的主角手动时钟驱动多属性同步TimerCACurrentMediaTime()setDisableActions(true)把缓动曲线写成纯函数每帧全量重算几何变形 交叉淡化代替切换让飞行中的卡片和最终的卡片在终点完全一致交接时观众只会觉得它一直在那相关源码速查飞行动画主体CaptureFlight.swift起飞决策与落点计算AppDelegate.swift#L202-L253截图监听与取景区域ScreenshotWatcher.swift、CaptureFlight.swift#L7-L24落地后的卡片与晃动PeggedView.swift绳上状态管理Line.swift【免费下载链接】tendederoScreenshots, hung out to dry. A tiny native macOS app that hangs every screenshot on a line at the top of your screen.项目地址: https://gitcode.com/gh_mirrors/te/tendedero创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考