手把手教你实现视频列表滚动自动播放-短视频列表滚动播放实战

本文始发于我的博文手把手教你实现视频列表滚动自动播放-短视频列表滚动播放实战,现转发至此。

目录

  • 前言
  • 方案实现
    • 原始需求
    • 隐藏需求
    • 方案制定
    • 具体实现
  • 总结
  • 附录

前言

互联网内容已经逐渐从图文阅读往如今火热的短视频更迭,某种程度上短视频有着图文所没有的优势和不可替代性,降低了自我表达的门槛。近期迭代做了个短视频列表滚动自动播放的需求,上线了一段时间。觉得略有趣,简单分享下方案。

本文提供了 Demo ,将方案进行简化处理,只包含核心的功能实现。

本文的 Demo 附在文末

方案实现

demo.gif

介绍下整个方案的思考和实现的一些过程。

原始需求

简化如下:

  • 自动播放条件

Wifi环境下,当视频中心位置从下往上越过屏幕的2/3位置,或从上往下越过屏幕的1/3位置时,视频开始自动播放;

  • 停止播放条件

当视频中心位置离开可见区域时,视频自动停止播放。若下一个视频满足开始自动播放的条件,则上一个视频自动停止播放。

  • 手动播放

点播放按钮可手动开始播放。开始播放某视频后,其他视频停止播放。

隐藏需求

原始需求背后需要考虑的其他情况:

  • 页面进入/离开的处理
  • App进入后台/返回前台的处理
  • 从一个视频列表跳转到另一个视频列表的处理
  • 页面包含安全区的处理
  • 列表滚动播放的性能问题
  • 视频循环播放/静音功能
  • 上拉加载更多/下拉刷新等动作触发的滚动的处理
  • 列表 cell 的复用问题

方案制定

主要考虑了以下一些方面:

  1. 优先考虑的是性能问题,同一个列表中尽量只有一个视频控件
  2. 尽量降低方案的侵入性,目前有多个现有列表需要支持该功能,涉及到多个 controller 和多个 cell ,侵入性低也利于改动和维护
  3. 隐藏需求会影响到自动播放逻辑的调用时机

大致思路如下:

  1. 由一个类管理整个 App 的视频滚动播放的相关逻辑,包括视频组件。
  2. 由 controller 监控滚动,触发管理类进行处理。管理类计算当前符合自动播放的视频,播放并将视频组件嵌入 cell 中。
  3. 为了降低侵入性,采用协议实现管理类和 controller、管理类和 cell 间的通信。也利于改动逻辑时只改动到管理类,而不是牵涉各个调用处和 cell。

隐藏需求处理:

  1. 页面进入/离开的处理、从一个视频列表跳转到另一个视频列表的处理

在 controller 的生命周期中去调用管理类方法进行处理。离开时停止当前播放中的视频,进入时播放当前列表的视频。进入时判断数据源是否已经获取,已经获取则调用播放。另外在数据源获取后判断是否已经 Appear,是则调用播放。用变量标志避免两个逻辑重复调用。

  1. App进入后台/返回前台的处理

App 进入后台/返回前台不会调用 controller 的生命周期,需要另外处理。

  1. 页面包含安全区的处理

在计算视频的相对位置时,将安全区考虑在内进行计算。

  1. 列表滚动播放的性能问题

测试列表滚动停止时实时滚动时滚动降速到一定速度时等情况下,调用自动播放逻辑的性能和体验,

  1. 视频循环播放/静音功能

视频播放完毕的事件需要通过监听 NSNotification.Name.AVPlayerItemDidPlayToEndTime 实现,放在管理类中进行处理。

  1. 上拉加载更多/下拉刷新等动作触发的滚动的处理

在这两种情况下,需要停止页面的逻辑调用,直到数据源返回成功或失败为止。

  1. 列表 cell 的复用问题

复用时需要重新布置frame、清空数据源等。

具体实现

  1. 定义管理类 VideoListAutoPlayManager
class VideoListAutoPlayManager {
    
    private init() {
        playerVC.player = AVPlayer()
        playerVC.showsPlaybackControls = false
        playerVC.view.backgroundColor = UIColor.clear
    }
    static let shared = VideoListAutoPlayManager()
    
    private var playerVC: AVPlayerViewController = AVPlayerViewController()
    private var preOffsetY: CGFloat = 0
    private var currentPlayingView: VideoPlayable?
}

需要保存一些信息和状态,所以定义成单例。

AVPlayerViewController 自带控制条,需要隐藏。

视频播放时背景从黑色开始,会导致出现先看到封面,然后黑色,然后再播放视频的问题,设置为透明会让从封面到视频的过渡自然。

preOffsetY 记录当前滚动的 UIScrollView 的 contentOffset.y 。用于在多个视频满足自动播放时,通过判断滚动方向来决定选取哪个视频自动播放。

currentPlayingView 记录当前播放中的 cell。用于通知上一个播放的 cell 即将停止播放视频,方便 cell 处理另外的逻辑。

  1. 定义协议
protocol VideoPlayable: UIView {
    var viewToContainVideo: UIView {get}
    var urlToPlay: URL? {get}
    func videoStatusChanged(changeTo isPlaying: Bool)
}

protocol VideoListPlayable: UIScrollView {
    var visibleViews: [VideoPlayable] {get}
}

