使用的是b站开源的ijk播放器
正常的列表视频在视频加载完成之前肯定是要显示图片,视频加载好后在播放视频,ijk中没有发现视频有缩略图的选项,所以布局使用一个帧布局,用张图片把VideoView
盖住,当视频加载好后再把图片去掉(为什么不是VideoView
盖住图片,如果这样的话再把VideoView
展示出来的时候会有一个黑屏,比较影响体验)
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.app.widget.live.VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/ivItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="url" />
</FrameLayout>
一般列表都是使用RecyclerView
,在ViewHolder
中初始化数据
haveVideo = false;
ivItem.setVisibility(VISIBLE);
if (videoView != null) {
// 获取视频url
videoUrl = getVideoUrl(bean);
// 是否展示视频
if (videoUrl != null) {
haveVideo = true;
VideoViewManager.Companion.getVideoViewList().add(videoView);
VideoViewManager.Companion.getVideoViewMap().put(videoView, ivItem);
Object tag = videoView.getTag();
// 这里有item复用问题,所以给每个item加上tag,然后在这里判断tag和index是否一样,不一样说明被复用了
if (tag != null && Integer.parseInt(tag.toString()) != index) {
// 如果不release后面的start无法正常执行,只能release
videoView.release();
}
videoView.setTag(String.valueOf(index));
// 使用缓存
HttpProxyCacheServer proxy = getProxy();
String proxyUrl = proxy.getProxyUrl(videoUrl);
videoView.setUrl(proxyUrl);
videoView.setMute(true);
videoView.start();
videoView.setLooping(true);
videoView.setOnStateChangeListener(new VideoView.OnStateChangeListener() {
@Override
public void onPlayerStateChanged(int playerState) {
}
@Override
public void onPlayStateChanged(int playState) {
// 如果不加haveVideo的判断,别的图片位复用前面的视频,然后滑动停止之后会开始播放视频,这时候就会通过这个if。所以需要加haveVideo来判断这个item是否有视频
// playState == VideoView.STATE_PLAYING 由于ijk没有视频准备好的回调,所以只能在这判断他的状态,开始播放时就代表准备好了,就可以把图片隐藏了
if (playState == VideoView.STATE_PLAYING && haveVideo) {
ivItem.setVisibility(View.INVISIBLE);
}
}
});
}
}
上面的代码是踩过很多坑之后完善的代码 一开始简单的展示视频的话只需要这些即可
if (videoView != null) {
// 获取视频url
videoUrl = getVideoUrl(bean);
// 是否展示视频
if (videoUrl != null) {
VideoViewManager.Companion.getVideoViewList().add(videoView);
VideoViewManager.Companion.getVideoViewMap().put(videoView, ivItem);
videoView.setUrl(videoUrl);
videoView.setMute(true);
videoView.start();
videoView.setLooping(true);
videoView.setOnStateChangeListener(new VideoView.OnStateChangeListener() {
@Override
public void onPlayerStateChanged(int playerState) {
}
@Override
public void onPlayStateChanged(int playState) {
if (playState == VideoView.STATE_PLAYING) {
ivItem.setVisibility(View.INVISIBLE);
}
}
});
}
}
这么一看的话就简便了很多
先把VideoViewManager
的代码贴一下
class VideoViewManager {
companion object {
val videoViewList = ArrayList<VideoView>()
val videoViewMap = HashMap<VideoView, ResizeImageView>()
// 首页release之后重新播放视频
@JvmStatic
fun startVideoViewAfterRelease(recyclerView: RecyclerView) {
val videoList = getVisibleItems(recyclerView)
for (videoView in videoList) {
// 首页release之后不会重新走onBindView,所以要在这手动把这些video view加在list里,要不然pause的时候没法管理
if (!videoViewList.contains(videoView)) {
videoViewList.add(videoView)
}
if (!videoView.isPlaying) {
videoView.start()
}
}
}
private fun getOutRange(recyclerView: RecyclerView): IntArray {
val array = IntArray(2)
array[0] = -1
val layoutManager = recyclerView.layoutManager
if (layoutManager is StaggeredGridLayoutManager) {
val first = IntArray(layoutManager.spanCount)
layoutManager.findFirstVisibleItemPositions(first)
val last = IntArray(layoutManager.spanCount)
layoutManager.findLastVisibleItemPositions(last)
array[0] = first[0]
Arrays.sort(last)
array[1] = last[last.size - 1]
} else if (layoutManager is LinearLayoutManager) {
array[0] =
(Objects.requireNonNull(layoutManager) as LinearLayoutManager).findFirstVisibleItemPosition()
array[1] =
(layoutManager as LinearLayoutManager?)!!.findLastVisibleItemPosition()
}
return array
}
// 释放所有video view
@JvmStatic
fun releaseVideoView() {
for (videoView in videoViewList) {
videoViewMap[videoView]?.visibility = View.VISIBLE
videoView.release()
}
videoViewMap.clear()
videoViewList.clear()
}
private fun getVisibleItems(recyclerView: RecyclerView): List<VideoView> {
val videoList = java.util.ArrayList<VideoView>()
val array = getOutRange(recyclerView)
val layoutManager = recyclerView.layoutManager
for (i in array[0]..array[1]) {
val itemView = layoutManager?.findViewByPosition(i)
val viewHolder = recyclerView.findViewHolderForAdapterPosition(i)
if (viewHolder !is ProductHolder) {
continue
}
val videoView = itemView?.findViewById<View?>(R.id.videoView) as VideoView?
val productImg = itemView?.findViewById<View?>(R.id.ivProduct) as ResizeImageView?
if (videoView != null && viewHolder.haveVideo()) {
videoList.add(videoView)
if (productImg != null) {
videoViewMap[videoView] = productImg
}
}
}
return videoList
}
}
private val videoViews = ArrayList<VideoView>()
private var playingVideoViews = HashSet<VideoView>()
// 在RecyclerView滚动监听中调用这个方法,注意要判断一下newState != RecyclerView.SCROLL_STATE_SETTLING,这个情况下就不需要调用这个了,要不然会比较卡
fun adjustVideo(recyclerView: RecyclerView) {
val list = getVisibleItems(recyclerView)
val newPlayingVideoViews = HashSet<VideoView>()
for (videoView in list) {
newPlayingVideoViews.add(videoView)
if (playingVideoViews.contains(videoView)) {
playingVideoViews.remove(videoView)
}
if (!videoView.isPlaying) {
videoView.start()
}
}
for (i in playingVideoViews) {
if (i.isPlaying) {
i.pause()
}
}
playingVideoViews = newPlayingVideoViews
}
fun pauseVisibleVideoView(recyclerView: RecyclerView) {
val videoList = getVisibleItems(recyclerView)
for (videoView in videoList) {
if (videoView.isPlaying) {
if (!videoViews.contains(videoView)) {
videoViews.add(videoView)
}
videoView.pause()
}
}
}
fun startVisibleVideoView() {
for (videoView in videoViews) {
videoView.start()
}
}
}
首先看前面代码
if (videoView != null) {
// 获取视频url
videoUrl = getVideoUrl(bean);
// 是否展示视频
if (videoUrl != null) {
VideoViewManager.Companion.getVideoViewList().add(videoView);
VideoViewManager.Companion.getVideoViewMap().put(videoView, ivItem);
videoView.setUrl(videoUrl);
videoView.setMute(true);
videoView.start();
videoView.setLooping(true);
videoView.setOnStateChangeListener(new VideoView.OnStateChangeListener() {
@Override
public void onPlayerStateChanged(int playerState) {
}
@Override
public void onPlayStateChanged(int playState) {
if (playState == VideoView.STATE_PLAYING) {
ivItem.setVisibility(View.INVISIBLE);
}
}
});
}
}
ivItem.setVisibility(View.INVISIBLE);
这里把上面的图片隐藏了,但是了解RecyclerView
的就知道,这玩意会复用的,前面的holder
把图片隐藏了,后面的holder
复用的时候重新init
数据,走到这发现videoView
为空,或者url为空的时候下面就不走了,这时候视频是没法加载的,展示出来的就是一个黑屏,因为这个holder
复用的前面的,前面的已经把图片去掉了,所以后面需要把图片加回来,也就是常说的RecyclerView
中写了if,就得写else。
这是其中一个复用问题,所有的RecyclerView
中都会有这个问题,但是这个视频组件还有别的复用问题:
在多个视频存在的时候,可能前面的视频开始播放了,然后滚到下面来,开始播放新的视频,这时候发现播放的是前面的视频,断点调试url是正确设置的,然后看videoView.start()
方法,这里应该是不同实现有不同的写法,我这里的写法是会判断一下这个视频的状态,如果是播放中就不会再执行start()
,那为什么会在播放中呢,因为复用了前面的视频,他处在了播放中的状态,所以这里就会出现这个情况,播放了前面的视频
所以给每个VideoView
都加上了Tag,值为index,来判断是否发生了复用,发生复用了就要release
掉视频。
还有一个haveVideo
的bool值判断,也是复用的问题,可以看看前面的注释
ijk每次播放视都回去网络重新加载,如果视频比较大的话加载消耗也比较大,这里可以使用HttpProxyCacheServer
视频缓存组件,具体的使用可以去网上查一下相关的用法