You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 06:55:11