extension UITableView: VideoListPlayable {
    var visibleViews: [VideoPlayable] {
        let views: [VideoPlayable] = visibleCells.compactMap({ $0 as? VideoPlayable })
        return views
    }
}
extension UICollectionView: VideoListPlayable {
    var visibleViews: [VideoPlayable] {
        let views: [VideoPlayable] = visibleCells.compactMap({ $0 as? VideoPlayable })
        return views
    }
}

第一个协议,VideoPlayable,是存放视频的 cell 需要实现的。实现协议返回需要包含视频的 view ,需要播放的视频 URL,以及用于 VideoListAutoPlayManager 通知 cell 处理视频播放状态变化的调用方法。

第二个协议,VideoListPlayable,是滚动列表需要实现的。实现协议返回滚动列表当前可见的 cell,用于 VideoListPlayable 去判断哪些视频需要自动播放。

两个协议都遵循某个类,UIView 或 UIScrollView,是有些取巧,方便后面取 frame 等。也可以不遵循,然后在协议中返回需要的数据即可。

另外为 UITableView 和 UICollectionView 做了默认实现。

  1. 触发滚动播放的处理
func scrollViewDidScroll(_ scrollView: VideoListPlayable) {

    let currentOffsetY = scrollView.contentOffset.y
    let minY = scrollView.frame.height / 3
    let maxY = minY * 2
    // 获取在 scrollView 自动播放区域内的视频
    let autoPlayableViews = scrollView.visibleViews.filter { view in
        guard let relativeRect = relativeRect(view: view.viewToContainVideo, relativeTo: scrollView), view.urlToPlay != nil else {return false}
        let containerCenterY = relativeRect.minY + relativeRect.height / 2
        return (containerCenterY > minY && containerCenterY < maxY)
    }

    guard let first = autoPlayableViews.first else {
        // 没有需要自动播放的视频
        // 移除当前正在离开/已经离开屏幕的视频
        removeCurrentVideoIfLeavingScreen(scrollView: scrollView)
        preOffsetY = currentOffsetY
        return
    }

    // 取出需要自动播放的视频
    let viewToPlay: VideoPlayable = autoPlayableViews.reduce(first) { (result, view) in
        let isScrollToUpper = currentOffsetY < preOffsetY
        return result.frame.maxY > view.frame.maxY ? (isScrollToUpper ? view : result) : (isScrollToUpper ? result : view)
    }
    if let currentPlayingView = currentPlayingView, viewToPlay as UIView == currentPlayingView {
        // 满足条件的视频正在播放中
        preOffsetY = currentOffsetY
        return
    }
    removeCurrentVideo(on: scrollView)

    addPlayerView(to: viewToPlay, on: scrollView)
    preOffsetY = currentOffsetY
}

VideoListAutoPlayManager 提供该方法用于 controller 需要进行视频自动播放处理时进行调用。

外部可以自行决定在什么时机,进行视频自动播放逻辑的触发,不需要是在 scrollViewDidScroll 的时机。

该方法主要逻辑是:

取出当前可见区域中,满足自动播放条件(func relativeRect(view: UIView, relativeTo scrollView: VideoListPlayable) -> CGRect?)的 cell,即相对位置为滚动列表的 1/3 至 2/3 的位置。

如果没有满足条件的,则判断当前是否有播放中的视频,且视频即将或已经离开屏幕,有则停止播放视频,并通知 cell。

如果有满足条件的视频,则根据滚动方向选取视频(列表向上滚动时,播放靠下的视频,反之则播放靠上的视频),移除上一个播放中的视频(通知对应的 cell),切换视频源并播放,通知最新播放的 cell。

  1. 手动播放的处理
func play(at videoView: VideoPlayable, on scrollView: VideoListPlayable) {
    removeCurrentVideo(on: scrollView)
    addPlayerView(to: videoView, on: scrollView)
}

即移除当前播放中的视频,并将当前手动指定播放的视频进行播放。

  1. 添加视频组件
func addPlayerView(to view: VideoPlayable, on scrollView: VideoListPlayable) {

    guard let url = view.urlToPlay else {
        return
    }

    let avItem = AVPlayerItem(url: url)
    let avPlayer = AVPlayer(playerItem: avItem)
    playerVC.player = avPlayer
    avPlayer.isMuted = true
    avPlayer.play()

    view.videoStatusChanged(changeTo: true)

    let containerView = view.viewToContainVideo
    containerView.addSubview(playerVC.view)

    playerVC.view.translatesAutoresizingMaskIntoConstraints = false
    playerVC.view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
    playerVC.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
    playerVC.view.leftAnchor.constraint(equalTo: containerView.leftAnchor).isActive = true
    playerVC.view.rightAnchor.constraint(equalTo: containerView.rightAnchor).isActive = true

    currentPlayingView = view
}

通过协议获取包含视频的 view,将视频放入其中,通知 cell 进行状态变化处理。

  1. 隐藏需求的实现

基本按照上一个部分讲的思路实现,没有将这部分代码放到 Demo 中。

总结

目前的方案,cell只需实现协议,添加一个用于包含视频的 view 即可。这样降低了对原代码的侵入性、减少修改和维护的成本,可随时去除该自动播放的特性。另外隐藏需求实际花费的思考和时间会比原始需求多些,需要考虑很多细节。

附录

本文Demo:VideoListPlayDemo

-END-
欢迎到我的博客交流:https://zackzheng.info

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容