From 28d3f295715e4196a78b2f5d6ced840acef94bf0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 6 Aug 2022 15:27:34 +0200 Subject: [PATCH] Improve fullscreen animation --- Model/Player/PlayerModel.swift | 2 +- Shared/Player/PlayerBackendView.swift | 98 ++++++++++ Shared/Player/VideoPlayerSizeModifier.swift | 4 +- Shared/Player/VideoPlayerView.swift | 193 ++++++++------------ Yattee.xcodeproj/project.pbxproj | 10 +- 5 files changed, 184 insertions(+), 123 deletions(-) create mode 100644 Shared/Player/PlayerBackendView.swift diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 8cd6b285..cd3ac275 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -811,7 +811,7 @@ final class PlayerModel: ObservableObject { } func toggleFullscreen(_ isFullScreen: Bool) { - controls.resetTimer() + controls.presentingControls = false #if os(macOS) if isFullScreen { diff --git a/Shared/Player/PlayerBackendView.swift b/Shared/Player/PlayerBackendView.swift new file mode 100644 index 00000000..92808ba7 --- /dev/null +++ b/Shared/Player/PlayerBackendView.swift @@ -0,0 +1,98 @@ +import SwiftUI + +struct PlayerBackendView: View { + #if os(iOS) + @Environment(\.verticalSizeClass) private var verticalSizeClass + #endif + @EnvironmentObject private var player + @EnvironmentObject private var thumbnails + + var body: some View { + ZStack(alignment: .top) { + Group { + switch player.activeBackend { + case .mpv: + player.mpvPlayerView + case .appleAVPlayer: + player.avPlayerView + #if os(iOS) + .onAppear { + player.pipController = .init(playerLayer: player.playerLayerView.playerLayer) + let pipDelegate = PiPDelegate() + pipDelegate.player = player + + player.pipDelegate = pipDelegate + player.pipController?.delegate = pipDelegate + player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer + } + #endif + } + } + .overlay(GeometryReader { proxy in + Color.clear + .onAppear { + player.playerSize = proxy.size + } + .onChange(of: proxy.size) { _ in player.playerSize = proxy.size } + .onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size } + .onChange(of: player.aspectRatio) { _ in player.playerSize = proxy.size } + }) + #if os(iOS) + .padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0) + #endif + + #if !os(tvOS) + PlayerGestures() + PlayerControls(player: player, thumbnails: thumbnails) + #if os(iOS) + .padding(.top, controlsTopPadding) + .padding(.bottom, controlsBottomPadding) + #endif + #endif + } + #if os(iOS) + .statusBarHidden(fullScreenLayout) + #endif + } + + var fullScreenLayout: Bool { + #if os(iOS) + player.playingFullScreen || verticalSizeClass == .compact + #else + player.playingFullScreen + #endif + } + + #if os(iOS) + var controlsTopPadding: Double { + guard fullScreenLayout else { return 0 } + + if UIDevice.current.userInterfaceIdiom != .pad { + return verticalSizeClass == .compact ? safeAreaInsets.top : 0 + } else { + return safeAreaInsets.top.isZero ? safeAreaInsets.bottom : safeAreaInsets.top + } + } + + var controlsBottomPadding: Double { + guard fullScreenLayout else { return 0 } + + if UIDevice.current.userInterfaceIdiom != .pad { + return fullScreenLayout && verticalSizeClass == .compact ? safeAreaInsets.bottom : 0 + } else { + return fullScreenLayout ? safeAreaInsets.bottom : 0 + } + } + + var safeAreaInsets: UIEdgeInsets { + UIApplication.shared.windows.first?.safeAreaInsets ?? .init() + } + #endif +} + +struct PlayerBackendView_Previews: PreviewProvider { + static var previews: some View { + PlayerBackendView() + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Player/VideoPlayerSizeModifier.swift b/Shared/Player/VideoPlayerSizeModifier.swift index f7cc76ac..5c9290f9 100644 --- a/Shared/Player/VideoPlayerSizeModifier.swift +++ b/Shared/Player/VideoPlayerSizeModifier.swift @@ -37,12 +37,12 @@ struct VideoPlayerSizeModifier: ViewModifier { return VideoPlayerView.defaultAspectRatio } - return [aspectRatio, VideoPlayerView.defaultAspectRatio].min()! + return aspectRatio } var usedAspectRatioContentMode: ContentMode { #if os(iOS) - !fullScreen ? .fit : .fill + fullScreen ? .fill : .fit #else .fit #endif diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index e5685e92..65601bf5 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -95,6 +95,9 @@ struct VideoPlayerView: View { playerSize = geometry.size } } + #if os(iOS) + .frame(width: playerWidth.isNil ? nil : Double(playerWidth!), height: playerHeight.isNil ? nil : Double(playerHeight!)) + #endif .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea) .onChange(of: geometry.size) { size in self.playerSize = size @@ -141,25 +144,32 @@ struct VideoPlayerView: View { #endif } - var playerEdgesIgnoringSafeArea: Edge.Set { - #if os(iOS) + #if os(iOS) + var playerWidth: Double? { + fullScreenLayout ? (UIScreen.main.bounds.size.width - safeAreaInsets.left - safeAreaInsets.right) : nil + } + + var playerHeight: Double? { + fullScreenLayout ? UIScreen.main.bounds.size.height - (OrientationTracker.shared.currentInterfaceOrientation.isPortrait ? (safeAreaInsets.top + safeAreaInsets.bottom) : 0) : nil + } + + var playerEdgesIgnoringSafeArea: Edge.Set { if fullScreenLayout, UIDevice.current.orientation.isLandscape { return [.vertical] } - #endif - return [] - } + return [] + } + #endif var content: some View { Group { ZStack(alignment: .bottomLeading) { #if os(tvOS) ZStack { - playerView + PlayerBackendView() tvControls } - .ignoresSafeArea(.all, edges: .all) .onMoveCommand { direction in if direction == .up || direction == .down { playerControls.show() @@ -193,17 +203,16 @@ struct VideoPlayerView: View { if player.playingInPictureInPicture { pictureInPicturePlaceholder } else { - playerView - + PlayerBackendView() #if !os(tvOS) - .modifier( - VideoPlayerSizeModifier( - geometry: geometry, - aspectRatio: player.aspectRatio, - fullScreen: fullScreenLayout + .modifier( + VideoPlayerSizeModifier( + geometry: geometry, + aspectRatio: player.aspectRatio, + fullScreen: fullScreenLayout + ) ) - ) - .overlay(playerPlaceholder) + .overlay(playerPlaceholder) #endif } } @@ -239,8 +248,8 @@ struct VideoPlayerView: View { VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) #endif } - #if !os(macOS) - .transition(.move(edge: .bottom)) + #if os(iOS) + .transition(.asymmetric(insertion: .opacity, removal: .identity)) #endif .background(colorScheme == .dark ? Color.black : Color.white) .modifier(VideoDetailsPaddingModifier( @@ -279,102 +288,6 @@ struct VideoPlayerView: View { #endif } - var playerView: some View { - ZStack(alignment: .top) { - Group { - switch player.activeBackend { - case .mpv: - player.mpvPlayerView - case .appleAVPlayer: - player.avPlayerView - #if os(iOS) - .onAppear { - player.pipController = .init(playerLayer: player.playerLayerView.playerLayer) - let pipDelegate = PiPDelegate() - pipDelegate.player = player - - player.pipDelegate = pipDelegate - player.pipController?.delegate = pipDelegate - player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer - } - #endif - } - } - .overlay(GeometryReader { proxy in - Color.clear - .onAppear { player.playerSize = proxy.size } - .onChange(of: proxy.size) { _ in player.playerSize = proxy.size } - .onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size } - .onChange(of: player.aspectRatio) { _ in player.playerSize = proxy.size } - }) - #if os(iOS) - .padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0) - #endif - - #if !os(tvOS) - PlayerGestures() - PlayerControls(player: player, thumbnails: thumbnails) - #if os(iOS) - .padding(.top, controlsTopPadding) - .padding(.bottom, fullScreenLayout ? safeAreaInsets.bottom : 0) - #endif - #endif - } - #if os(iOS) - .statusBarHidden(fullScreenLayout) - #endif - } - - #if os(iOS) - var playerDragGesture: some Gesture { - DragGesture(minimumDistance: 0, coordinateSpace: .global) - .onChanged { value in - guard player.presentingPlayer, - !playerControls.presentingControlsOverlay else { return } - - let drag = value.translation.height - - guard drag > 0 else { return } - - if drag > 60, - player.playingFullScreen, - !OrientationTracker.shared.currentInterfaceOrientation.isLandscape - { - player.exitFullScreen() - player.lockedOrientation = nil - } - - viewVerticalOffset = drag - } - .onEnded { _ in - guard player.presentingPlayer, - !playerControls.presentingControlsOverlay else { return } - if viewVerticalOffset > 100 { - player.backend.setNeedsDrawing(false) - player.hide() - player.exitFullScreen() - } else { - viewVerticalOffset = 0 - player.backend.setNeedsDrawing(true) - player.show() - } - } - } - - var controlsTopPadding: Double { - guard fullScreenLayout else { return 0 } - - let idiom = UIDevice.current.userInterfaceIdiom - guard idiom == .pad else { return 0 } - - return safeAreaInsets.top.isZero ? safeAreaInsets.bottom : safeAreaInsets.top - } - - var safeAreaInsets: UIEdgeInsets { - UIApplication.shared.windows.first?.safeAreaInsets ?? .init() - } - #endif - var fullScreenLayout: Bool { #if os(iOS) player.playingFullScreen || verticalSizeClass == .compact @@ -450,6 +363,49 @@ struct VideoPlayerView: View { } #if os(iOS) + var playerDragGesture: some Gesture { + DragGesture(minimumDistance: 0, coordinateSpace: .global) + .onChanged { value in + guard player.presentingPlayer, + !playerControls.presentingControlsOverlay else { return } + + if player.controls.presentingControls { + player.controls.presentingControls = false + } + + let drag = value.translation.height + + guard drag > 0 else { return } + + if drag > 60, + player.playingFullScreen, + !OrientationTracker.shared.currentInterfaceOrientation.isLandscape + { + player.exitFullScreen() + player.lockedOrientation = nil + } + + viewVerticalOffset = drag + } + .onEnded { _ in + guard player.presentingPlayer, + !playerControls.presentingControlsOverlay else { return } + if viewVerticalOffset > 100 { + player.backend.setNeedsDrawing(false) + player.hide() + player.exitFullScreen() + } else { + viewVerticalOffset = 0 + player.backend.setNeedsDrawing(true) + player.show() + } + } + } + + var safeAreaInsets: UIEdgeInsets { + UIApplication.shared.windows.first?.safeAreaInsets ?? .init() + } + private func configureOrientationUpdatesBasedOnAccelerometer() { if OrientationTracker.shared.currentInterfaceOrientation.isLandscape, Defaults[.enterFullscreenInLandscape], @@ -457,6 +413,7 @@ struct VideoPlayerView: View { !player.playingInPictureInPicture { DispatchQueue.main.async { + player.controls.presentingControls = false player.enterFullScreen() } } @@ -488,14 +445,12 @@ struct VideoPlayerView: View { } if orientation.isLandscape { + player.controls.presentingControls = false player.enterFullScreen() Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) } else { - if !player.playingFullScreen { - player.exitFullScreen() - } else { - Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) - } + player.exitFullScreen() + Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) } } } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index bead38af..6526c232 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -340,6 +340,9 @@ 375E45F627B1976B00BA7902 /* MPVOGLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F427B1976B00BA7902 /* MPVOGLView.swift */; }; 375E45F827B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */; }; 375E45F927B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */; }; + 375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375F740F289DC35A00747050 /* PlayerBackendView.swift */; }; + 375F7411289DC35A00747050 /* PlayerBackendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375F740F289DC35A00747050 /* PlayerBackendView.swift */; }; + 375F7412289DC35A00747050 /* PlayerBackendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375F740F289DC35A00747050 /* PlayerBackendView.swift */; }; 3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; }; 3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; }; 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */; }; @@ -1033,6 +1036,7 @@ 375DFB5726F9DA010013F468 /* InstancesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModel.swift; sourceTree = ""; }; 375E45F427B1976B00BA7902 /* MPVOGLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVOGLView.swift; sourceTree = ""; }; 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsModel.swift; sourceTree = ""; }; + 375F740F289DC35A00747050 /* PlayerBackendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackendView.swift; sourceTree = ""; }; 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = ""; }; 37648B68286CF5F1003D330B /* TVControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVControls.swift; sourceTree = ""; }; @@ -1491,7 +1495,9 @@ 371B7E602759706A00D21217 /* CommentsView.swift */, 37EF9A75275BEB8E0043B585 /* CommentView.swift */, 37DD9DA22785BBC900539416 /* NoCommentsView.swift */, + 375F740F289DC35A00747050 /* PlayerBackendView.swift */, 3703100127B0713600ECDDAA /* PlayerGestures.swift */, + 373031F22838388A000CFD59 /* PlayerLayerView.swift */, 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */, 37CC3F4B270CFE1700608308 /* PlayerQueueView.swift */, 373197D82732015300EF734F /* RelatedView.swift */, @@ -1501,7 +1507,6 @@ 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */, 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */, - 373031F22838388A000CFD59 /* PlayerLayerView.swift */, ); path = Player; sourceTree = ""; @@ -2789,6 +2794,7 @@ 37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */, 37030FFF27B04DCC00ECDDAA /* PlayerControls.swift in Sources */, 374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */, + 375F7410289DC35A00747050 /* PlayerBackendView.swift in Sources */, 37FB28412721B22200A57617 /* ContentItem.swift in Sources */, 37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */, @@ -3008,6 +3014,7 @@ 3748186726A7627F0084E870 /* Video+Fixtures.swift in Sources */, 3784B23E2728B85300B09468 /* ShareButton.swift in Sources */, 37BE0BDA26A214630092E2DB /* AppleAVPlayerViewController.swift in Sources */, + 375F7411289DC35A00747050 /* PlayerBackendView.swift in Sources */, 37FEF11427EFD8580033912F /* PlaceholderCell.swift in Sources */, 37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */, 374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */, @@ -3226,6 +3233,7 @@ 37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */, 370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */, 376A33E62720CB35000C1D6B /* Account.swift in Sources */, + 375F7412289DC35A00747050 /* PlayerBackendView.swift in Sources */, 37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */, 37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */, 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,