Android Media3(四)— 在RecyclerView中使用Media3

最近在开发新App的过程中遇到了个需求,需要在RecyclerView中播放视频。本文介绍如何在RecyclerView中使用Media3播放视频。

添加依赖

在app module下的build.gradle中添加代码,如下:

scss 复制代码
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 代码
kotlin 复制代码
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)
}
  • 示例页面代码
kotlin 复制代码
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中播放视频,但是存在如下问题:

  1. 当 item 已经滑出屏幕后视频不会暂停播放(图中无法体现,可以自行通过代码验证一下)。
  2. 当打开新页面时视频不会暂停播放。

暂停和继续播放

不会暂停播放的原因显而易见,在上面的代码中并没有主动调用暂停播放。那么针对上面两点问题,什么时候调用暂停播放比较合适呢?

  1. 当 Item 滑出屏幕时,Adapater中的onViewDetachedFromWindow方法会被回调,可以在此调用暂停方法。
  2. Adapter无法像Activity一样感知生命周期,因此需要对外曝露刷新方法,然后在Activity的onResume和onPause方法中调用刷新方法,改变视频的状态。

改动代码如下:

  • Adapater 代码
kotlin 复制代码
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()
        }
    }
    
    ......
}
  • 示例页面代码
kotlin 复制代码
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时传入即可。

改动代码如下:

kotlin 复制代码
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

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui