列表中自动播放视频,常规方案在每个 xml 中写入视频布局,然后在滑动时获取当前的下标,播放视频

弊端:

播放容易出错,需要精准控制好停止播放操作,并且适配器中容易触发多次刷新,导致执行多次同样的操作;

不易控制离开停止等操作,增加了布局的负担,影响滑动流畅度;

无法复用...

使用过的都比较清楚这些弊端,所以需要一套统一的播放逻辑控制,并且播放中的视频只有一个,方便控制

新的思路方案:

在当前的下标布局中,动态监听你滑动的位置,在当前布局中去注入一个通用的视频播放器,然后去控制这个播放器

优点:

跟展示列表分离,容易控制状态;

可复用,不影响列表滑动流畅度,提高效率;

有效避免多次刷新后执行,简化了控制逻辑;

实现流程

1、在xml中编写好播放布局,然后 inflate 加载布局 videoLayout,并且绑定生命周期(控制释放),设置滑动监听

private var rootView: View? = null
        get() {
            if (field == null) {
                field = inflateRootVideoPlayer()
            }
            return field
        }

2、获取到当前下标,并且拿到该下标(viewholder)的根布局,或者是 viewpager 的 current position

3、获取到当前播放的实体类(播放路径等,通过 adapter直接获取)

4、拿到的根布局调用系统方法  viewholderRoot.addView(videoLayout) ,把需要播放的布局添加进 当前的 item 根布局中

5、使用 WeakReference 弱引用持有当前工具类,然后在 Handler 中去控制播放等方法

private class WeakReferenceHandler(tag: AutoPlayVideoUtils) : Handler() {
            private val mView = WeakReference(tag)
            override fun handleMessage(msg: Message) {
                when (msg.what) {
                    MSG_LOAD_VIDEO -> mView.get()?.startVideo(msg.obj as VideoData)
                }
            }
        }

6、在监听的滑动事件中判断可见范围,然后移除播放布局 videoLayout

root?.parent?.let {
            if (it is ViewGroup) {
                val video = root.findViewById<VideoPlayerView>(R.id.video_player_view)
                if (video?.isPlaying == true) {
                    video.pause()
                    video.stop()
                }
                it.removeView(root)
                videoPlayerView = null
                rootView = null
            }
        }

此时基本流程完成,实际项目中还有很多需要处理

比如滑动时如果可见下标想等,需要跳过逻辑,不用执行播放操作,然后滑动中监听是否可见,什么时候添加布局跟移除布局

比如在生命周期中恢复,暂停,释放

所幸 Recyclerview 中有滑动停止监听,可以很好的监听控制,viewpager 就更简单点了,这里以 viewpager做参照

package com.frogsing.hotface.message.util

import android.content.Context
import android.os.Handler
import android.os.Message
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.viewpager2.widget.ViewPager2
import com.elvishew.xlog.XLog
import com.frogsing.common.extension.invisible
import com.frogsing.common.extension.setOnThrottledClickListener
import com.frogsing.common.extension.visible
import com.frogsing.common.utils.ScrollStatusListenerUtils
import com.frogsing.hotface.message.group.adapter.GroupPhotoPreviewAdapter
import com.frogsing.hotface.service.comment.data.VideoData
import com.frogsing.libutil.time.TimeUtils
import com.frogsing.videorecord.R
import com.frogsing.videorecord.widgets.VideoPlayerView
import com.pili.pldroid.player.widget.PLVideoTextureView
import java.lang.ref.WeakReference

/**
 * 用于群相册视频自动播放
 */
