项目中的一个技术方案替换历程(surfaceview+fragment 变成悬浮窗window)

背景:
项目中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,所以遇到问题,我们要敢想思路和方案,遇到新方案带来的问题,我们要多思考,必要的时候,还是得多从源码入手。

你可能感兴趣的:(项目疑难杂症)