解决android 滑动列表页自动播视频中的一些技术难点。助力更好的实现相似需求。不涉及到播放器的具体编解码技术,由于各家用的播放器可能都不同(实际上是我不会~)android
建议在滑动中止的时候播视频,还在滑动的时候建议不播。目前大厂的app基本上都是基于这个思路来作。 这么作的主要缘由有两点:算法
以RecyclerView 为例:bash
因此咱们其实只要在这个回调中进行markdown
这个case 的条件 就表明列表页滑动中止。 这就是咱们播视频的时机了网络
咱们以最流行的作法:符合人类视觉感知的,选择 从上到下列表页中第一个完整展现视频区域的 item 进行播放 有人说咱们直接用findFirstCompletelyVisibleItemPosition方法不就好了?其实这样是不完美的,由于咱们列表页中的item 并不仅是一个单一的视频播放区域,他还有标题,描述,阅读数等等**。你的视频播放区域其实只占你整个item的一部分**app
因此咱们就要计算出来 视频区域完整显示 这个条件。 代码以下:ide
case SCROLL_STATE_IDLE: //取第一个展现出来的item int firstPosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition(); //最后一个展现出来的item int lastPosition = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition(); //开始遍历 for (int i = firstPosition; i <= lastPosition; i++) { //取出这个列表页中的item View itemView = recyclerView.getLayoutManager().findViewByPosition(i); //必定要先判空 由于有时上面这个函数真的会返回null //而后接着判断 是否是有视频播放组件 好比有的列表页除了有视频item还有纯图片的item //因此要断定这个条件 if (itemView != null && itemView.findViewById(R.id.video_player) != null) { VideoPlayer videoPlayer = itemView.findViewById(R.id.video_player); if (isCanBePlayedByRect(videoPlayer)) { safePlayVideoMethod(videoPlayer); break; } } } 复制代码
private boolean isCanBePlayedByRect(VideoPlayer videoPlayer) { Rect rect = new Rect(); //这里就能够取出来视频播放区域的坐标轴了 videoPlayer.getLocalVisibleRect(rect); int videoHeight = videoPlayer.getHeight(); //符合这个条件就意味着 整个视频播放区域 都是完整的呈如今视频中的 final boolean playFlag = (rect.top == 0 && rect.bottom == videoHeight) return playFlag; } 复制代码
达成这个条件 咱们基本的算法就写好了,可是这样仍旧不完美。函数
考虑另一种复杂的状况,好比说看下面这张图:优化
咱们能够看到列表页中的最后2个item其实 是已经彻底露出屏幕的,他是符合咱们上一小节的断定条件的。 可是视觉上咱们仍然是以为 这2个视频播放区域是不完整的,由于被底部的tab 遮盖住了。spa
甚至有的用户会把虚拟键给调出来。以下图
这样咱们的item展现的区域被遮盖的就更多了。用前面一小节的算法已经不够了。
此时咱们须要更进一步,针对前面的算法作必定的修改。有人会说,咱们直接用底部tab的高度减一下不就好了么? 这样其实也能够,可是这样作 不太优雅,缘由有2:
咱们这里能够用另一种比较好的方法规避上述的问题:
直接取底部tab的top坐标(针对整个坐标轴而言)。
咱们能够在activity的第一帧绘制完毕之后 取一下这个坐标:
if (mTabWidget != null && MAIN_PAGE_BOTTOM_TAB_RECT_TOP == 0) { Rect rect = new Rect(); //注意 咱们这里 使用的是getGlobalVisibleRect 而不是getLocal了 //区别就是 global取得就是 针对全屏幕的坐标轴 mTabWidget.getGlobalVisibleRect(rect); MAIN_PAGE_BOTTOM_TAB_RECT_TOP = rect.top; } 复制代码
到这里咱们就拿到了 底部tab 这个view的 top的坐标值了。因此剩下的条件咱们只要增长一条
视频播放组件的 全屏bottom的值 要大于 咱们这个tab的top的值。 只有这样才能保证 咱们视频的view 是没有被遮盖的
private boolean isCanBePlayedByRect(VideoPlayer videoPlayer) { Rect rect = new Rect(); videoPlayer.getLocalVisibleRect(rect); Rect gRect = new Rect(); //把视频view的全局 bottom坐标 也取出来 与tab的top坐标作对比 videoPlayer.getGlobalVisibleRect(gRect); int videoHeight = videoPlayer.getHeight(); final boolean playFlag = (rect.top == 0 && rect.bottom == videoHeight && gRect.bottom < MAIN_PAGE_BOTTOM_TAB_RECT_TOP); return playFlag; } 复制代码
有人说 咱们用
其实这样也是不完美的,仍是前面那个问题,有些item 他就是有其余要素要展现, 就会出现 视频区域已经看不到了,被滑走了, 此时理应中止播放,可是由于还有好比视频阅读数等东西刚好在列表但是区域范围以内 致使这个item没法进入这个回调。
为了解决这个问题,咱们使用以下方案: 在列表页滑动的时候,断定视频区域是否所有在列表页以外,人已经看不到了,符合这个条件就中止播放视频
代码以下:
int position = 0; if (dy > 0) { //dy>0 表明 item 从下往上 被划出屏幕的 因此咱们只须要断定最上面一个可视的item便可 position = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition(); } else if (dy < 0) { //dy>0 表明 item 从上往下 被划出屏幕的 因此咱们只须要断定最下面一个可视的item便可 position = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastVisibleItemPosition(); } View itemView = mRecyclerView.getLayoutManager().findViewByPosition(position); if (itemView != null) { VideoPlayer videoPlayer = itemView.findViewById(R.id.video_player); if (videoPlayer != null) { Rect rect = new Rect(); Rect gRect = new Rect(); vivoSpaceVideoPlayer.getLocalVisibleRect(rect); vivoSpaceVideoPlayer.getGlobalVisibleRect(gRect); //item 从下往上划出屏幕 的断定条件 final boolean stopFlagForDownToUp = (dy > 0 && rect.bottom - rect.top <= STOP_FLAG_PX_VALUE); //item 从上往下划出屏幕的 断定条件 ----有底部tab final boolean stopFlagForUptoDown1 = (dy < 0 && gRect.top > (MAIN_PAGE_BOTTOM_TAB_RECT_TOP - STOP_FLAG_PX_VALUE)); //item 从上往下划出屏幕的 断定条件 ----没有底部tab final boolean stopFlagForUptoDown2 = (dy < 0 && rect.top != 0); if (stopFlagForDownToUp || stopFlagForUptoDownHasParent || stopFlagForUptoDownNoParent) { videoPlayer.release(); } } } 复制代码
这里的算法比较简单,我就不一一解释了,惟一要说一下的就是这个
//定义个阈值 中止播放断定使用
private static final int STOP_FLAG_PX_VALUE = 5;
复制代码
为何要加一个这个?由于针对全屏坐标而言,有时候滑动快了并不会通过某一个值,因此咱们要有必定的余量。 例如 咱们条件是 当咱们的视频区域的top坐标(滑动的时候会从例如 1900开始递增) 大于 tab的top坐标(假设这个值是2100)的时候 就断定被遮盖了 中止滑动, 并不表明视频区域的top坐标是 1900 1901 1902 这样1px 1px的递增到2100的。 有时候他会忽略掉,好比某些手机常常出现1997 1998 1999 2102 等等 状况,因此这里咱们要必定的余量。 确保 视频必定会被中止播放。
当咱们网络数据回来之后 ,咱们会把值set到RecyclerView 里面,而后notify,此时界面就有展现了,可是这个时候 若是用户没有滑动,又刚好第一屏的item有一个是视频,那咱们怎么触发这种条件的自动播放呢?
其实RecyclerView有个特性就是 第一次渲染完毕之后 onScrolled会被执行一次,因此咱们只要在这里作个标志位 而后去播放一次 就好了。
@Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (!mRvInitFlag) { playFirstVideoView(); mRvInitFlag = true; return; } 复制代码
private void playFirstVideoView() { VLog.d(TAG, "method in playFirstVideoView"); if (mRecyclerView.getLayoutManager() != null) { //只有是 视频 才有意义,若是都不是视频 那没有任何意义 int firstPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition(); int lastPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findLastVisibleItemPosition(); for (int i = firstPosition; i <= lastPosition; i++) { // 若是 i<0 则直接跳过 由于毫无心义 if (i < 0) { continue; } View itemView = mRecyclerView.getLayoutManager().findViewByPosition(i); if (itemView != null && itemView.findViewById(R.id.video_player) != null) { VideoPlayer videoPlayer = itemView.findViewById(R.id.video_player); Rect rect = new Rect(); Rect gRect = new Rect(); videoPlayer.getLocalVisibleRect(rect); videoPlayer.getGlobalVisibleRect(gRect); int videoHeight = videoPlayer.getHeight(); //只要第一个元素 可视区域是完整的,那么就直接给他播放 if ((rect.bottom - rect.top) == videoHeight && gRect.bottom < MAIN_PAGE_BOTTOM_TAB_RECT_TOP && gRect.bottom > 0) { safePlayVideoMethod(videoPlayer); //既然找到了 那么就直接播放 不要再管其余的了 break; } } } } 复制代码