118 lines
4.0 KiB
Swift
118 lines
4.0 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.playerLayer.player = controller.player
|
||
view.controller = controller
|
||
view.onVerticalSwipe = onVerticalSwipe
|
||
return view
|
||
}
|
||
|
||
func updateUIView(_ uiView: VideoSurfaceView, context: Context) {
|
||
uiView.playerLayer.player = controller.player
|
||
uiView.onVerticalSwipe = onVerticalSwipe
|
||
}
|
||
}
|
||
|
||
final class VideoSurfaceView: UIView {
|
||
weak var controller: PlayerController?
|
||
var onVerticalSwipe: ((Bool) -> Void)?
|
||
|
||
override class var layerClass: AnyClass {
|
||
AVPlayerLayer.self
|
||
}
|
||
|
||
var playerLayer: AVPlayerLayer {
|
||
layer as! AVPlayerLayer
|
||
}
|
||
|
||
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!
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .black
|
||
playerLayer.videoGravity = .resizeAspect
|
||
|
||
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")
|
||
}
|
||
|
||
@objc private func handleTap() {
|
||
controller?.handleGesture(.singleTap)
|
||
}
|
||
|
||
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
|
||
guard recognizer.state == .ended || recognizer.state == .cancelled else {
|
||
return
|
||
}
|
||
let translation = recognizer.translation(in: self)
|
||
let velocity = recognizer.velocity(in: self)
|
||
let dx = translation.x
|
||
let dy = translation.y
|
||
if abs(dy) > abs(dx),
|
||
abs(dy) >= minSwipeDistance,
|
||
abs(velocity.y) >= minSwipeVelocity {
|
||
onVerticalSwipe?(dy < 0)
|
||
return
|
||
}
|
||
guard abs(dx) >= abs(dy),
|
||
abs(dx) >= minSwipeDistance,
|
||
abs(velocity.x) >= minSwipeVelocity else {
|
||
return
|
||
}
|
||
controller?.handleGesture(dx > 0 ? .swipeRight : .swipeLeft)
|
||
}
|
||
|
||
@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
|
||
}
|
||
}
|
||
}
|