背景:
项目中UI层有SurfaceView,其渲染展示的是摄像机等采集画面,但是测试提了一个问题单,如果在当前页面中跳出到其他页面,会crash,经过log分析,是由于surfaceview 在失去焦点的时候会走到onDestroy方法,也就是surfaceview会失效。
解决思路:surfaceview不失去焦点就可以了,改用悬浮窗实现。
UI层 最主要的页面结构如下:
// 摄像机画面
//叠在摄像机画面上的fragment
页面底层是摄像机画面,叠在上面的是fragment,用 framelayout 作为容器去承载,surfaceview既然要作为悬浮窗中去展示,因为悬浮窗的层级比Activity页面高,所以fragment层页面当然也要放到悬浮窗,否则页面就没法操作了。
1、先检查悬浮窗权限。
if (!Settings.canDrawOverlays(this)) {
//启动权限页面
startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())), 0);
} else {
//添加悬浮窗
addWindowSurfaceView();
}
2、创建悬浮窗。
// add window
private void addWindowSurfaceView() {
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
mRootViewLayout = new RootViewLayout(this);
mRootViewLayout.setOrientation(LinearLayout.VERTICAL);
LayoutInflater.from(this).inflate(R.layout.activity_main, mRootViewLayout);
WindowManager.LayoutParams layoutParams = createDefaultWindowLayoutParams();
windowManager.addView(mRootViewLayout, layoutParams);
}
// 添加默认的悬浮窗参数
private WindowManager.LayoutParams createDefaultWindowLayoutParams() {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
layoutParams.height = ScreenUtils.getScreenHeight(this);
layoutParams.width = ScreenUtils.getScreenWidth(this);
return layoutParams;
}
这块代码还是比较简单的,在跳到其他页面的时候,将悬浮窗设置成1x1 px的大小,再次回来又恢复成默认大小。
// 退到后台
private void updateWindowSufaceViewOnStop() {
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
layoutParams.height = 1;
layoutParams.width = 1;
//设置成不可获取焦点
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
windowManager.updateViewLayout(mRootViewLayout, layoutParams);
}
//再次回来
private void updateWindowSufaceViewOnResume() {
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
WindowManager.LayoutParams layoutParams = createDefaultWindowLayoutParams();
windowManager.updateViewLayout(mRootViewLayout, layoutParams);
}
不过需要注意的是,在退到后台的时候,需要将虚浮窗设置成不可获取焦点
以为就这么简单就结束了?No,绝非那么simple,重点来了。
【问题一】run 一下项目,crash掉了,报如下的错误。
No view found for id 0x7f080088 (com.xxxxxx:id/fragment_layout) for fragment LauncherFragment{ab9831c #0 id=0x7f080088 LauncherFragment}
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1413)
at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1740)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1809)
at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:799)
at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2580)
从日志中看,是没有找到承载fragment的layout的资源,我们知道fragment是依附于Activity的,所以这个view没有被找到,是不是用Activity上下文findviewbyId没有获取到?下面继续我一贯的源码分析,当然这次比较不贴很多代码~~.
FragmentManager中moveState方法中关键添加fragment 视图的代码如下:
// 1、寻找装载fragment的容器
container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
f.mContainer = container;
// XX 省略了无用的代码
// 2、创建fragmentView
f.mView = f.performCreateView(f.getLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
// 3、添加到容器中
if (container != null) {
container.addView(f.mView);
}
从这段源码以及日志中看到,container 是通过mContainer(Activity)去找的,日志报的错,说明findViewbyId没找到,我们去Activity中看下这个方法
@Nullable
@Override
public View onFindViewById(int id) {
return Activity.this.findViewById(id);
}
/**
* Finds a view that was identified by the id attribute from the XML that
* was processed in {@link #onCreate}.
*
* @return The view if found or null otherwise.
*/
@Nullable
public View findViewById(@IdRes int id) {
return getWindow().findViewById(id);
}
从这段源码中,可以清楚的看到,是从Activity的setContentView中加载资源的,由于我们这里采用了悬浮窗,所以自然没有办法从中获取到资源,那怎么改?
既然我们知道了原因,那就好入手了啊,实现 override findViewById方法,然后从我们的悬浮窗根view中去加载。
@Override
public T findViewById(int id) {
return mRootViewLayout.findViewById(id);
}
mRootViewLayout 就是悬浮窗根layout。
好了,第一个棘手的问题解决了,但是其他的问题又来了,我们现在的方案是悬浮窗,key 事件分发就不走Activity了,而我们按返回键本应该会弹出fragment的
【问题二】按返回键等,不走onBackPressed,fragment没有被弹出去。
原因我已经讲了,原生的back事件,是会经过Activity的onBackPressed方法,继而调用fragmentManager的popFragment方法,但是现在悬浮窗的层级在Activity之上,它优先获取到了焦点事件,key事件等都处理给它。
/**
* Called when the activity has detected the user's press of the back
* key. The default implementation simply finishes the current activity,
* but you can override this to do whatever you want.
*/
public void onBackPressed() {
if (mActionBar != null && mActionBar.collapseActionView()) {
return;
}
//弹出fragment
if (!mFragments.getFragmentManager().popBackStackImmediate()) {
finishAfterTransition();
}
}
那怎么解决?
写一个封装的layout,作为悬浮窗的根viewGroup。重写其dispatchKeyEvent方法,在这个方法中根据其keyCode进行判断处理。
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if(event.getAction()==KeyEvent.ACTION_DOWN && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
mFragmentManager.popBackStack();
}
return super.dispatchKeyEvent(event);
}
搞定!,当然这里面还可以继续进行其他key事件判断。
心得:这个方案运行了几个版本,crash也解决了,也没有引入新的bug,所以遇到问题,我们要敢想思路和方案,遇到新方案带来的问题,我们要多思考,必要的时候,还是得多从源码入手。