天天看點

android短視訊清單自動播放,手把手教你實作視訊清單滾動自動播放-短視訊清單滾動播放實戰...

前言

方案實作

原始需求

隐藏需求

方案制定

具體實作

總結

附錄

前言

網際網路内容已經逐漸從圖文閱讀往如今火熱的短視訊更疊,某種程度上短視訊有着圖文所沒有的優勢和不可替代性,降低了自我表達的門檻。近期疊代做了個短視訊清單滾動自動播放的需求,上線了一段時間。覺得略有趣,簡單分享下方案。

本文提供了 Demo ,将方案進行簡化處理,隻包含核心的功能實作。

本文的 Demo 附在文末

方案實作

android短視訊清單自動播放,手把手教你實作視訊清單滾動自動播放-短視訊清單滾動播放實戰...

介紹下整個方案的思考和實作的一些過程。

原始需求

簡化如下:

自動播放條件

Wifi環境下,當視訊中心位置從下往上越過螢幕的2/3位置,或從上往下越過螢幕的1/3位置時,視訊開始自動播放;

停止播放條件

當視訊中心位置離開可見區域時,視訊自動停止播放。若下一個視訊滿足開始自動播放的條件,則上一個視訊自動停止播放。

手動播放

點播放按鈕可手動開始播放。開始播放某視訊後,其他視訊停止播放。

隐藏需求

原始需求背後需要考慮的其他情況:

頁面進入/離開的處理

App進入背景/傳回前台的處理

從一個視訊清單跳轉到另一個視訊清單的處理

頁面包含安全區的處理

清單滾動播放的性能問題

視訊循環播放/靜音功能

上拉加載更多/下拉重新整理等動作觸發的滾動的處理

清單 cell 的複用問題

方案制定

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

優先考慮的是性能問題,同一個清單中盡量隻有一個視訊控件

盡量降低方案的侵入性,目前有多個現有清單需要支援該功能,涉及到多個 controller 和多個 cell ,侵入性低也利于改動和維護

隐藏需求會影響到自動播放邏輯的調用時機

大緻思路如下:

由一個類管理整個 App 的視訊滾動播放的相關邏輯,包括視訊元件。

由 controller 監控滾動,觸發管理類進行處理。管理類計算目前符合自動播放的視訊,播放并将視訊元件嵌入 cell 中。

為了降低侵入性,采用協定實作管理類和 controller、管理類和 cell 間的通信。也利于改動邏輯時隻改動到管理類,而不是牽涉各個調用處和 cell。

隐藏需求處理:

頁面進入/離開的處理、從一個視訊清單跳轉到另一個視訊清單的處理

在 controller 的生命周期中去調用管理類方法進行處理。離開時停止目前播放中的視訊,進入時播放目前清單的視訊。進入時判斷資料源是否已經擷取,已經擷取則調用播放。另外在資料源擷取後判斷是否已經 Appear,是則調用播放。用變量标志避免兩個邏輯重複調用。

App進入背景/傳回前台的處理

App 進入背景/傳回前台不會調用 controller 的生命周期,需要另外處理。

頁面包含安全區的處理

在計算視訊的相對位置時,将安全區考慮在内進行計算。

清單滾動播放的性能問題

測試 清單滾動停止時 、 實時滾動時 、 滾動降速到一定速度時 等情況下,調用自動播放邏輯的性能和體驗,

視訊循環播放/靜音功能

視訊播放完畢的事件需要通過監聽 NSNotification.Name.AVPlayerItemDidPlayToEndTime 實作,放在管理類中進行處理。

上拉加載更多/下拉重新整理等動作觸發的滾動的處理

在這兩種情況下,需要停止頁面的邏輯調用,直到資料源傳回成功或失敗為止。

清單 cell 的複用問題

複用時需要重新布置frame、清空資料源等。

具體實作

定義管理類 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 處理另外的邏輯。

定義協定

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 做了預設實作。

觸發滾動播放的處理

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 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。

手動播放的處理

func play(at videoView: VideoPlayable, on scrollView: VideoListPlayable) {

removeCurrentVideo(on: scrollView)

addPlayerView(to: videoView, on: scrollView)

}

即移除目前播放中的視訊,并将目前手動指定播放的視訊進行播放。

添加視訊元件

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 進行狀态變化處理。

隐藏需求的實作

基本按照上一個部分講的思路實作,沒有将這部分代碼放到 Demo 中。

總結

目前的方案,cell隻需實作協定,添加一個用于包含視訊的 view 即可。這樣降低了對原代碼的侵入性、減少修改和維護的成本,可随時去除該自動播放的特性。另外隐藏需求實際花費的思考和時間會比原始需求多些,需要考慮很多細節。