内容简介:本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布上一篇文章简单讲解了腾讯新闻的视频无缝切换效果的实现(视频在播放中进行页面切换),如果你没有看过上篇,可以先去看看看看。 说一下这次新增的效果吧:
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
上一篇文章简单讲解了腾讯新闻的视频无缝切换效果的实现(视频在播放中进行页面切换),如果你没有看过上篇,可以先去看看 Android 高仿腾讯新闻视频切换效果 。 上一篇写得比较随意,只是讲解了两个页面间如何实现视频在播放中的切换(切换播放器的container)及滚动停止播放等,部分效果没有实现,有一些细节不是处理得很好,所以重新补上一篇更加详细的教程。相同的内容这次就不在赘述了。 同样,还是先上效果图
帧率有稍微的调整,压缩得有些掉帧了,感兴趣的可以 下载看看。 说一下这次新增的效果吧:
- 进入全屏自动判断横竖屏切换方向
- 全屏播放完毕后自动播放下一个,并且根据视频宽高切换方向
- 播放时增加遮罩层,播放下一个文字提示
- 播放时增加倒计时及动画
- wifi切换4G提示,4G切wifi自动播放
这次播放器换成了JZVideoPlayer,如果项目中还没有接入播放器或者刚接入的,还是建议换成 PlayerBase ,高度解耦,可扩展性高,提供无缝续播助手。
JZVideoPlayer 版本是之前的,并且改动有点大。这里主要介绍思路,跟注意点,用PlayerBase同样也可以实现的。 JZVideoPlayer实现无缝切换其实就是 更改player的ViewParent
public void attachToContainer(ViewGroup container) { detachSuperContainer(); if (container != null) { container.addView(JZVideoPlayerManager.getCurrentJzvd(), new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); playerContainer = container; } } public void detachSuperContainer() { JZVideoPlayer player = JZVideoPlayerManager.getCurrentJzvd(); ViewParent parent = player.getParent(); if (parent != null && parent instanceof ViewGroup) { ((ViewGroup) parent).removeView(player); } } 复制代码
4G跟wifi切换出现提示:注册一个广播进行监听
@Override protected void onResume() { super.onResume(); JZVideoPlayer.goOnPlayOnResume(); IntentFilter filter = new IntentFilter(); filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION); filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION); registerReceiver(wifiReceiver, filter); } @Override protected void onStop() { super.onStop(); try { //weChat moment share will execute twice so try catch unregisterReceiver(wifiReceiver); } catch (Exception e) { e.printStackTrace(); } } private BroadcastReceiver wifiReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent != null && WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) { NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); if (info != null) { if (info.getState().equals(NetworkInfo.State.DISCONNECTED)) { if (JZMediaManager.isWiFi) { JZMediaManager.isWiFi = false; JZVideoPlayer.WIFI_TIP_DIALOG_SHOWED = false; if(播放中或者加载中){ JZMediaManager.instance().jzMediaInterface.pause(); JZVideoPlayerManager.getCurrentJzvd().onStatePause(); } } } else if (info.getState().equals(NetworkInfo.State.CONNECTED)) { if (!JZMediaManager.isWiFi) { JZMediaManager.isWiFi = true; JZVideoPlayer.WIFI_TIP_DIALOG_SHOWED = true; if (JZVideoPlayerManager.getCurrentJzvd() != null && JZVideoPlayerManager.getCurrentJzvd().currentState == JZVideoPlayer.CURRENT_STATE_PAUSE) { JZVideoPlayer.goOnPlayOnResume(); } } } } } } }; 复制代码
这里放在onResume里面注册是因为我的项目不止一个页面有视频,所以需要在这里监听。这里注意一下,微信分享的时候onStop会调用2次,所以要try catch。4G切wifi的时候,要注意如果是用户手动暂停,是不需要自动播放的。
新闻页面
- 滑动停止后播放第一个完全可见的视频
public static void onScrollPlayVideo(RecyclerView recyclerView, int firstVisiblePosition, int lastVisiblePosition) { if (JZMediaManager.isWiFi) { for (int i = 0; i <= lastVisiblePosition - firstVisiblePosition; i++) { View child = recyclerView.getChildAt(i); View view = child.findViewById(R.id.player); if (view != null && view instanceof JZVideoPlayerStandard) { JZVideoPlayerStandard player = (JZVideoPlayerStandard) view; if (getViewVisiblePercent(player) == 1f) { if (JZMediaManager.instance().positionInList != i + firstVisiblePosition) { player.startButton.performClick(); } break; } } } } } 复制代码
这里使用的是播放中item的 position 去判断是否是第一个完全可见的视频,如果你的item的position会变(别问我为什么,真的会有这种情况,手动狗头),就要用
JZVideoPlayerManager.getCurrentJzvd() != player 复制代码
去判断。 计算view的可见百分比,范围是 0-1
public static float getViewVisiblePercent(View view) { if (view == null) { return 0f; } float height = view.getHeight(); Rect rect = new Rect(); if (!view.getLocalVisibleRect(rect)) { return 0f; } float visibleHeight = rect.bottom - rect.top; Log.d(TAG, "getViewVisiblePercent: emm " + visibleHeight); return visibleHeight / height; } 复制代码
- 部分滚出屏幕后停止播放
public static void onScrollReleaseAllVideos(int firstVisiblePosition, int lastVisiblePosition,float percent) { int currentPlayPosition = JZMediaManager.instance().positionInList; if (currentPlayPosition >= 0) { if ((currentPlayPosition <= firstVisiblePosition || currentPlayPosition >= lastVisiblePosition - 1)) { if (getViewVisiblePercent(JZVideoPlayerManager.getCurrentJzvd()) < percent) { JZVideoPlayer.releaseAllVideos(); } } } } 复制代码
- 无缝切换 上个版本的切换代码有点小小的瑕疵(Y坐标没有算对),所以这里还是贴一下移动的代码,留意一下这里的 translationY 的值
//第一版 holder.itemView.setTranslationY(attr.getY() - l[1]); holder.container.setScaleX(attr.getWidth() / (float) holder.container.getMeasuredWidth()); holder.container.setScaleY(attr.getHeight() / (float) holder.container.getMeasuredHeight()); //修改版 holder.itemView.setTranslationY(attr.getY() - l[1] - (holder.container.getMeasuredHeight() - attr.getHeight()) / 2); holder.container.setScaleX(attr.getWidth() / (float) holder.container.getMeasuredWidth()); holder.container.setScaleY(attr.getHeight() / (float) holder.container.getMeasuredHeight()); 复制代码
如果容器大小相同(视频列表页进入评论页),那直接用坐标相减就行,但这里对播放器的大小进行了改变,就需要减去高度差的一半,这里还要 除以2 是因为缩放的中心是view的中点。
这里由于用的JZVideoPlayer,需要固定播放容器的宽高,不然会触发view的onMeasure导致闪烁
视频列表页面
进入这个页面的时候需要分直接进入和视频播放进入两种情况。直接进入,就是直接添加fragment,再播放第一个视频,视频播放进入就是无缝切换效果。退出页面同理
PS:无缝切换的时候要留意一下,在新闻页,点击是直接进入视频列表,而在视频列表这里,点击是出现控制器的。在新闻页有个倒计时动画,而在视频列表页是没有的。这些在页面切换的时候,都需要进行对应的显示隐藏和点击事件的设置等等。
- 滑动停止后播放第一个完全可见视频 代码跟上面的差不多,不再赘述,详见demo 这里留意一下视频滑出区域后回收监听。
public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); if (dy != 0) { JZUtils.onScrollReleaseAllVideos(mLayoutManager.findFirstVisibleItemPosition(), mLayoutManager.findLastVisibleItemPosition(), 0.2f); } } 复制代码
这里的 onScrolled()
方法有个小小的坑(个人感觉)。
能什么坑,我一直都是这么用的呀。肯定有人这么想吧。 还是来看看这个方法的注释吧
* Callback method to be invoked when the RecyclerView has been scrolled. This will be * called after the scroll has completed. * <p> * This callback will also be called if visible item range changes after a layout * calculation. In that case, dx and dy will be 0. * * @param recyclerView The RecyclerView which scrolled. * @param dx The amount of horizontal scroll. * @param dy The amount of vertical scroll. */ public void onScrolled(RecyclerView recyclerView, int dx, int dy) 复制代码
当recyclerView滑动后,这个方法就会被回调。很正常对吧。可是下面还有两行呢。当可见item重新测量,布局后,也会触发这个方法, 此时dx,dy都是0 。这里要注意的就是我们这里是有全屏功能的,而且还会切换横竖屏,那就会触发这个方法。导致功能不正常了。所以上面加了个不为0的判断。
-
播放完毕自动播放下一个视频 这里需要留意一下,播放下一个视频我是通过滑动下一个视频到顶部从而触发播放的。可是也会有这种情况,就是你的视频特别少(我们的app就是),那就无法播放最后一个视频了。腾讯的数据量够大,一般不会有这个问题。所以当没有更多的时候,需要在recyclerView的底部插入一条数据,显示没有更多数据,就可以播放这个视频了,腾讯也是这么处理的,看得出来设计得很周全,一个页面只能完全显示一个视频,考虑得十分全面啊。
-
遮罩
遮罩这里用的是自定义view,画一个半通明的背景。
当列表播放时,显示遮罩,并且需要一个过渡的效果(透明度动画)。
当滑动界面,显示评论页,切换全屏,退出视频列表时,隐藏遮罩,不需要过渡效果。
onVideoSizeChanged()
if (JZMediaManager.instance().currentVideoWidth > JZMediaManager.instance().currentVideoHeight) { JZVideoPlayer.FULLSCREEN_ORIENTATION = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; if(JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN){ JZUtils.setRequestedOrientation(JZVideoPlayerManager.getCurrentJzvd().getContext(), ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } } else { JZVideoPlayer.FULLSCREEN_ORIENTATION = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; if(JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN){ JZUtils.setRequestedOrientation(JZVideoPlayerManager.getCurrentJzvd().getContext(), ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } } 复制代码
根据宽高设置进入全屏是竖屏还是横屏(如果你们公司非主流,不能根据视频宽高判断,那就后台加个字段设置吧)。
android P这里有个bug,切换屏幕方向的时候会黑屏,暂时未发现解决办法,知道怎么解决的大佬欢迎下方留言啊!!!另外,部分国产手机seekbar点击之后不是直接跳到对应的进度,而是快进一点点,也是服了呀...
- 全屏播放完毕后,切换url播放下一条,根据视频宽高和当前方向判断是否需要切换屏幕方向,并且滑动列表 切换url
public void changeUrl(String url, Object... objects) { this.currentUrlMapIndex = 0; this.seekToInAdvance = 0; LinkedHashMap map = new LinkedHashMap(); map.put(URL_KEY_DEFAULT, url); Object[] dataSourceObjects = new Object[1]; dataSourceObjects[0] = map; this.dataSourceObjects = dataSourceObjects; this.objects = objects; setState(CURRENT_STATE_PREPARING_CHANGING_URL); resetProgressAndTime(); } 复制代码
主要就是重置一些状态,改变变量的值。 判断方向 同样也会触发 onVideoSizeChanged()
方法,在里面进行判断就好了,其实就是上面那段代码啦。
PS:切换url的时候最好把画面渲染层隐藏起来,播放的时候再显示。不然的话部分机器可能会出现最后一帧的画面被拉伸的情况。
列表滑动
这里需要注意一下,我们上面对滑动进行了监听,不能调用 smoothScrollTo() 或者 smoothScrollBy() 方法。这里可以直接调用 scrollToPositionWithOffset() ,直接滑动到对应位置(如果你不是LinearLayoutManager,那就自己想办法吧。)
如果你跟我一样,都是用的JZVideoPlayer,那下面就要留意一下啦
JZVideoPlayer全屏跟非全屏用的是2个播放器,所以全屏的时候要做好状态跟接口的同步,并且退出全屏的时候,如果url不一样,就不能继续播放了。所以在滑动完毕后,需要更改第一个播放器。部分代码如下:
if (JZVideoPlayerManager.getCurrentJzvd().currentScreen == SCREEN_WINDOW_FULLSCREEN) { JZMediaManager.instance().positionInList++; JZVideoPlayerManager.getCurrentJzvd().changeUrl(mList.get(JZMediaManager.instance().positionInList).getVideoUrl()); mLayoutManager.scrollToPositionWithOffset(JZMediaManager.instance().positionInList, 0); mRecycler.postDelayed(new Runnable() { @Override public void run() { JZVideoPlayerManager.setFirstFloor((JZVideoPlayer) mRecycler.getChildAt(0).findViewById(R.id.player)); } }, 500); } 复制代码
进入和退出视频列表页进行无缝播放时,对播放器的父view进行了更改,也就会需要进行addView或者removeView,并且修改相关接口等等操作。
退出的逻辑比较复杂,来看看退出视频列表的处理的伪代码,进入的代码在VideoListAdapter的onBindViewHolder里,可对照上篇博客自行查看。
if (还在播放第一个视频) { videoListFragment.removeVideoList(); recycler.postDelayed(new Runnable() { @Override public void run() { JZMediaManager.instance().positionInList = clickPosition; int first = mLayoutManager.findFirstVisibleItemPosition(); View v = recycler.getChildAt(clickPosition - first); if (v != null) { final PlayerContainer container = v.findViewById(R.id.adapter_video_container); if (不是无缝播放进入视频列表页) { container.removeAllViews(); } //播放器接口,状态设置 } FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.remove(videoListFragment); transaction.commitAllowingStateLoss(); } }, 800); } else { JZVideoPlayer.releaseAllVideos(); FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.remove(videoListFragment); transaction.commitAllowingStateLoss(); if (是无缝播放进入视频列表页) { int first = mLayoutManager.findFirstVisibleItemPosition(); View v = recycler.getChildAt(clickPosition - first); if (v != null) { final PlayerContainer container = v.findViewById(R.id.adapter_video_container); container.removeAllViews(); //重新添加播放器 } } } 复制代码
还是解释一下吧,这里分4种情况:
- 进入和退出都是无缝播放,直接修改player的viewParent即可
- 进入和退出都不是无缝播放,不需要处理
- 进入是无缝播放,退出不是。也就是进入页面时移动了播放器,但退出时没有移回去。就需要在原来的位置添加一个一样的播放器
- 进入不是无缝播放,退出是。也就是退出的时候把播放器移到新闻页了,但原本新闻页时有一个播放器的。那就需要把原来的播放器移除,然后对移动的播放器进行接口,状态改变。
逻辑确实复杂,需要多看几遍
如果还没接入播放器,还是用 PlayerBase 吧。
评论页
评论页跟上一次没有大的区别,做了一点小小的改动:视频播放完毕后,会重置为普通状态,退出评论页返回视频列表页,会自动播放下一条。代码就不贴了,详见demo。 大功告成,喝杯82年雪碧庆祝一下吧。
下面是关于 动态加载ijkplayer so文件 的,不需要的可以跳过 动态加载so目前只见到这2种方案:
- 通过反射修改libPath,增加一个加载路径
- 使用 System.load(String filename) 代替 System.loadLibrary(String libname) 这里我选了第二种,对第一种感兴趣的可以到网上搜一搜,试一下。为了方便,demo中只是动态加载了其中一个so。 简单说一下实现步骤:使用IntentService检测so文件是否存在且完整(md5),是则切换ijk。否则请求接口(需传递支持的aibs)下载文件(aibs,断点续传,md5校检请自行处理,demo均没有实现),下载完毕后切换。注意一下下载的路径
File dir = getDir("libs", Context.MODE_PRIVATE); File soFile = new File(dir, "ijkffmpeg.so"); 复制代码
是soFile的路径。 然后就是加载so库,刚开始我以为直接把 IjkMediaPlayer.Java 拷出来修改加载路径就大功告成,可是却还是报错,找不到方法。查了下,发现JNI的方法名是需要 包名+类名+方法名 ,而我这里直接拷过来,包名变了,也就找不到方法了。所以 需要把ijk整个库拷下来,引入到项目里再进行修改(也可以修改so库中的包名)。 PS:如果你担心还是找不到so,可以这样做
try { jzMediaInterface.prepare(); } catch (Throwable e) { e.printStackTrace(); Object dataSource = JZMediaManager.getCurrentDataSource(); Log.e(TAG, "handleMessage: " + e.getMessage()); Toast.makeText(MyApplication.getInstance(), "so error", Toast.LENGTH_SHORT).show(); JZVideoPlayer.setMediaInterface(new JZExoPlayer()); jzMediaInterface.currentDataSource = dataSource; jzMediaInterface.prepare(); } 复制代码
捕获初始化错误,再切换回备用内核。
拖了好久终于把这个东西写完了,高难度的东西没多少,全都是细节的处理。虽然效果还可以,但还是逃不了上次说的问题,不能在activity间切换,逻辑复杂,耦合度太高。
顺便吐槽几句,一分钱一分货
好的产品是打磨出来的,不是赶出来的,希望某些人心中有点AC数
最后,附上 源码 ,有问题或者有更好的实现方式,欢迎下方留言,有空看到会回复的。
以上所述就是小编给大家介绍的《Android 视频无缝切换2.0》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 进阶运维:SSH无缝切换远程加密
- Linux安全运维进阶:SSH 无缝切换远程加密
- Flutter Web 网站之最简方式实现暗黑主题无缝切换
- 漂亮~pandas可以无缝衔接Bokeh
- H5移动端获奖无缝滚动动画实现
- 支持企业无缝上云,CynosDB应“云”而生
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。