在视频聊天app中一般会有这样的场景,一个大的Surface显示对方的画面,一个略小的Surface显示自己的画面(类似于微信视频聊天),然后点击一下小的那个Surface,会把两个画面对调。今天就来看看,这个是怎么实现的。
先给大家一个直观的认识,来两张截图。
就是这两个画面,一大一小,大的是自己摄像头的预览画面,小的是对面传过来的画面(现在暂时是这样的。)点击小的画面里面右上角按钮,就可以切换两个换面。具体如何实现,会碰到哪些问题呢?
####方案一:采用切换数据源
这是什么意思呢?就是说,Surface的大小和位置不去改变他,数据绘制的位置,来实现,这个方案且不说实现起来复杂度和可行性,有一个致命的问题,就是在切换Surface的过程中,必然会有重新加载的过程,会导致短暂的黑屏(取决于网络和缓存时间),所以果断放弃了。
####方案二:改变Surface大小,并改变层级
这个方案理论上是可行的,只是改变下Surface的大小和位置,大的变小,小的变大,然后大的把小的覆盖掉了,再把小的显示到上面来。但是有个问题,修改层级的方法SurfaceView#setZOrderMediaOverlay有个限制,只能在初始的的时候调用,不信可以看官方注释:
/**
* Control whether the surface view's surface is placed on top of another
* regular surface view in the window (but still behind the window itself).
* This is typically used to place overlays on top of an underlying media
* surface view.
*
* <p>Note that this must be set before the surface view's containing
* window is attached to the window manager.
*
* <p>Calling this overrides any previous call to {@link #setZOrderOnTop}.
*/
public void setZOrderMediaOverlay(boolean isMediaOverlay) {
mWindowType = isMediaOverlay
? WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY
: WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
}
Note that this must be set before the surface view’s containing window is attached to the window manager.应该可以看得懂吧。
所以就得想个办法来绕开这个这个限制,最简单的就是先removeView(View)再addView(View)这样就相当于重新做了attach操作,setZOrderMediaOverlay操作也就可以生效了。接下来看看代码具体如何实现
private RelativeLayout.LayoutParams getSmallLayoutParams() {
RelativeLayout.LayoutParams lpSmall = null;
lpSmall = new RelativeLayout.LayoutParams(mWindowWidth / 2, mWindowHeight / 2);
lpSmall.width = mWindowWidth / 2;
lpSmall.height = mWindowHeight / 2;
lpSmall.topMargin = mWindowHeight / 2;
lpSmall.leftMargin = 0;
return lpSmall;
}
private RelativeLayout.LayoutParams getNormalLayoutParams() {
RelativeLayout.LayoutParams lpNormal = null;
lpNormal = new RelativeLayout.LayoutParams(mWindowWidth / 2, mWindowHeight / 2);
lpNormal.width = mWindowWidth;
lpNormal.height = mWindowHeight;
lpNormal.topMargin = 0;
lpNormal.leftMargin = 0;
return lpNormal;
}
private void changeScreen() {
mIsChangeScreen = !mIsChangeScreen;
RelativeLayout.LayoutParams lpSmall = getSmallLayoutParams();
RelativeLayout.LayoutParams lpNormal = getNormalLayoutParams();
mRlRoot.removeView(mSurfaceViewSmall);
mRlRoot.removeView(mSurfaceView);
mRlRoot.removeView(mRlFun);
if (mIsChangeScreen) {
mSurfaceView.setLayoutParams(lpSmall);
mRlRoot.addView(mSurfaceView);
mSurfaceViewSmall.setLayoutParams(lpNormal);
mRlRoot.addView(mSurfaceViewSmall);
// mSurfaceView.setZOrderOnTop(true);
mSurfaceView.setZOrderMediaOverlay(true);
} else {
mSurfaceViewSmall.setLayoutParams(lpSmall);
mRlRoot.addView(mSurfaceViewSmall);
mSurfaceView.setLayoutParams(lpNormal);
mRlRoot.addView(mSurfaceView);
// mSurfaceViewSmall.setZOrderOnTop(true);
mSurfaceViewSmall.setZOrderMediaOverlay(true);
}
mRlRoot.addView(mRlFun);
}
点击小画面右上角按钮的时候,调用changeScreen()方法,这个代码其实不难理解,先removeView调大小两个Surface,修改LayoutParams后一次addView进去,然后通过调用setZOrderMediaOverlay方法修改层级关系,就实现了大小和层级的切换。
这里还有一个mRlRoot.removeView(mRlFun);和mRlRoot.addView(mRlFun);其实是把其他控件的移除和加入操作,目的是防止Surface把其他控件覆盖调。
####tips: setZOrderOnTop&setZOrderMediaOverlay
这里再简单讲解下这两个方法的区别,我再网上查到很多关于多个Surface重叠显示,放置遮盖其他控件的解决方法,都是说先调用setZOrderOnTop(true)方法,再调用setZOrderMediaOverlay(true),先不评价这个方法的正确性(因为这样确实可以达到目的),我们先看看这两个方法的含义。
setZOrderMediaOverlay方法的源码注释我已经在前面有贴,意思就是"把目标Surface显示在其他Surface之前,如果以前有其他控件覆盖,任然会覆盖在它上面"
然后setZOrderOnTop方法,
/**
* Control whether the surface view's surface is placed on top of its
* window. Normally it is placed behind the window, to allow it to
* (for the most part) appear to composite with the views in the
* hierarchy. By setting this, you cause it to be placed above the
* window. This means that none of the contents of the window this
* SurfaceView is in will be visible on top of its surface.
*
* <p>Note that this must be set before the surface view's containing
* window is attached to the window manager.
*
* <p>Calling this overrides any previous call to {@link #setZOrderMediaOverlay}.
*/
public void setZOrderOnTop(boolean onTop) {
if (onTop) {
mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
// ensures the surface is placed below the IME
mLayout.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
} else {
mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
mLayout.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
}
}
这个方法的意思就是“调用之后,会把目标Surface显示到window的最前面,包括其他控件”
意思已经很明显了 ,我们在Surface上要显示一下其他控件,比如小画面右上角的按钮,所以调用setZOrderMediaOverlay(true)是最好的,进过试验,只调用setZOrderMediaOverlay(true)就可以达到想要的结果。
所以我就不太理解,为什么那么多博文要说同时调用两个方法呢。总结下自己的经验,希望对大家有所帮助。
最近研究了一下,只需要setZOrderMediaOverlay,来切换SurfaceView的层级关系。不过需要注意的是:
1.上层的SurfaceView需要调用setZOrderMediaOverlay(true),底层的SurfaceView需要调用setZOrderMediaOverlay(false)
2.需要切换的两个SurfaceView必须要有大小变化
3.自测安卓8 9 10版本都是可以的,但是安卓6会有问题