赞
踩
在上一篇文章AndroidX Media3之ExoPlayer简单使用(1)中介绍了ExoPlayer的简单使用,运用了media3-ui包中提供的关于ExoPlayer的UI组件和资源。但是在日常开发中,播放器的界面会被要求为各式各样的,没有办法使用media3-ui包中提供的通用界面。
在这篇文章将介绍如何自己实现一个简单的PlayerView。
看一下最终的效果界面图:
在上一篇文章的依赖项基础上,可以去除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:用于播放事件的监听,包括有播放状态、播放异常情况等。
contentFrame:播放界面的宽高比例大小等。
surfaceView:用于渲染视频的Surface。
bufferingView:视频缓冲时显示。
errorMessageView:视频播放异常时显示。
controller:视频播放控制界面,包括播放暂停操作、播放进度操作等。
通过player.addListener添加一个Player.Listener进行播放事件的监听。Player.Listener有空的默认方法,因此按需实现所需要的方法即可。
我们所需要实现的方法主要有以下几个:
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进行播放事件的监听,可以监听到播放状态、播放异常情况等,此外还有onVideoSizeChanged方法可以获取到视频的宽度和高度,在该方法中更新播放界面的宽高比例即可。
我们需要一个SurfaceView用于渲染视频:
private val surfaceView by lazy { SurfaceView(context) }
binding.exoContentFrame.addView(surfaceView, 0)
player.setVideoSurfaceView(surfaceView)
视频的播放会需要缓冲时间,在上一篇文章中有介绍到播放监听onPlaybackStateChanged方法中会有视频播放的四种播放状态:
在监听到STATE_READY状态为止之前就是视频的缓冲时间,进行一个缓冲状态的展示,例如loading,监听到STATE_READY状态之后,隐藏缓冲状态loading,展示视频播放。
在播放监听onPlayerError方法中可以监听到播放异常情况,此时需要展示播放异常界面,展示错误信息和重新加载界面,当需要重新加载时调用player.prepare()方法。
视频播放控制界面,包括播放暂停操作、播放进度操作等跟用户进行互动的操作。
根据播放器当前状态来进行播放或者暂停的操作,同时更新操作界面展示。
binding.tvPlay.setOnClickListener {
if (player.isPlaying) {
player.pause()
binding.tvPlay.text = "播放"
} else {
player.play()
binding.tvPlay.text = "暂停"
}
}
播放进度是一个需要不断自动更新的状态,在播放监听中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不断的发消息来进行进度的更新。当视频不是播放状态时自然也是不需要更新播放进度的。
/**
* 更新进度条状态
*/
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发消息来进行播放进度的更新,所以也需要对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()
}
}
如果不采取任何操作,在进行后台切至前台的操作时,会出现黑屏的情况,因此当切至后台时,对播放状态进行一个保存,并暂停播放器,当切回前台时,恢复播放器之前的播放状态,可以在onWindowVisibilityChanged方法中进行该优化操作:
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()
}
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。