VideoPlayer's Double tap related UX improvement

Change VideoPlayer's overlay show implementation
Add player gestures lock gesture settings
This commit is contained in:
PangMo5 2022-05-01 05:12:23 +09:00
parent f98aa8a396
commit 0f92343970
8 changed files with 107 additions and 34 deletions

View File

@ -240,6 +240,8 @@ internal enum L10n {
internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") }
/// Playback Speed
internal static var playbackSpeed: String { return L10n.tr("Localizable", "playbackSpeed") }
/// Player Gestures Lock Gesture Enabled
internal static var playerGesturesLockGestureEnabled: String { return L10n.tr("Localizable", "playerGesturesLockGestureEnabled") }
/// Play From Beginning
internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") }
/// Play Next

View File

@ -48,6 +48,8 @@ extension Defaults.Keys {
static let jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,

View File

@ -95,6 +95,9 @@ final class VideoPlayerViewModel: ViewModel {
@Published
var mediaItems: [BaseItemDto.ItemDetail]
@Published
var isHiddenOverlay = false
// MARK: ShouldShowItems
let shouldShowPlayPreviousItem: Bool
@ -116,6 +119,7 @@ final class VideoPlayerViewModel: ViewModel {
let overlayType: OverlayType
let jumpGesturesEnabled: Bool
let systemControlGesturesEnabled: Bool
let playerGesturesLockGestureEnabled: Bool
let resumeOffset: Bool
let streamType: ServerStreamType
let container: String
@ -244,6 +248,7 @@ final class VideoPlayerViewModel: ViewModel {
self.jumpForwardLength = Defaults[.videoPlayerJumpForward]
self.jumpGesturesEnabled = Defaults[.jumpGesturesEnabled]
self.systemControlGesturesEnabled = Defaults[.systemControlGesturesEnabled]
self.playerGesturesLockGestureEnabled = Defaults[.playerGesturesLockGestureEnabled]
self.shouldShowJumpButtonsInOverlayMenu = Defaults[.shouldShowJumpButtonsInOverlayMenu]
self.resumeOffset = Defaults[.resumeOffset]

View File

@ -40,6 +40,8 @@ struct SettingsView: View {
var jumpGesturesEnabled
@Default(.systemControlGesturesEnabled)
var systemControlGesturesEnabled
@Default(.playerGesturesLockGestureEnabled)
var playerGesturesLockGestureEnabled
@Default(.resumeOffset)
var resumeOffset
@Default(.subtitleSize)
@ -111,6 +113,8 @@ struct SettingsView: View {
Toggle(L10n.systemControlGesturesEnabled, isOn: $systemControlGesturesEnabled)
Toggle(L10n.playerGesturesLockGestureEnabled, isOn: $playerGesturesLockGestureEnabled)
Toggle(L10n.resume5SecondOffset, isOn: $resumeOffset)
Button {

View File

@ -400,13 +400,11 @@ struct VLCPlayerOverlayView: View {
.foregroundColor(Color.white)
}
var body: some View {
@ViewBuilder
var contents: some View {
if viewModel.overlayType == .normal {
mainBody
.contentShape(Rectangle())
.onTapGesture {
viewModel.playerOverlayDelegate?.didGenerallyTap()
}
.background {
Color(uiColor: .black.withAlphaComponent(0.5))
.ignoresSafeArea()
@ -414,11 +412,22 @@ struct VLCPlayerOverlayView: View {
} else {
mainBody
.contentShape(Rectangle())
.onTapGesture {
viewModel.playerOverlayDelegate?.didGenerallyTap()
}
}
}
var body: some View {
contents
.onLongPressGesture {
guard viewModel.playerGesturesLockGestureEnabled else { return }
viewModel.playerOverlayDelegate?.didGenerallyTap(point: nil)
viewModel.playerOverlayDelegate?.didLongPress()
}
.gesture(DragGesture(minimumDistance: 0)
.onEnded { value in
viewModel.playerOverlayDelegate?.didGenerallyTap(point: value.location)
})
.opacity(viewModel.isHiddenOverlay ? 0 : 1)
}
}
struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {

View File

@ -8,6 +8,7 @@
import Foundation
import JellyfinAPI
import UIKit
protocol PlayerOverlayDelegate {
@ -19,7 +20,8 @@ protocol PlayerOverlayDelegate {
func didSelectForward()
func didSelectMain()
func didGenerallyTap()
func didGenerallyTap(point: CGPoint?)
func didLongPress()
func didBeginScrubbing()
func didEndScrubbing()

View File

@ -46,6 +46,10 @@ class VLCPlayerViewController: UIViewController {
private var panBeganBrightness = CGFloat.zero
private var panBeganVolumeValue = Float.zero
private var panBeganPoint = CGPoint.zero
private var tapLocationStack = [CGPoint]()
private var isJumping = false
private var jumpingCompletionWork: DispatchWorkItem?
private var isTapWhenJumping = false
private lazy var videoContentView = makeVideoContentView()
private lazy var mainGestureView = makeMainGestureView()
@ -230,24 +234,17 @@ class VLCPlayerViewController: UIViewController {
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap))
let doubleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap))
doubleTapGesture.numberOfTapsRequired = 2
let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:)))
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
let longPeessGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(didLongPress))
view.addGestureRecognizer(singleTapGesture)
view.addGestureRecognizer(pinchGesture)
view.addGestureRecognizer(longPeessGesture)
if viewModel.jumpGesturesEnabled {
view.addGestureRecognizer(doubleTapGesture)
singleTapGesture.require(toFail: doubleTapGesture)
singleTapGesture.delaysTouchesBegan = true
doubleTapGesture.delaysTouchesBegan = true
if viewModel.playerGesturesLockGestureEnabled {
view.addGestureRecognizer(longPressGesture)
}
if viewModel.systemControlGesturesEnabled {
@ -264,6 +261,7 @@ class VLCPlayerViewController: UIViewController {
label.alpha = 0
label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 48)
label.layer.zPosition = 1
return label
}
@ -271,11 +269,13 @@ class VLCPlayerViewController: UIViewController {
private func makeGestureLockedOverlayView() -> UIView {
let backgroundView = UIView()
backgroundView.layer.zPosition = 1
backgroundView.alpha = 0
backgroundView.translatesAutoresizingMaskIntoConstraints = false
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
self?.isGesturesLocked = false
self?.hideLockedOverlay()
self?.didGenerallyTap()
}))
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "lock.open", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))?
@ -295,21 +295,12 @@ class VLCPlayerViewController: UIViewController {
}
@objc
private func didTap() {
didGenerallyTap()
private func didTap(_ gestureRecognizer: UITapGestureRecognizer) {
didGenerallyTap(point: gestureRecognizer.location(in: mainGestureView))
}
@objc
private func didDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) {
if gestureRecognizer.location(in: mainGestureView).x > (mainGestureView.frame.width / 2) {
didSelectForward()
} else {
didSelectBackward()
}
}
@objc
private func didLongPress() {
func didLongPress() {
guard !isGesturesLocked else { return }
isGesturesLocked = true
didGenerallyTap()
@ -647,9 +638,10 @@ extension VLCPlayerViewController {
guard let overlayHostingController = currentOverlayHostingController else { return }
guard overlayHostingController.view.alpha != 1 else { return }
overlayHostingController.view.alpha = 1
UIView.animate(withDuration: 0.2) {
overlayHostingController.view.alpha = 1
withAnimation(.easeInOut(duration: 0.2)) { [weak self] in
self?.viewModel.isHiddenOverlay = false
}
}
@ -660,8 +652,16 @@ extension VLCPlayerViewController {
guard overlayHostingController.view.alpha != 0 else { return }
UIView.animate(withDuration: 0.2) {
// for gestures UX
view.exchangeSubview(at: view.subviews.firstIndex(of: mainGestureView)!,
withSubviewAt: view.subviews.firstIndex(of: overlayHostingController.view)!)
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut) {
overlayHostingController.view.alpha = 0
} completion: { [weak self] _ in
guard let self = self else { return }
self.view.exchangeSubview(at: self.view.subviews.firstIndex(of: self.mainGestureView)!,
withSubviewAt: self.view.subviews.firstIndex(of: overlayHostingController.view)!)
self.viewModel.isHiddenOverlay = true
}
}
@ -998,16 +998,65 @@ extension VLCPlayerViewController: PlayerOverlayDelegate {
}
}
func didGenerallyTap() {
func didGenerallyTap(point: CGPoint? = nil) {
if isGesturesLocked {
toggleLockedOverlay()
} else {
if viewModel.jumpGesturesEnabled,
let point = point
{
let tempStack = tapLocationStack
tapLocationStack.append(point)
if isSameLocationWithLast(point: point, in: tempStack) {
isTapWhenJumping = false
isJumping = true
tapLocationStack.removeAll()
jumpingCompletionWork?.cancel()
jumpingCompletionWork = DispatchWorkItem(block: { [weak self] in
guard let self = self else { return }
self.isJumping = false
guard self.isTapWhenJumping else { return }
self.isTapWhenJumping = false
self.toggleOverlay()
})
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: jumpingCompletionWork!)
hideOverlay()
if point.x > (mainGestureView.frame.width / 2) {
didSelectForward()
} else {
didSelectBackward()
}
return
} else {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
guard let self = self else { return }
guard !self.tapLocationStack.isEmpty else { return }
self.tapLocationStack.removeFirst()
}
}
}
guard !isJumping else {
isTapWhenJumping = true
return
}
toggleOverlay()
}
restartOverlayDismissTimer(interval: 5)
}
private func isSameLocationWithLast(point: CGPoint, in stack: [CGPoint]) -> Bool {
guard let last = stack.last else { return false }
if last.x > (mainGestureView.frame.width / 2) {
return point.x > (mainGestureView.frame.width / 2)
} else {
return point.x <= (mainGestureView.frame.width / 2)
}
}
func didBeginScrubbing() {
stopOverlayDismissTimer()
}