Files
mediaplayer/ios/OralTrainer/Player/PlayerSurfaceView.swift
2026-09-13 12:58:45 +08:00

187 lines
6.1 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}
}