如何在macOS上用AppKit实现无异常的窗口滑入动画?
macOS AppKit 窗口平滑滑入动画解决方案
问题回顾
在实现窗口滑入动画时遇到两个异常:
- 修改X坐标动画:窗口初始会出现在屏幕左侧,再向右移动,而非从屏幕外平滑滑入
- 修改宽度动画:动画过程中contentView被裁剪,无法自适应窗口宽度变化
解决方案
方案1:修正X坐标滑入动画(解决初始窗口可见问题)
核心思路是初始隐藏窗口,动画启动时再显示并平滑移动到目标位置,同时使用animator()代理保证动画稳定性:
let contentView = ContentView(viewModel: viewModel) contentView.autoresizingMask = [.width, .height] // 确保contentView自适应窗口 let dockWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 250, height: 900), styleMask: [.nonactivatingPanel], backing: .buffered, defer: false ) dockWindow.contentView = contentView dockWindow.isReleasedWhenClosed = false dockWindow.orderOut(nil) // 初始隐藏窗口 if let focusedScreen = NSScreen.main { let screenHeight = focusedScreen.visibleFrame.height let targetY = (screenHeight - dockWindow.frame.height) / 2 + focusedScreen.visibleFrame.origin.y let targetRect = NSRect( x: focusedScreen.visibleFrame.minX, y: targetY, width: dockWindow.frame.width, height: dockWindow.frame.height ) // 初始位置设置在屏幕左侧外部 let initialRect = NSRect( x: focusedScreen.visibleFrame.minX - dockWindow.frame.width, y: targetY, width: dockWindow.frame.width, height: dockWindow.frame.height ) dockWindow.setFrame(initialRect, display: false) // 不立即绘制初始位置 NSAnimationContext.runAnimationGroup { context in context.duration = 0.2 context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) // 平滑时序曲线 // 使用animator()代理执行动画,比隐式动画更可靠 dockWindow.animator().setFrame(targetRect, display: true) dockWindow.orderFront(nil) // 动画启动时显示窗口 } completionHandler: { self.isAnimating = false } } else { isAnimating = false }
关键调整点:
- 初始调用
orderOut(nil)隐藏窗口,避免初始可见 setFrame(display: false)跳过初始位置的立即绘制- 使用
animator()代理执行窗口frame动画,替代隐式动画,减少异常 - 添加
easeInEaseOut时序函数,让动画更自然
方案2:修正宽度动画的contentView裁剪问题
如果坚持使用宽度变化实现滑入,需确保contentView能随窗口宽度自适应:
方法A:使用Autoresizing Mask
contentView.autoresizingMask = [.width, .height]
该设置让contentView随窗口的宽高变化自动拉伸,避免裁剪。
方法B:使用Auto Layout约束
contentView.translatesAutoresizingMaskIntoConstraints = false guard let windowContentView = dockWindow.contentView else { return } NSLayoutConstraint.activate([ contentView.leadingAnchor.constraint(equalTo: windowContentView.leadingAnchor), contentView.trailingAnchor.constraint(equalTo: windowContentView.trailingAnchor), contentView.topAnchor.constraint(equalTo: windowContentView.topAnchor), contentView.bottomAnchor.constraint(equalTo: windowContentView.bottomAnchor) ])
通过约束将contentView与窗口内容视图四边绑定,确保完全适配窗口大小变化。
方案3:替代方案——Core Layer平移动画
通过动画窗口contentView的layer实现滑入,避免直接修改窗口frame带来的异常,动画更平滑:
let contentView = ContentView(viewModel: viewModel) let dockWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 250, height: 900), styleMask: [.nonactivatingPanel], backing: .buffered, defer: false ) dockWindow.contentView = contentView dockWindow.isReleasedWhenClosed = false dockWindow.contentView?.wantsLayer = true // 开启layer支持 if let focusedScreen = NSScreen.main { let screenHeight = focusedScreen.visibleFrame.height let targetY = (screenHeight - dockWindow.frame.height) / 2 + focusedScreen.visibleFrame.origin.y let targetRect = NSRect( x: focusedScreen.visibleFrame.minX, y: targetY, width: dockWindow.frame.width, height: dockWindow.frame.height ) dockWindow.setFrame(targetRect, display: false) // 初始将contentView平移到屏幕左侧外 let initialTransform = CATransform3DMakeTranslation(-dockWindow.frame.width, 0, 0) dockWindow.contentView?.layer?.transform = initialTransform dockWindow.orderFront(nil) NSAnimationContext.runAnimationGroup { context in context.duration = 0.2 context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) CATransaction.begin() CATransaction.setAnimationDuration(context.duration) CATransaction.setAnimationTimingFunction(context.timingFunction) // 恢复到原始位置,实现滑入 dockWindow.contentView?.layer?.transform = CATransform3DIdentity CATransaction.commit() } completionHandler: { self.isAnimating = false } } else { isAnimating = false }
这种方式动画的是视图层而非窗口本身,不会触发窗口的frame重绘异常,视觉效果更流畅。
内容的提问来源于stack exchange,提问作者0x55aa
相关产品推荐
相关产品推荐

