You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ViewPager中ExoPlayer播放/暂停控制及滚动播放异常问题

解决ViewPager中ExoPlayer滚动切换页面无法播放的问题

问题根源分析

你遇到的核心问题有两个:

  1. 全局Player实例冲突:你的适配器里只用了一个player全局变量,当ViewPager销毁页面(调用destroyItem)时,这个Player会被释放,导致切换到新的视频页面时没有可用的Player实例。
  2. 页面切换的播放控制未关联到当前页面:之前的onPageSelected逻辑操作的是全局Player,但这个Player可能已经被释放或者不属于当前页面,自然无法控制播放。

修复方案

我们需要让每个视频页面持有自己的Player实例,并通过ViewPager的回调正确控制当前页面的播放状态。

第一步:修改MediaVPAdapter

重构适配器,为每个视频页面创建独立的Player实例,并通过Map管理这些实例,同时添加外部控制播放/暂停的方法:

class MediaVPAdapter(
    val context: Context,
    @LayoutRes val layoutRes: Int,
    val medias: List<MediaFiles> = listOf()
) : PagerAdapter() {

    // 保存每个位置对应的Player实例,避免重复创建和内存泄漏
    private val playerMap = mutableMapOf<Int, SimpleExoPlayer>()

    override fun isViewFromObject(view: View, o: Any): Boolean {
        return view == o
    }

    override fun getCount() = medias.size

    override fun instantiateItem(container: ViewGroup, position: Int): Any {
        return LayoutInflater.from(context).inflate(layoutRes, container, false).apply {
            if (medias.isNotEmpty()) {
                when (medias[position].mediaType) {
                    MediaType.PHOTO -> {
                        ssImageView.makeInvisible(true)
                        exoPlayerView.makeInvisible()
                        // 保留你原本的图片显示逻辑
                    }
                    MediaType.VIDEO -> {
                        ssImageView.makeInvisible(true)
                        exoPlayerView.makeVisible()
                        // 为当前视频页面创建独立的Player实例
                        val player = ExoPlayerFactory.newSimpleInstance(
                            DefaultRenderersFactory(context),
                            DefaultTrackSelector(),
                            DefaultLoadControl()
                        )
                        playerMap[position] = player
                        medias[position].uri?.let { uri ->
                            initPlayer(this, player, uri)
                        }
                    }
                    null -> { /* 处理null媒体类型的情况 */ }
                }
            }
            container.addView(this, 0)
        }
    }

    private fun initPlayer(view: View, player: SimpleExoPlayer, uri: Uri) {
        Timber.i("initPlayer: uri: $uri")
        with(view) {
            exoPlayerView.player = player
            val mediaSource = buildMediaSource(uri)
            player.prepare(mediaSource, true, false)
            player.playWhenReady = false
            // 可选:恢复之前的播放进度(需要给MediaFiles添加playPosition字段)
            medias.firstOrNull { it.uri == uri }?.let { media ->
                player.seekTo(media.playPosition)
            }
        }
    }

    private fun buildMediaSource(uri: Uri) = ExtractorMediaSource.Factory(
        DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getString(R.string.app_name)))
    )
        .setExtractorsFactory(DefaultExtractorsFactory())
        .createMediaSource(uri)

    override fun destroyItem(container: ViewGroup, position: Int, obj: Any) {
        // 释放当前位置的Player,并保存播放进度
        playerMap[position]?.let { player ->
            medias.getOrNull(position)?.playPosition = player.currentPosition
            player.playWhenReady = false
            player.release()
            playerMap.remove(position)
        }
        (obj as View).exoPlayerView.player = null
        container.removeView(obj)
    }

    // 外部调用:播放指定位置的视频
    fun playPage(position: Int) {
        playerMap[position]?.let { player ->
            player.playWhenReady = true
        }
    }

    // 外部调用:暂停非当前位置的所有视频
    fun pauseOtherPages(currentPosition: Int) {
        playerMap.forEach { (pos, player) ->
            if (pos != currentPosition) {
                player.playWhenReady = false
            }
        }
    }

    // 页面销毁时调用:释放所有Player实例
    fun releaseAllPlayers() {
        playerMap.values.forEach { it.release() }
        playerMap.clear()
    }
}

第二步:添加ViewPager页面切换监听

在你的Activity/Fragment中,给ViewPager添加监听,控制页面切换时的播放状态:

// 初始化适配器和ViewPager
val mediaAdapter = MediaVPAdapter(
    this,
    R.layout.fragment_media,
    medias?.toList() ?: listOf()
)
vpMedia.adapter = mediaAdapter
vpMedia.currentItem = position ?: 0
vpMedia.offscreenPageLimit = 2 // 适当提高预加载数量,减少Player频繁创建销毁

// 添加页面切换监听
vpMedia.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        // 滚动过程中暂停所有非当前页面的视频,避免卡顿
        mediaAdapter.pauseOtherPages(position)
    }

    override fun onPageSelected(position: Int) {
        // 选中页面后,如果是视频则自动播放
        if (medias[position].mediaType == MediaType.VIDEO) {
            mediaAdapter.playPage(position)
        } else {
            // 如果是图片,暂停所有视频播放
            mediaAdapter.pauseOtherPages(position)
        }
    }

    override fun onPageScrollStateChanged(state: Int) {
        // 滚动停止时,确保当前视频页面处于正确的播放状态
        if (state == ViewPager.SCROLL_STATE_IDLE) {
            val currentPos = vpMedia.currentItem
            if (medias[currentPos].mediaType == MediaType.VIDEO) {
                mediaAdapter.playPage(currentPos)
            }
        }
    }
})

// 页面销毁时释放所有Player,避免内存泄漏
override fun onDestroy() {
    super.onDestroy()
    mediaAdapter.releaseAllPlayers()
}

第三步:可选优化(保存播放进度)

给你的MediaFiles类添加一个playPosition字段(类型为Long,默认0),这样用户切换页面后再回来可以继续之前的进度:

data class MediaFiles(
    val uri: Uri?,
    val mediaType: MediaType?,
    var playPosition: Long = 0 // 添加这个字段
)

额外注意事项

  • offscreenPageLimit设置:不要设为0,适当提高(比如2),这样ViewPager会预加载前后页面,减少Player的频繁创建和销毁,提升流畅度。
  • 内存泄漏防范:一定要在页面销毁时调用releaseAllPlayers(),释放所有Player资源。
  • 播放按钮控制:如果你的页面有自定义的播放/暂停按钮,需要在按钮点击时获取当前页面的Player实例,直接控制playWhenReady状态,而不是操作全局变量。

内容的提问来源于stack exchange,提问作者Vishak A Kamath

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 09:21:01