187 lines
6.1 KiB
Swift
187 lines
6.1 KiB
Swift
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
|
||
}
|
||
}
|
||
}
|