You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在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

火山引擎 最新活动