zl程序教程

您现在的位置是:首页 >  APP

当前栏目

Android 列表视频

2023-02-26 09:50:14 时间

视频组件选择

使用的是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

先把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()
        }
    }
}

遇到的坑

Item复用问题

首先看前面代码

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视频缓存组件,具体的使用可以去网上查一下相关的用法