SwiftUI子视图视频播放器实现点击全屏及返回功能问询
SwiftUI实现ScrollView内视频平滑全屏切换(保持播放进度)
要实现ScrollView内视频点击全屏、下拉返回原位置且保持播放进度的效果,核心在于共享播放器实例、跨视图状态管理和基于原位置的平滑动画,以下是具体实现步骤:
1. 全局播放器状态管理
创建一个ObservableObject统一管理播放器状态、全屏状态以及视频的位置信息,确保跨视图层级的数据同步:
import SwiftUI import AVKit class PlayerManager: ObservableObject { @Published var isFullScreen = false @Published var player: AVPlayer? @Published var videoFrame: CGRect = .zero // 记录视频在ScrollView中的全局位置和尺寸 @Published var videoAspect: CGSize = CGSize(width: 16, height: 9) }
2. 注入状态到视图层级
在顶层ContentView中注入PlayerManager,让所有子视图共享同一状态:
struct ContentView: View { @StateObject private var playerManager = PlayerManager() var body: some View { SomeView() .environmentObject(playerManager) } }
3. 修改ScrollView内的视频视图
更新VideoPlayerView,通过GeometryReader捕获视频的全局位置,点击时触发全屏状态,并共享播放器实例:
struct SomeView: View { let videoUrl = URL(string: "你的视频URL")! var body: some View { ScrollView { // 示例占位视图 Text("上方内容") .frame(height: 200) .background(Color.gray) VideoPlayerView(url: videoUrl) // 示例占位视图 Text("下方内容") .frame(height: 500) .background(Color.gray) } } } struct VideoPlayerView: View { let url: URL @EnvironmentObject var playerManager: PlayerManager @State private var isLoaded = false var body: some View { ZStack { if let player = playerManager.player { VideoPlayer(player: .constant(player), showControls: false) .aspectRatio(playerManager.videoAspect, contentMode: .fit) .onTapGesture { playerManager.isFullScreen = true } } } .background( // 捕获视频的全局位置和尺寸 GeometryReader { proxy in Color.clear .onAppear { playerManager.videoFrame = proxy.frame(in: .global) } .onChange(of: proxy.frame(in: .global)) { newFrame in if !playerManager.isFullScreen { playerManager.videoFrame = newFrame } } } ) .onAppear { guard !isLoaded else { return } isLoaded = true let player = AVPlayer(url: url) playerManager.player = player // 获取视频宽高比 Task { do { if let aspect = try await getVideoResolution(url: url.absoluteString) { playerManager.videoAspect = aspect } } catch { print("获取视频分辨率失败: \(error)") } } player.play() } } }
4. 实现全屏播放器与平滑动画
在顶层添加全屏播放器视图,通过动画过渡实现从原位置到全屏的切换,同时添加下拉手势支持返回:
struct ContentView: View { @StateObject private var playerManager = PlayerManager() var body: some View { ZStack { SomeView() .environmentObject(playerManager) // 全屏播放器视图 if playerManager.isFullScreen, let player = playerManager.player { FullScreenVideoPlayer(player: player) .transition(.asymmetric( insertion: .opacity.animation(.easeInOut(duration: 0.3)), removal: .opacity.animation(.easeInOut(duration: 0.3)) )) .zIndex(1000) } } } } struct FullScreenVideoPlayer: View { let player: AVPlayer @EnvironmentObject var playerManager: PlayerManager @State private var dragOffset: CGSize = .zero var body: some View { ZStack { Color.black.edgesIgnoringSafeArea(.all) VideoPlayer(player: .constant(player), showControls: true) .frame( width: UIScreen.main.bounds.width + dragOffset.width, height: UIScreen.main.bounds.height + dragOffset.height ) .scaleEffect(1 - (dragOffset.height / UIScreen.main.bounds.height) * 0.3) .offset(y: dragOffset.height) .gesture( DragGesture() .onChanged { value in dragOffset = value.translation } .onEnded { value in if value.translation.height > 100 { // 下拉距离足够,退出全屏 withAnimation(.easeInOut(duration: 0.3)) { playerManager.isFullScreen = false } } // 重置偏移 dragOffset = .zero } ) // 顶部退出提示(可选) VStack { Text("下拉退出全屏") .foregroundColor(.white) .padding() .background(Color.black.opacity(0.5)) .cornerRadius(8) Spacer() } .opacity(dragOffset.height > 50 ? 1 : 0) .animation(.easeInOut, value: dragOffset.height) } .onTapGesture { // 点击空白处切换播放/暂停 if player.timeControlStatus == .playing { player.pause() } else { player.play() } } } }
5. 保留原有工具方法与UIKit桥接
原有的视频分辨率获取方法和VideoPlayer桥接结构可以直接保留,确保播放器正常工作:
func getVideoResolution(url: String) async throws -> CGSize? { guard let url = URL(string: url), let track = try await AVURLAsset(url: url).loadTracks(withMediaType: .video).first else { return nil } let naturalSize = try await track.load(.naturalSize) let transform = try await track.load(.preferredTransform) return naturalSize.applying(transform) } struct VideoPlayer : UIViewControllerRepresentable { @Binding var player : AVPlayer let showControls: Bool func makeUIViewController(context: Context) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player controller.showsPlaybackControls = showControls controller.allowsVideoFrameAnalysis = false return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { } }
关键说明
- 共享播放器实例:通过
PlayerManager统一管理AVPlayer对象,确保全屏前后使用同一个播放器,播放进度完全连续。 - 位置捕获:使用
GeometryReader获取视频在全局坐标系中的位置,动画时以此为起点过渡到全屏。 - 平滑动画:通过
transition、scaleEffect和offset组合实现自然的缩放和平移动画,下拉手势的阈值判断确保交互流畅。
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

