最近在开发新App的过程中遇到了个需求,需求在RecyclerView
中播映视频。本文介绍如安在RecyclerView
中运用Media3
播映视频。
增加依靠
在app module下的build.gradle中增加代码,如下:
dependencies {
implementation("androidx.media3:media3-ui:1.1.0")
implementation("androidx.media3:media3-session:1.1.0")
implementation("androidx.media3:media3-exoplayer:1.1.0")
}
在RecyclerView中运用Media3播映视频
设置ExoPlayer
RecyclerView
有复用机制,能够在ViewHolder
中配置ExoPlayer
和通用参数,削减ExoPlayer
实体的数量,代码如下:
- Adapater 代码
class Media3ListExampleAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val containerData = ArrayList<ExampleListEntity>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
// 经过viewType判断该运用什么布局,1为视频item,2为普通item
return if (viewType == 1) {
VideoItemViewHolder(LayoutMedia3ListVideoItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
} else {
NormalItemViewHolder(LayoutMedia3ListNormalItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
}
override fun getItemCount(): Int {
return containerData.size
}
override fun getItemViewType(position: Int): Int {
return containerData[position].type
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// 设置item项的点击事情
holder.itemView.setOnClickListener {
itemClickCallback?.onItemClick(containerData[position])
}
when (holder) {
is VideoItemViewHolder -> {
containerData[position].run {
// 加载榜首帧作为封面
Glide.with(holder.itemView.context)
.setDefaultRequestOptions(RequestOptions()
.frame(1)
.centerCrop())
.load(videoUrl)
.into(holder.itemViewBinding.ivVideoCover)
holder.itemViewBinding.playerView.player?.run {
// 设置播映链接
videoUrl?.let { newUrl -> setMediaItem(MediaItem.fromUri(newUrl))}
playWhenReady = true
prepare()
}
}
}
is NormalItemViewHolder -> holder.itemViewBinding.tvTextContent.text = "${containerData[position].itemText ?: "Normal item"} $position"
}
}
fun setNewData(newData: ArrayList<ExampleListEntity>?) {
val currentItemCount = itemCount
if (currentItemCount != 0) {
containerData.clear()
notifyItemRangeRemoved(0, currentItemCount)
}
if (!newData.isNullOrEmpty()) {
containerData.addAll(newData)
notifyItemRangeChanged(0, itemCount)
}
}
class VideoItemViewHolder(val itemViewBinding: LayoutMedia3ListVideoItemBinding) : RecyclerView.ViewHolder(itemViewBinding.root) {
private val exoPlayer: ExoPlayer = ExoPlayer.Builder(itemView.context).apply {
CacheController.getMediaSourceFactory()?.let { setMediaSourceFactory(it) }
}.build()
private val videoListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
// 开端播映时躲藏封面图
itemViewBinding.ivVideoCover.visibility = View.GONE
}
}
}
init {
// 配置ExoPlayer到PlayerView
itemViewBinding.playerView.player = exoPlayer
itemViewBinding.playerView.player?.run {
removeListener(videoListener)
addListener(videoListener)
repeatMode = Player.REPEAT_MODE_ALL
}
}
}
class NormalItemViewHolder(val itemViewBinding: LayoutMedia3ListNormalItemBinding) : RecyclerView.ViewHolder(itemViewBinding.root)
}
- 示例页面代码
class Media3ListExampleActivity : BaseGestureDetectorActivity<LayoutMeida3ListExampleAcitivityBinding>() {
private val media3ListExampleAdapter = Media3ListExampleAdapter()
override fun initViewBinding(layoutInflater: LayoutInflater): LayoutMeida3ListExampleAcitivityBinding {
return LayoutMeida3ListExampleAcitivityBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.rvMedia3ListContainer.adapter = media3ListExampleAdapter
media3ListExampleAdapter.itemClickCallback = object : Media3ListExampleAdapter.ItemClickCallback {
override fun onItemClick(data: ExampleListEntity) {
// 打开一个半透明的Activity
startActivity(Intent(this@Media3ListExampleActivity, TransparentActivity::class.java))
}
}
media3ListExampleAdapter.setNewData(arrayListOf(
ExampleListEntity(1, "https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4", "Video item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(1, "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4", "Video item"),
ExampleListEntity(2, "", "Normal item"),
ExampleListEntity(2, "", "Normal item")))
}
}
作用如图:
在作用图中能够看到,上面的代码能够简略实现在RecyclerView
中播映视频,可是存在如下问题:
- 当 item 现已滑出屏暗地视频不会暂停播映(图中无法表现,能够自行经过代码验证一下)。
- 当打开新页面时视频不会暂停播映。
暂停和持续播映
不会暂停播映的原因显而易见,在上面的代码中并没有主动调用暂停播映。那么针对上面两点问题,什么时候调用暂停播映比较合适呢?
- 当 Item 滑出屏幕时,
Adapater
中的onViewDetachedFromWindow
办法会被回调,能够在此调用暂停办法。 -
Adapter
无法像Activity
相同感知生命周期,因而需求对外曝露改写办法,然后在Activity
的onResume
和onPause
办法中调用改写办法,改动视频的状态。
改动代码如下:
- Adapater 代码
class Media3ListExampleAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
.....
private var pauseVideo = false
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
......
when (holder) {
is VideoItemViewHolder -> {
containerData[position].run {
......
holder.itemViewBinding.playerView.player?.run {
if (pauseVideo) {
// 暂停播映
holder.pauseVideo()
} else {
// 开端播映
videoUrl?.let { newUrl -> setMediaItem(MediaItem.fromUri(newUrl))}
playWhenReady = true
prepare()
}
}
}
}
is NormalItemViewHolder -> holder.itemViewBinding.tvTextContent.text = "${containerData[position].itemText ?: "Normal item"} $position"
}
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
super.onViewDetachedFromWindow(holder)
if (holder is VideoItemViewHolder) {
// 当View从Window中被移除时暂停播映视频
holder.pauseVideo()
}
}
fun notifyVideoItemStatus(pauseVideo: Boolean) {
// 设置是否暂停播映,更新视频 item
this.pauseVideo = pauseVideo
containerData.forEach {
if (it.type == 1) {
notifyItemChanged(containerData.indexOf(it))
}
}
}
class VideoItemViewHolder(val itemViewBinding: LayoutMedia3ListVideoItemBinding) : RecyclerView.ViewHolder(itemViewBinding.root) {
......
fun pauseVideo() {
// 暂停播映视频
itemViewBinding.playerView.onPause()
exoPlayer.pause()
}
}
......
}
- 示例页面代码
class Media3ListExampleActivity : BaseGestureDetectorActivity<LayoutMeida3ListExampleAcitivityBinding>() {
......
override fun onPause() {
super.onPause()
media3ListExampleAdapter.notifyVideoItemStatus(true)
}
override fun onResume() {
super.onResume()
media3ListExampleAdapter.notifyVideoItemStatus(false)
}
}
作用如图:
现在能够在需求暂停时暂停,需求播映时播映了,满足了基本的运用。可是还有一个问题:暂停后持续播映时视频从头开端播映了,这个体验不太好,能够优化一下。
记录播映进展
当 item 被移出屏幕再移回时、主动调用改写办法时,Adapater
的onBindViewHolder
办法都会履行,所以其实每次都是设置新的播映链接,然后从头开端播映。Media3
的setMediaItem
办法能够传入开端播映的位置,所以只要在暂停时记录一下当前视频播映的进展,然后在调用setMediaItem
时传入即可。
改动代码如下:
class Media3ListExampleAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
......
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
......
when (holder) {
is VideoItemViewHolder -> {
containerData[position].run {
......
holder.itemViewBinding.playerView.player?.run {
if (pauseVideo) {
// 暂停播映
holder.pauseVideo()
} else {
// 开端播映
videoUrl?.let { newUrl ->
// 运用播映链接获取缓存的播映进展,没有的话从0开端播映
setMediaItem(MediaItem.fromUri(newUrl), holder.mediaProgress[newUrl] ?: 0L)
}
playWhenReady = true
prepare()
}
}
}
}
is NormalItemViewHolder -> holder.itemViewBinding.tvTextContent.text = "${containerData[position].itemText ?: "Normal item"} $position"
}
}
......
class VideoItemViewHolder(val itemViewBinding: LayoutMedia3ListVideoItemBinding) : RecyclerView.ViewHolder(itemViewBinding.root) {
......
val mediaProgress = ConcurrentHashMap<String, Long>()
private val videoListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
if (isPlaying) {
// 开端播映时躲藏封面图
itemViewBinding.ivVideoCover.visibility = View.GONE
} else {
itemViewBinding.playerView.player?.run {
// 暂停时根据视频链接记录播映的进展
currentMediaItem?.localConfiguration?.uri?.toString()?.let { uri -> mediaProgress[uri] = currentPosition }
}
}
}
}
......
}
......
}
作用如图:
示例
演示代码已在示例Demo中增加。
ExampleDemo github
ExampleDemo gitee