如何在iOS自定义相机中实现类似iPhone原生相机的超广角与广角镜头平滑动画过渡效果
如何在iOS自定义相机中实现类似iPhone原生相机的超广角与广角镜头平滑动画过渡效果
我太懂你想要的那种丝滑感了——原生相机在广角和超广角之间切换时,完全没有突兀的跳变,就像只是在连续变焦一样,根本察觉不到设备在切换。其实核心逻辑不是直接硬切相机设备,而是把**「变焦平滑过渡」和「设备无缝切换」**绑定在一起,让视觉上形成连续的体验。下面结合你的Camera类代码,一步步帮你实现这个效果:
一、先理解原生相机的切换逻辑
原生相机的丝滑切换本质是「变焦衔接」:
- 从广角(1x)切到超广角(0.5x):先以广角镜头做数码变焦降到0.5x,到达这个点时无缝切换到超广角硬件镜头,继续拉变焦就用超广角的数码变焦(比如到0.3x)
- 从超广角(0.5x)切到广角(1x):先以超广角做数码变焦升到1x,到达后切换到广角镜头,继续拉就用广角的数码变焦或长焦镜头
简单说:先通过变焦让两个镜头的画面大小完全一致,再切换设备,这样用户完全看不到跳变。
二、修改你的Camera类,实现平滑切换
1. 添加必要的状态属性
首先在Camera类中新增几个关键属性,用来跟踪当前设备、切换状态和可用镜头:
@MainActor @Observable class Camera: NSObject, AVCaptureSessionControlsDelegate, @preconcurrency AVCapturePhotoCaptureDelegate { // ... 你原有的属性 ... // 新增:当前激活的相机设备 private var currentDevice: AVCaptureDevice? // 新增:标记是否正在切换镜头,避免重复触发 private var isSwitchingLens = false // 新增:所有可用的后置镜头(按等效变焦排序) private var availableBackLenses: [AVCaptureDevice] { let discoverySession = AVCaptureDevice.DiscoverySession( deviceTypes: [.builtInUltraWideCamera, .builtInWideAngleCamera, .builtInTelephotoCamera], mediaType: .video, position: .back ) // 按基础变焦从小到大排序:超广角(0.5x) < 广角(1x) < 长焦(2x) return discoverySession.devices.sorted { $0.minAvailableVideoZoomFactor < $1.minAvailableVideoZoomFactor } } // ... 你原有的方法 ... }
2. 重构设备切换与变焦逻辑
替换原有的setCameraDevice方法,新增硬件级平滑变焦和镜头切换的核心方法:
// 重写setCameraDevice,保存当前设备并同步变焦状态 func setCameraDevice(to device: AVCaptureDevice) { guard permission == .granted else { print("Permissão para uso da câmera não concedida.") return } do { try device.lockForConfiguration() session.beginConfiguration() // 移除旧输入输出 session.inputs.forEach { session.removeInput($0) } session.outputs.forEach { session.removeOutput($0) } let input = try AVCaptureDeviceInput(device: device) guard session.canAddInput(input), session.canAddOutput(cameraOutput) else { session.commitConfiguration() print("Cannot add camera output") device.unlockForConfiguration() return } session.addInput(input) session.addOutput(cameraOutput) setupCameraControl(device) // 配置预设 for preset in presets { if session.canSetSessionPreset(preset) { session.sessionPreset = preset print("Preset configurado para: \(preset)") break } } session.commitConfiguration() device.unlockForConfiguration() // 保存当前设备并同步变焦因子 self.currentDevice = device self.zoomFactor = device.videoZoomFactor } catch { print(error.localizedDescription) } } // 新增:硬件级平滑变焦方法 private func setZoomSmoothly(to targetZoom: CGFloat, animated: Bool, completion: (() -> Void)? = nil) { guard let device = currentDevice, !isSwitchingLens else { completion?() return } // 限制变焦在设备支持的范围内 let clampedZoom = max(device.minAvailableVideoZoomFactor, min(targetZoom, device.maxAvailableVideoZoomFactor)) do { try device.lockForConfiguration() if animated { // 用苹果提供的硬件级变焦动画,比UI动画更流畅 device.ramp(toVideoZoomFactor: clampedZoom, withRate: 3.0) { [weak self] finished in device.unlockForConfiguration() self?.zoomFactor = clampedZoom completion?() } } else { device.videoZoomFactor = clampedZoom device.unlockForConfiguration() self.zoomFactor = clampedZoom completion?() } } catch { print(error.localizedDescription) device.unlockForConfiguration() completion?() } } // 新增:核心的平滑镜头切换方法 func smoothSwitchToEquivalentZoom(_ targetZoom: CGFloat, animated: Bool = true) { guard permission == .granted, !isSwitchingLens, let currentDevice = currentDevice else { return } // 找到能覆盖目标变焦的最合适镜头 guard let targetDevice = availableBackLenses.first(where: { targetZoom >= $0.minAvailableVideoZoomFactor && targetZoom <= $0.maxAvailableVideoZoomFactor }) else { return } if currentDevice == targetDevice { // 已经是目标镜头,直接调整变焦 setZoomSmoothly(to: targetZoom, animated: animated) return } isSwitchingLens = true // 计算过渡变焦点:两个镜头画面完全重合的位置 let transitionZoom = currentDevice.minAvailableVideoZoomFactor < targetDevice.minAvailableVideoZoomFactor ? targetDevice.minAvailableVideoZoomFactor // 超广角→广角:过渡到1x : targetDevice.maxAvailableVideoZoomFactor // 广角→超广角:过渡到0.5x // 三步完成丝滑切换:变焦→切设备→继续变焦 setZoomSmoothly(to: transitionZoom, animated: animated) { [weak self] in guard let self = self else { return } self.setCameraDevice(to: targetDevice) if targetZoom != transitionZoom { self.setZoomSmoothly(to: targetZoom, animated: animated) { self.isSwitchingLens = false } } else { self.isSwitchingLens = false } } }
3. 替换原有的变焦和切换触发逻辑
修改zoomFactor的监听和镜头切换方法,改用新的平滑逻辑:
// 修改zoomFactor的didSet,用平滑变焦替代硬切 var zoomFactor: CGFloat = 1.0 { didSet { guard !isSwitchingLens else { return } setZoomSmoothly(to: zoomFactor, animated: true) } } // 新增快捷切换按钮的方法(比如UI上的0.5x/1x按钮) func switchToUltraWide() { smoothSwitchToEquivalentZoom(0.5) } func switchToWideAngle() { smoothSwitchToEquivalentZoom(1.0) } // 重构前后置切换逻辑,避免冲突 func toggleCamera() { cameraPosition = cameraPosition == .back ? .front : .back guard let device = AVCaptureDevice.DiscoverySession( deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: cameraPosition ).devices.first else { print("Couldn't find the \(cameraPosition == .back ? "back" : "front") camera") return } // 前后置切换时重置为1x变焦 isSwitchingLens = true setCameraDevice(to: device) self.zoomFactor = 1.0 isSwitchingLens = false print("Switched to \(cameraPosition == .back ? "back" : "front") camera") }
三、SwiftUI预览层同步
确保你的预览视图使用AVCaptureVideoPreviewLayer,它会自动跟随相机的变焦和设备切换状态:
struct CameraPreviewView: UIViewRepresentable { let camera: Camera func makeUIView(context: Context) -> UIView { let view = UIView(frame: UIScreen.main.bounds) let previewLayer = AVCaptureVideoPreviewLayer(session: camera.session) previewLayer.videoGravity = .resizeAspectFill previewLayer.frame = view.bounds view.layer.addSublayer(previewLayer) return view } func updateUIView(_ uiView: UIView, context: Context) { // 预览层会自动同步相机状态,无需额外操作 } }
现在你在SwiftUI视图中调用camera.switchToUltraWide()或调整zoomFactor,就能得到和原生相机完全一致的丝滑过渡效果了!
内容来源于stack exchange