class GroupAlbumAutoPlayVideoAttacher(
    val context: Context,
    val target: ViewPager2?,
    var isSoundOff: Boolean = false,
    var onPositionPlayed: ((Int) -> Unit)? = null
) : LifecycleObserver {
    private var lastFindPosition = -1
    private var currentPosition = 0
    private var isStarted = false
    private val handler by lazy {
        WeakReferenceHandler(this)
    }

    private var rootView: View? = null
        get() {
            if (field == null) {
                field = inflateRootVideoPlayer()
            }
            return field
        }

    private var videoPlayerView: VideoPlayerView? = null

    private val startVideoListener: (() -> Unit) = {
        startAttach()
    }

    var onPageListener: ((Int) -> Unit)? = null

    fun init() {
        lastFindPosition = -1
        initRecycleView()
        startAttach(1500)
    }

    private fun initRecycleView() {
        target?.registerOnPageChangeCallback(onPagerChangeListener)
    }

    private fun inflateRootVideoPlayer(): View {
        val rootView = LayoutInflater.from(context)
            .inflate(R.layout.video_widget_group_album_player, null, false)
        val video = rootView.findViewById<VideoPlayerView>(R.id.video_player_view)
        val tvTime = rootView.findViewById<TextView>(R.id.tv_time)
        val ivAudio = rootView.findViewById<View>(R.id.iv_audio)
        ivAudio.setOnThrottledClickListener {
            isSoundOff = !isSoundOff
            //静音播放
            if (isSoundOff) {
                video.setSoundOff()
            } else {
                video.setSoundOpen()
            }
        }
        if (isSoundOff) {
            video.setSoundOff()
        }
        inflateVerticalVideoPlayer(video, tvTime)
        return rootView
    }


    private fun inflateVerticalVideoPlayer(
        video: VideoPlayerView,
        tvTime: TextView
    ) {
        videoPlayerView = video
        video.setCoverViewScaleType(ImageView.ScaleType.FIT_CENTER)
            .setScreenRatio(PLVideoTextureView.ASPECT_RATIO_FIT_PARENT)
            .setOnVideoCompletionListener {
                removeVideoParent(rootView)
            }
        video.setOnCurrentProgressChange(object :
            VideoPlayerView.OnCurrentProgressChange {
            override fun onCurrentProgressChange(
                currentProgress: Int,
                currentTime: String?
            ) {
                val totalVideoDurationTimeMill = (video.currentPosition) * 1000L//得到秒
                val countDownTime =
                    (totalVideoDurationTimeMill - totalVideoDurationTimeMill * (currentProgress / 100f)).toInt()
                val countDownTimeStr = TimeUtils.getVideoFormatTime(countDownTime / 1000)
                if (tvTime.text.toString() == countDownTimeStr) return
                if (currentProgress in 1..99) {
                    tvTime.text = countDownTimeStr
                }
            }

            override fun onCurrentPlayPosition(
                currentPosition: Int,
                totalDuration: Int
            ) {
            }

        })
        video.tag = VIDEO_PLAYER_TAG
    }

    /**
     * 1、计算当前显示的条目是否包含视频类型
     * 2、判断当前的条目是否和为上一个播放的条目,是的话返回
     */
    private fun findViewAndHandleVideoPlayer() {
        isLastItemStillVisible()
        if (currentPosition == -1) return
        doAfterFindPosition(currentPosition)
    }

    private fun doAfterFindPosition(lastCompletelyItemPosition: Int) {
        if (getVideoItemWithPosition(lastCompletelyItemPosition) == null) return
        rootView?.let {
            if (lastCompletelyItemPosition == lastFindPosition && rootView?.visibility == View.VISIBLE) {
                // 同一个视频条目,且未滑出过界面
                if (videoPlayerView?.isPlaying == false) {
                    addVideoPlayerView(lastCompletelyItemPosition)
                }
                return
            }
            // 滑出过界面的同一个条目,也要重新加载视频
            removeVideoParent(rootView)
            addVideoPlayerView(lastCompletelyItemPosition)
            lastFindPosition = lastCompletelyItemPosition
        }
    }

    private fun isLastItemStillVisible() {
        val needRemove = isCurrentItemNeedRemove()
        if (needRemove) {
            // 上一条已经不在可视范围内
            removeVideoParent(rootView)
        }
    }

    private fun isCurrentItemNeedRemove(): Boolean {
        if (lastFindPosition != currentPosition) {
            return true
        }
        return false
    }

    private fun addVideoPlayerView(eddVisibleItem: Int) {
        val nextItemView = getItemViewWithPosition(eddVisibleItem) ?: return
        val videoData = getVideoItemWithPosition(eddVisibleItem) ?: return
        rootView?.let { root ->
            if (nextItemView.indexOfChild(root) == -1) {
                root.invisible()
                val params = root.layoutParams
                params?.let {
                    if (params.width != nextItemView.width || params.height != nextItemView.height) {
                        params.width = nextItemView.width
                        params.height = nextItemView.height
                        root.layoutParams = params
                    }
                }

                // java.lang.IllegalStateException:
                // The specified child already has a parent. You must call removeView() on the child's parent first.
                try {
                    nextItemView.addView(root, nextItemView.width, nextItemView.height)
                } catch (e: Throwable) {
                    XLog.e("${e.message}")
                }
            } else {
                root.visible()
            }
            val msg = handler.obtainMessage(MSG_LOAD_VIDEO_DATA)
            msg.obj = videoData
            handler.sendMessageDelayed(msg, 100L)
        }
    }

    private fun startVideo(videoData: VideoData) {
        rootView?.let {
            onPositionPlayed?.invoke(lastFindPosition)
            videoData.img_path?.let {
                videoPlayerView?.setTotalDuration((videoData.len ?: 0) * 1000)?.setCoverView(it)
                    ?.prepare()
            }
            rootView?.visible()
            videoPlayerView?.setPlayerPath(videoData.video_path)?.start()
        }
    }

    private fun removeVideoParent(root: View?) {
        root?.parent?.let {
            if (it is ViewGroup) {
                val video = root.findViewById<VideoPlayerView>(R.id.video_player_view)
                if (video?.isPlaying == true) {
                    video.pause()
                    video.stop()
                }
                it.removeView(root)
                videoPlayerView = null
                rootView = null
            }
        }
    }

    private fun getItemViewWithPosition(position: Int): ViewGroup? {
        val adapter = target?.adapter as? GroupPhotoPreviewAdapter
        adapter?.getViewByPosition(position, com.frogsing.hotface.message.R.id.fl_root)?.let {
            return it as? ViewGroup
        }
        return null
    }

    private fun getVideoItemWithPosition(position: Int): VideoData? {
        val adapter = target?.adapter as? GroupPhotoPreviewAdapter
        adapter?.getItemOrNull(position)?.let {
            if (it.imageData?.video_path?.isNotEmpty() == true) {
                return VideoData(
                    video_path = it.imageData?.video_path,
                    img_path = it.imageData?.thumbUrl
                )
            }
        }
        return null
    }

    fun startAttach(delay: Long = 1000L) {
        if (handler.hasMessages(MSG_ATTACH)) {
            handler.removeMessages(MSG_ATTACH)
        }
        handler.sendEmptyMessageDelayed(MSG_ATTACH, delay)
    }

    fun stop() {
        isStarted = false
        videoPlayerView?.stop()
    }

    @OnLifecycleEvent(value = Lifecycle.Event.ON_PAUSE)
    fun onPause() {
        if (videoPlayerView?.isPlaying == true) {
            videoPlayerView?.pause()
        }
    }

    @OnLifecycleEvent(value = Lifecycle.Event.ON_RESUME)
    fun onResume() {
        if (rootView?.parent != null) {
            videoPlayerView?.start()
        } else {
            findViewAndHandleVideoPlayer()
        }
    }

    @OnLifecycleEvent(value = Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        handler.removeCallbacksAndMessages(null)
        ScrollStatusListenerUtils.instance().clear()
        videoPlayerView?.onDestroy()
    }

    fun addObserver(lifecycle: Lifecycle): GroupAlbumAutoPlayVideoAttacher {
        lifecycle.addObserver(this)
        return this
    }

    private val onPagerChangeListener = object : ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            currentPosition = position
            onPageListener?.invoke(position)
            startAttach(500)
        }
    }

    companion object {
        const val VIDEO_PLAYER_TAG = "VIDEO_PLAYER_TAG"
        const val MSG_LOAD_VIDEO_DATA = 1
        const val MSG_ATTACH = 2

        private class WeakReferenceHandler(tag: GroupAlbumAutoPlayVideoAttacher) : Handler() {
            private val mView = WeakReference(tag)
            override fun handleMessage(msg: Message) {
                when (msg.what) {
                    MSG_LOAD_VIDEO_DATA -> mView.get()?.startVideo(msg.obj as VideoData)
                    MSG_ATTACH -> {
                        mView.get()?.apply {
                            isStarted = true
                            findViewAndHandleVideoPlayer()
                        }
                    }
                }
            }
        }
    }
}
View Code

使用方式

private fun initAutoVideoPlayHelper() {
        autoPlayVideoAttr = GroupAlbumAutoPlayVideoAttacher(this, mBinding.viewPager, true) {
            adapter.preCacheVideo(it)
        }.addObserver(lifecycle)
        autoPlayVideoAttr?.init()
    }

 

posted on 2023-01-15 23:39  翻滚的咸鱼  阅读(882)  评论(0编辑  收藏  举报