关于近期遇到的坑-SurfaceView,RecylerView等

近期参与了一个多人视频通话的业务,在其中使用SurfaceView负责显示用户的视频流,没想到被坑死。。

关于显示和隐藏

背景: 由于是用六宫格的形式同时展示多人的视频,就选用了RecyclerView来实现,视频用SurfaceView展示,如果用户关掉了摄像头,使用语音模式就不显示SurfaceView,只显示用户头像就可以了。 因为之前仅仅用过SurfaceView当做显示摄像头数据,没有深入研究过,这次栽倒坑里了。

实现的方式是如果是视频模式就add一个surfaceView到一个FrameLayout(如果FrameLayout里面没有的话),如果用户切换到语音模式,就把那个FrameLayout设为GONE,显示头像。
流程看起来一点毛病没有,当收到用户切换模式的回调时,刷新该用户的item。问题来了,用户从视频模式->语音模式切换后,那个SurfaceView还依然显示,诡异的是再次刷新一下就切换了,但是已经差了一步,已经是错误的状态了,还有更诡异的是如果是当前用户点击按钮来切换自己的模式时却会立即改变,一点毛病没有。并且执行notifyDataSetChanged也是正常的。。如果每个用户的添加减少都全量刷新的话,肯定会黑一下,添加退出的动画也废了,这肯定没法接受。

一开始我是认为没有这个View没有失去焦点的原因,之前用的ViewStub,后来干掉,还是不行,用的自定义控件包装的整个多人视频的控件和逻辑,拆开还是不行,问题也不在这儿,因为是加在聊天界面的上面,怀疑是不是聊天的内容或者输入框抢占了焦点,测试了下别的控件,也没有出现这种情况。我想静静了。。

后来偶然发现stackoverflow一个问题(链接,说的是将SurfaceView的父控件setVisibility(GONE),并不能把SurfaceView隐藏,,,SurfaceView单独在一个Window之上,不和父控件在一个View树中。要想隐藏最简单的方式是直接remove掉。虽然这个问题解决了,但是之前点击按钮为啥是可以的,百思不得姐。。。

尽可能选择TextureView来替代SurfaceView吧,TextureView没有这种问题,但项目中因为用的第三方的库,对方只提供了surfaceView的方式,没办法,。。

RecylerView 数据源的坑

多人视频限制最多有6个人参加,如果参与的人少于6个最后面会显示一个邀请或者加入的item,也就是如果有2个人参与,救护显示3个item,5个人参与显示6个item,6个人参与也是6个item。开始的实现方案是,在Adapter的getItemCount里面判断,如果参与人数少于6个要讲参与视频的人数加1,如果已经有6个了,就加0。一有用户加入就调用notifyItemInserted,看似没问题,实际上第6个人加入时,全部崩掉,,好像报了类似越界的问题,在GridLayoutManager.onLayoutChildren crash,原因应该是第六个成员加入的时候,RecylerView认为会变成7个, 实际上getItemCOunt返回的是6个,位移的时候,当然crash掉。找到原因了,就在第6个加入的时候调用notifyItemChanged而不是notifyItemInserted。问题解决。后来又来需求,成员的顺序需要后台配置,每次用户加入的时候,需要去服务端拉一下列表,由于列表的顺序是不可期的,每次拉取后要和本地列表作比对,又不能全刷一次,好在谷歌更提供了DiffUtil,可以将两个列表进行比对,轻松进行位置的移动添加删除的操作,还能保留动画,perfect,果断换上。问题又来了,6个人的时候还是可能会有跟上面一样的crash,没办法,先catch掉再说。写了一个GridLayoutManager的子类,如果崩掉,就catch住,并且全量刷新列表。全量刷新列表带来的问题是全部都要黑一下,并且复现率还挺高。最后还是乖乖的把数据源改了,也就是在列表后面加一个假的mock item,专门用来表示邀请/加入按钮,在所有的add,remove类的写操作时都要check一下成员个数,判断是否要有这个mock item,并且check他的位置是不是在最后。这样后,就没再出问题。

关于局部刷新

在每个item的元素主要有视频,头像,麦克风标识,如果用户关掉麦克风,就要更改麦克风的图标,如果是视频模式的话,调用notifyItemChanged就会整个item刷新,带来的问题是视频黑一下。。所以用了局部更新,也就是使用 void notifyItemChanged(int position, Object payload) ,相应的会执行带payload的onBindViewHolder。但测试发现重写的带payload的onBindViewHolder死活不调用,后来跟踪发现因为使用了github.com/wasabeef/recyclerview-animators这个动画库,这个库并没有重写带payload的onBindViewHolder,好吧,坑,在AnimationAdapter.java重写一下就好了,实际上这个动画库后来也不用了。

关于动画makeSceneTransitionAnimation

makeSceneTransitionAnimation在5.x上有bug,点击A中RecyclerView的item,进入B,A列表刷新,返回A,Crash,报空指针,不知是不是跟有surfaceView 有关,因为在普通的列表刷新是没有问题的。在Android 6+上已经没有问题了,唉,,,坑货。。。