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") } internal static var playbackSettings: String { return L10n.tr("Localizable", "playbackSettings") }
/// Playback Speed /// Playback Speed
internal static var playbackSpeed: String { return L10n.tr("Localizable", "playbackSpeed") } 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 /// Play From Beginning
internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") } internal static var playFromBeginning: String { return L10n.tr("Localizable", "playFromBeginning") }
/// Play Next /// 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 jumpGesturesEnabled = Key<Bool>("gesturesEnabled", default: true, suite: SwiftfinStore.Defaults.generalSuite)
static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true, static let systemControlGesturesEnabled = Key<Bool>("systemControlGesturesEnabled", default: true,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let playerGesturesLockGestureEnabled = Key<Bool>("playerGesturesLockGestureEnabled", default: true,
suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen, static let videoPlayerJumpForward = Key<VideoPlayerJumpLength>("videoPlayerJumpForward", default: .fifteen,
suite: SwiftfinStore.Defaults.generalSuite) suite: SwiftfinStore.Defaults.generalSuite)
static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen, static let videoPlayerJumpBackward = Key<VideoPlayerJumpLength>("videoPlayerJumpBackward", default: .fifteen,

View File

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

View File

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

View File

@ -400,13 +400,11 @@ struct VLCPlayerOverlayView: View {
.foregroundColor(Color.white) .foregroundColor(Color.white)
} }
var body: some View { @ViewBuilder
var contents: some View {
if viewModel.overlayType == .normal { if viewModel.overlayType == .normal {
mainBody mainBody
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture {
viewModel.playerOverlayDelegate?.didGenerallyTap()
}
.background { .background {
Color(uiColor: .black.withAlphaComponent(0.5)) Color(uiColor: .black.withAlphaComponent(0.5))
.ignoresSafeArea() .ignoresSafeArea()
@ -414,11 +412,22 @@ struct VLCPlayerOverlayView: View {
} else { } else {
mainBody mainBody
.contentShape(Rectangle()) .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 { struct VLCPlayerCompactOverlayView_Previews: PreviewProvider {

View File

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

View File

@ -46,6 +46,10 @@ class VLCPlayerViewController: UIViewController {
private var panBeganBrightness = CGFloat.zero private var panBeganBrightness = CGFloat.zero
private var panBeganVolumeValue = Float.zero private var panBeganVolumeValue = Float.zero
private var panBeganPoint = CGPoint.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 videoContentView = makeVideoContentView()
private lazy var mainGestureView = makeMainGestureView() private lazy var mainGestureView = makeMainGestureView()
@ -230,24 +234,17 @@ class VLCPlayerViewController: UIViewController {
let singleTapGesture = UITapGestureRecognizer(target: self, action: #selector(didTap)) 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 pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(didPinch(_:)))
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:))) 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(singleTapGesture)
view.addGestureRecognizer(pinchGesture) view.addGestureRecognizer(pinchGesture)
view.addGestureRecognizer(longPeessGesture)
if viewModel.jumpGesturesEnabled { if viewModel.playerGesturesLockGestureEnabled {
view.addGestureRecognizer(doubleTapGesture) view.addGestureRecognizer(longPressGesture)
singleTapGesture.require(toFail: doubleTapGesture)
singleTapGesture.delaysTouchesBegan = true
doubleTapGesture.delaysTouchesBegan = true
} }
if viewModel.systemControlGesturesEnabled { if viewModel.systemControlGesturesEnabled {
@ -264,6 +261,7 @@ class VLCPlayerViewController: UIViewController {
label.alpha = 0 label.alpha = 0
label.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false
label.font = .systemFont(ofSize: 48) label.font = .systemFont(ofSize: 48)
label.layer.zPosition = 1
return label return label
} }
@ -271,11 +269,13 @@ class VLCPlayerViewController: UIViewController {
private func makeGestureLockedOverlayView() -> UIView { private func makeGestureLockedOverlayView() -> UIView {
let backgroundView = UIView() let backgroundView = UIView()
backgroundView.layer.zPosition = 1
backgroundView.alpha = 0 backgroundView.alpha = 0
backgroundView.translatesAutoresizingMaskIntoConstraints = false backgroundView.translatesAutoresizingMaskIntoConstraints = false
let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in let button = UIButton(type: .custom, primaryAction: UIAction(handler: { [weak self] _ in
self?.isGesturesLocked = false self?.isGesturesLocked = false
self?.hideLockedOverlay() self?.hideLockedOverlay()
self?.didGenerallyTap()
})) }))
button.translatesAutoresizingMaskIntoConstraints = false button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(UIImage(systemName: "lock.open", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))? button.setImage(UIImage(systemName: "lock.open", withConfiguration: UIImage.SymbolConfiguration(pointSize: 48))?
@ -295,21 +295,12 @@ class VLCPlayerViewController: UIViewController {
} }
@objc @objc
private func didTap() { private func didTap(_ gestureRecognizer: UITapGestureRecognizer) {
didGenerallyTap() didGenerallyTap(point: gestureRecognizer.location(in: mainGestureView))
} }
@objc @objc
private func didDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) { func didLongPress() {
if gestureRecognizer.location(in: mainGestureView).x > (mainGestureView.frame.width / 2) {
didSelectForward()
} else {
didSelectBackward()
}
}
@objc
private func didLongPress() {
guard !isGesturesLocked else { return } guard !isGesturesLocked else { return }
isGesturesLocked = true isGesturesLocked = true
didGenerallyTap() didGenerallyTap()
@ -647,9 +638,10 @@ extension VLCPlayerViewController {
guard let overlayHostingController = currentOverlayHostingController else { return } guard let overlayHostingController = currentOverlayHostingController else { return }
guard overlayHostingController.view.alpha != 1 else { return } guard overlayHostingController.view.alpha != 1 else { return }
overlayHostingController.view.alpha = 1
UIView.animate(withDuration: 0.2) { withAnimation(.easeInOut(duration: 0.2)) { [weak self] in
overlayHostingController.view.alpha = 1 self?.viewModel.isHiddenOverlay = false
} }
} }
@ -660,8 +652,16 @@ extension VLCPlayerViewController {
guard overlayHostingController.view.alpha != 0 else { return } 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 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 { if isGesturesLocked {
toggleLockedOverlay() toggleLockedOverlay()
} else { } 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() toggleOverlay()
} }
restartOverlayDismissTimer(interval: 5) 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() { func didBeginScrubbing() {
stopOverlayDismissTimer() stopOverlayDismissTimer()
} }