在上一篇文章AndroidX Media3之ExoPlayer簡單使用(1)中介紹了ExoPlayer的簡單使用,運用了media3-ui包中提供的關于ExoPlayer的UI組件和資源。但是在日常開發(fā)中,播放器的界面會被要求為各式各樣的,沒有辦法使用media3-ui包中提供的通用界面。
在這篇文章將介紹如何自己實現一個簡單的PlayerView。
demo下載
看一下最終的效果界面圖:

依賴項
在上一篇文章的依賴項基礎上,可以去除media3-ui的依賴,只需要一個androidx.media3:media3-exoplayer:1.0.0-beta02依賴即可。
基本要素
通過查看media3-ui庫中的PlayerView實現,可以看到自定義播放界面需要的一些基本要素:
private final ComponentListener componentListener;
@Nullable private final AspectRatioFrameLayout contentFrame;
@Nullable private final View shutterView;
@Nullable private final View surfaceView;
private final boolean surfaceViewIgnoresVideoAspectRatio;
@Nullable private final ImageView artworkView;
@Nullable private final SubtitleView subtitleView;
@Nullable private final View bufferingView;
@Nullable private final TextView errorMessageView;
@Nullable private final PlayerControlView controller;
componentListener:用于播放事件的監(jiān)聽,包括有播放狀態(tài)、播放異常情況等。
contentFrame:播放界面的寬高比例大小等。
surfaceView:用于渲染視頻的Surface。
bufferingView:視頻緩沖時顯示。
errorMessageView:視頻播放異常時顯示。
controller:視頻播放控制界面,包括播放暫停操作、播放進度操作等。
播放事件的監(jiān)聽
通過player.addListener添加一個Player.Listener進行播放事件的監(jiān)聽。Player.Listener有空的默認方法,因此按需實現所需要的方法即可。
我們所需要實現的方法主要有以下幾個:
- onVideoSizeChanged:獲取播放視頻的寬度和高度,用于更新播放界面的寬高比例大小
- onPlayerError:播放異常情況的監(jiān)聽,用于展示錯誤界面
- onPlaybackStateChanged:播放狀態(tài)的監(jiān)聽,用于視頻緩沖界面展示、視頻播放控制界面
- onPlayWhenReadyChanged:播放暫停操作的監(jiān)聽,用于視頻播放控制界面
private inner class ComponentListener : Player.Listener {
override fun onVideoSizeChanged(videoSize: VideoSize) {
updateAspectRatio()
}
override fun onPlayerError(error: PlaybackException) {
updateErrorMessage()
}
override fun onPlaybackStateChanged(playbackState: @State Int) {
if (playbackState == Player.STATE_READY && visibility != View.VISIBLE) {
visibility = View.VISIBLE
}
updateProgressState()
}
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean, reason: @Player.PlayWhenReadyChangeReason Int
) {
//播放暫停會回調該方法,播放時playWhenReady為true
updateProgressState()
}
}
播放界面的顯示
media3-ui庫中的PlayerView對于視頻播放界面的顯示用了AspectRatioFrameLayout,我們可以直接復用這個布局,也可以自己簡單的實現一個。
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<FrameLayout
android:id="@+id/exo_content_frame"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="2:1"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/exo_error_message"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_gravity="center"
android:gravity="center"
android:textColor="@color/white"
android:visibility="gone"
android:text="刷新"
app:layout_constraintBottom_toBottomOf="@id/exo_content_frame"
app:layout_constraintEnd_toEndOf="@id/exo_content_frame"
app:layout_constraintStart_toStartOf="@id/exo_content_frame"
app:layout_constraintTop_toTopOf="@id/exo_content_frame" />
</androidx.constraintlayout.widget.ConstraintLayout>
使用ConstraintLayout作為一個根布局,利用constraintDimensionRatio屬性可以設置播放界面的寬高比例顯示。當拿到視頻的寬度和高度之后,將播放界面的寬高比例更改為視頻自身的寬高比。
private fun updateAspectRatio() {
player?.videoSize?.let { videoSize ->
if (videoSize.width > 0 && videoSize.height > 0) {
val width = videoSize.width
val height = videoSize.height
//更改比例,根據視頻自身寬高比展示
(binding.exoContentFrame.layoutParams as? LayoutParams)?.let {
it.dimensionRatio = "$width:$height"
binding.exoContentFrame.layoutParams = it
}
}
}
}
什么時候可以拿到視頻的寬度和高度呢?在上一篇文章中,通過player.addListener添加一個Player.Listener進行播放事件的監(jiān)聽,可以監(jiān)聽到播放狀態(tài)、播放異常情況等,此外還有onVideoSizeChanged方法可以獲取到視頻的寬度和高度,在該方法中更新播放界面的寬高比例即可。
用于渲染視頻的Surface
我們需要一個SurfaceView用于渲染視頻:
- 創(chuàng)建一個SurfaceView:
private val surfaceView by lazy { SurfaceView(context) }
- 將SurfaceView添加到布局上:
binding.exoContentFrame.addView(surfaceView, 0)
- 渲染視頻:
player.setVideoSurfaceView(surfaceView)
視頻緩沖時顯示
視頻的播放會需要緩沖時間,在上一篇文章中有介紹到播放監(jiān)聽onPlaybackStateChanged方法中會有視頻播放的四種播放狀態(tài):
- STATE_IDLE:初始狀態(tài),此時播放器沒有可以播放的資源,播放器停止播放或者播放失敗后也會處于該狀態(tài)
- STATE_BUFFERING: 沒有足夠的數據可以加載播放,此時無法立即播放
- STATE_READY : 播放器可以立即播放,是否播放取決于playWhenReady的值,該值表達了使用者的意愿,為true,將會開始播放,否則不播。
- STATE_ENDED: 播放完了所有的資源后處于該狀態(tài)
在監(jiān)聽到STATE_READY狀態(tài)為止之前就是視頻的緩沖時間,進行一個緩沖狀態(tài)的展示,例如loading,監(jiān)聽到STATE_READY狀態(tài)之后,隱藏緩沖狀態(tài)loading,展示視頻播放。
視頻播放異常時顯示
在播放監(jiān)聽onPlayerError方法中可以監(jiān)聽到播放異常情況,此時需要展示播放異常界面,展示錯誤信息和重新加載界面,當需要重新加載時調用player.prepare()方法。
視頻播放控制界面
視頻播放控制界面,包括播放暫停操作、播放進度操作等跟用戶進行互動的操作。
播放暫停操作
根據播放器當前狀態(tài)來進行播放或者暫停的操作,同時更新操作界面展示。
binding.tvPlay.setOnClickListener {
if (player.isPlaying) {
player.pause()
binding.tvPlay.text = "播放"
} else {
player.play()
binding.tvPlay.text = "暫停"
}
}
播放進度操作
播放進度是一個需要不斷自動更新的狀態(tài),在播放監(jiān)聽中onPlaybackStateChanged方法和onPlayWhenReadyChanged方法中我們都需要調用更新播放進度的方法。onPlaybackStateChanged是加載播放視頻到加載完成播放會回調該方法,onPlayWhenReadyChanged是視頻播放或者暫停操作會回調該方法。
private inner class ComponentListener : Player.Listener {
override fun onVideoSizeChanged(videoSize: VideoSize) {
updateAspectRatio()
}
override fun onPlayerError(error: PlaybackException) {
updateErrorMessage()
}
override fun onPlaybackStateChanged(playbackState: @State Int) {
if (playbackState == Player.STATE_READY && visibility != View.VISIBLE) {
visibility = View.VISIBLE
}
updateProgressState()
}
override fun onPlayWhenReadyChanged(
playWhenReady: Boolean, reason: @Player.PlayWhenReadyChangeReason Int
) {
//播放暫停會回調該方法,播放時playWhenReady為true
updateProgressState()
}
}
在更新播放進度方法中,我們通過handler不斷的發(fā)消息來進行進度的更新。當視頻不是播放狀態(tài)時自然也是不需要更新播放進度的。
/**
* 更新進度條狀態(tài)
*/
private fun updateProgressState() {
if (player?.playbackState == Player.STATE_READY && (player?.isPlaying == true)) {
progressHandler.removeCallbacksAndMessages(null)
progressHandler.sendEmptyMessage(1)
} else {
//清空進度
progressHandler.removeCallbacksAndMessages(null)
}
}
通過播放器相關方法可以獲取到視頻總時長、當前加載時長和當前播放時長,進而進行播放進度的更新和展示。
private val progressHandler = object : Handler(Looper.myLooper()!!) {
override fun handleMessage(msg: Message) {
val currentPlayer = player
//獲取進度并通知
if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) {
val currentPosition = currentPlayer.currentPosition.toInt()
val bufferedPosition = currentPlayer.bufferedPosition.toInt()
val duration = currentPlayer.duration.toInt()
progressChangeList.forEach {
it.onProgressChanged(currentPosition, bufferedPosition, duration)
}
//0.5秒后自動獲取進度
sendEmptyMessageDelayed(1, 500)
}
}
}
如果播放進度條可以進行拖拽從而達到操作播放進度,只要使用player.seekTo()方法就可以指定播放器播放進度。
資源釋放
當不再需要播放器時,記得釋放資源,由于使用了handler發(fā)消息來進行播放進度的更新,所以也需要對handler進行資源釋放:
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
//釋放資源
try {
player?.setVideoSurfaceView(null)
player?.stop()
player?.release()
player?.removeListener(componentListener)
progressHandler.removeCallbacksAndMessages(null)
} catch (e: Exception) {
e.printStackTrace()
}
}
前后臺切換播放優(yōu)化
如果不采取任何操作,在進行后臺切至前臺的操作時,會出現黑屏的情況,因此當切至后臺時,對播放狀態(tài)進行一個保存,并暫停播放器,當切回前臺時,恢復播放器之前的播放狀態(tài),可以在onWindowVisibilityChanged方法中進行該優(yōu)化操作:
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(visibility)
if (visibility == View.VISIBLE) {
if (playState) {
player?.play()
}
} else {
playState = (player?.isPlaying == true)
if (player?.isPlaying == true) {
player?.pause()
}
}
}