前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android 列表视频

Android 列表视频

作者头像
SakuraTears
发布2023-02-01 17:09:03
9320
发布2023-02-01 17:09:03
举报
文章被收录于专栏:从零开始的Code生活

视频组件选择

使用的是b站开源的ijk播放器

组件布局

正常的列表视频在视频加载完成之前肯定是要显示图片,视频加载好后在播放视频,ijk中没有发现视频有缩略图的选项,所以布局使用一个帧布局,用张图片把VideoView盖住,当视频加载好后再把图片去掉(为什么不是VideoView盖住图片,如果这样的话再把VideoView展示出来的时候会有一个黑屏,比较影响体验)

代码语言:javascript
复制
<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中初始化数据

代码语言:javascript
复制
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);
                }
            }
        });
    }
}

上面的代码是踩过很多坑之后完善的代码 一开始简单的展示视频的话只需要这些即可

代码语言:javascript
复制
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的代码贴一下

代码语言:javascript
复制
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复用问题

首先看前面代码

代码语言:javascript
复制
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视频缓存组件,具体的使用可以去网上查一下相关的用法

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023年01月17日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 视频组件选择
  • 组件布局
  • 视频展示
  • VideoViewManager
  • 遇到的坑
    • Item复用问题
      • 本地缓存
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档