import AVFoundation import SwiftUI import UIKit /// 与安卓端 OralTrainerPlayerView 对齐的播放区: /// - 单击:播放 / 暂停 /// - 左划 / 右划:上一句 / 下一句(无句边界时回退为快退 / 快进) /// - 上划 / 下划:显示 / 隐藏字幕层(由 SwiftUI 横屏页接收) /// - 长按:按触摸点水平位置变速(左慢右快),松手恢复原速 struct PlayerSurfaceView: UIViewRepresentable { let controller: PlayerController var onVerticalSwipe: ((Bool) -> Void)? func makeUIView(context: Context) -> VideoSurfaceView { let view = VideoSurfaceView() view.controller = controller view.onVerticalSwipe = onVerticalSwipe view.attach(player: controller.player) view.setContentHuggingPriority(.defaultLow, for: .vertical) view.setContentHuggingPriority(.defaultLow, for: .horizontal) view.setContentCompressionResistancePriority(.defaultLow, for: .vertical) view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return view } func updateUIView(_ uiView: VideoSurfaceView, context: Context) { uiView.controller = controller uiView.onVerticalSwipe = onVerticalSwipe uiView.attach(player: controller.player) } } final class VideoSurfaceView: UIView { weak var controller: PlayerController? var onVerticalSwipe: ((Bool) -> Void)? private let videoLayer = AVPlayerLayer() var playerLayer: AVPlayerLayer { videoLayer } private let minSwipeDistance: CGFloat = 48 private let minSwipeVelocity: CGFloat = 160 private let longPressTimeout: TimeInterval = 0.5 private var tapRecognizer: UITapGestureRecognizer! private var panRecognizer: UIPanGestureRecognizer! private var longPressRecognizer: UILongPressGestureRecognizer! private var didDispatchSwipe = false private var swipeAxis: SwipeAxis? private enum SwipeAxis { case horizontal case vertical } override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .black isOpaque = true clipsToBounds = true videoLayer.videoGravity = .resizeAspect videoLayer.backgroundColor = UIColor.black.cgColor layer.addSublayer(videoLayer) tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap)) panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan)) panRecognizer.minimumNumberOfTouches = 1 panRecognizer.maximumNumberOfTouches = 1 longPressRecognizer = UILongPressGestureRecognizer( target: self, action: #selector(handleLongPress) ) longPressRecognizer.minimumPressDuration = longPressTimeout tapRecognizer.require(toFail: panRecognizer) tapRecognizer.require(toFail: longPressRecognizer) panRecognizer.require(toFail: longPressRecognizer) addGestureRecognizer(tapRecognizer) addGestureRecognizer(panRecognizer) addGestureRecognizer(longPressRecognizer) isUserInteractionEnabled = true } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() CATransaction.begin() CATransaction.setDisableActions(true) videoLayer.frame = bounds CATransaction.commit() } func attach(player: AVPlayer) { if videoLayer.player !== player { videoLayer.player = player } if videoLayer.superlayer !== layer { layer.addSublayer(videoLayer) } videoLayer.frame = bounds videoLayer.isHidden = false } @objc private func handleTap() { controller?.handleGesture(.singleTap) } @objc private func handlePan(_ recognizer: UIPanGestureRecognizer) { let translation = recognizer.translation(in: self) let velocity = recognizer.velocity(in: self) let dx = translation.x let dy = translation.y switch recognizer.state { case .began: resetSwipeTracking() case .changed: evaluateSwipe(dx: dx, dy: dy, velocity: velocity) case .ended, .cancelled, .failed: if !didDispatchSwipe { evaluateSwipe(dx: dx, dy: dy, velocity: velocity) } resetSwipeTracking() default: break } } private func evaluateSwipe(dx: CGFloat, dy: CGFloat, velocity: CGPoint) { guard !didDispatchSwipe else { return } if swipeAxis == nil { if abs(dx) > abs(dy) * 1.2 { swipeAxis = .horizontal } else if abs(dy) > abs(dx) * 1.2 { swipeAxis = .vertical } else { return } } switch swipeAxis { case .vertical: guard abs(dy) >= minSwipeDistance, abs(velocity.y) >= minSwipeVelocity else { return } didDispatchSwipe = true onVerticalSwipe?(dy < 0) case .horizontal: guard abs(dx) >= abs(dy), abs(dx) >= minSwipeDistance, abs(velocity.x) >= minSwipeVelocity else { return } didDispatchSwipe = true controller?.handleGesture(dx > 0 ? .swipeRight : .swipeLeft) default: break } } private func resetSwipeTracking() { didDispatchSwipe = false swipeAxis = nil } @objc private func handleLongPress(_ recognizer: UILongPressGestureRecognizer) { let location = recognizer.location(in: self) switch recognizer.state { case .began: controller?.beginLongPressSpeed(atX: location.x, width: bounds.width) case .changed: controller?.updateLongPressSpeed(atX: location.x, width: bounds.width) case .ended, .cancelled: controller?.endLongPressSpeed() default: break } } }