列表中自动播放视频,常规方案在每个 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() } } } } } } }
使用方式
private fun initAutoVideoPlayHelper() { autoPlayVideoAttr = GroupAlbumAutoPlayVideoAttacher(this, mBinding.viewPager, true) { adapter.preCacheVideo(it) }.addObserver(lifecycle) autoPlayVideoAttr?.init() }