Flutter坑之FlutterFragment中SafeArea失效的问题

原创
2020/11/02 09:48
阅读数 4.1K

背景:

最近有在做关于Android底部多tab下,对应多个Flutter Fragment的操作。又遇到一个比较坑的问题:FlutterFragment中的flutter页面的SafeArea失效(关于safeArea具体介绍参考官方SafeArea class),简单举例说一下SafeArea的作用:如果你有一刘海屏的手机,如果你的flutter内容为全屏,假如你的内容在全屏最顶部,那么所谓的刘海将会盖住你所想要的内容,如下图所示: 这当然不是我们想要的,于是Flutter官方推出:SafeArea这个属性,在dart语言中只需要在你的widget最外层包裹SafeArea就好了。

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: TabBarView(
          controller: mController,
          children: <Widget>[]
    );
  }

于是得到了正确的展示效果,如下图所示: 在这里插入图片描述 你以为这样就完了么?在多个Flutter Fragment中SafeArea的作用失效,尽管我在flutter中设置了SafeArea,但依然存在刘海盖住flutter content的情况。

原因分析:

这真的是一件很头疼的事情,对应的Flutter page在Flutter Activity中能够正常work,但是偏偏在Fluttter Fragment中就出问题了呢?于是又去看FlutterView源码,果然有收获!发现一个方法:onApplyWindowInsets()这里面有一大堆逻辑,很多都是关于处理 statusBar以及navigationBar,更惊喜地还发现了处理DisplayCutout的逻辑,这不就是刘海屏相关的类么!以下是部分代码逻辑:

 public final WindowInsets onApplyWindowInsets(WindowInsets insets) {
     ...
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
      int mask = 0;
      if (navigationBarVisible) {
        mask = mask | android.view.WindowInsets.Type.navigationBars();
      }
      if (statusBarVisible) {
        mask = mask | android.view.WindowInsets.Type.statusBars();
      }
     ...
      // TODO(garyq): Expose the full rects of the display cutout.
      // Take the max of the display cutout insets and existing padding to merge them
      DisplayCutout cutout = insets.getDisplayCutout();
      if (cutout != null) {
        Insets waterfallInsets = cutout.getWaterfallInsets();
        mMetrics.physicalPaddingTop =
            Math.max(
                Math.max(mMetrics.physicalPaddingTop, waterfallInsets.top),
                cutout.getSafeInsetTop());
       ...
      }
    } else {
      // Status bar (top) and left/right system insets should partially obscure the content
      // (padding).
    ...
    }

    updateViewportMetrics();
    return super.onApplyWindowInsets(insets);
  }

很明显这一块逻辑是处理刘海屏以及StatusBar相关的逻辑,于是进行相关的断点调试,发现FlutterFragment中的FlutterView的确是没有执行这个方法,对比同样在FlutterActivity中的FlutterView正常work并执行了这一串代码。

!那这不就神奇了么?这一下子又让人头秃了,这一定又是跟Fragment的相关机制导致的,自己对Fragment的具体处理逻辑不太熟,于是各种Google,找到两篇有点类似的答案: 1、fitsSystemWindows effect gone for fragments added via FragmentTransaction 2、一个Activity中添加多个Fragment导致fitsSystemWindows无效的问题

引入上面的解释说:

当第一个Fragment添加到Activity中的时候,Activity寻找出有fitsSystemWindows的子布局为其预留出状态栏的空间,其实就是设置一个padding,而其他Fragment添加到Activity中的时候,因为状态栏空间的适配已经被消费过一次了,Activity并不会再次去添加这个padding

虽然这里在进行fitsSystemWindows的操作,但是我们明确了一件事情:添加多个Fragment的时候,Activity对于padding相关操作只在第一个Fragment进行了相关处理逻辑。那么对应我们的FlutterFragment是否是同样的问题呢??

于是我进行了尝试,将Flutter Fragment放在Acitvity第一个需要展示的Fragment,经过尝试发现第一个FlutterFragment能正常work了!但之后的Flutter Fragment问题依然存在,那么我们可以肯定也就是说: 在多FlutterFragment中的FlutterView,只有在作为Acitivty添加为第一个Fragment的情况下才会去调用 onApplyWindowInsets(WindowInsets insets) 方法去处理一些statusBar相关的操作逻辑。 的确事实如此,经过尝试之后发现的确只会调用一次,那么如何解决呢?

解决方案:

参照上面的解决方案,可以写一个WindowInsetsFrameLayout继承FrameLayout,并setOnHierarchyChangeListener()监听Fragment的添加操作,在添加的时候执行 view的requestApplyInsets();

当然对于我们的问题并没有这么麻烦,我们在自己的FlutterFragment中手动去执行flutterView.requestApplyInsets();只需要执行时机保证在flutter渲染之前执行(Safe Area通过获去Native端onApplyWindowInsets()中传过去的params来执行相关渲染)

但还有一个问题:flutterView.requestApplyInsets();只能在Api大于20中使用,那么低于20呢?与其说低于20,不如直接说,19中怎么处理(Android 4.4 api 19引入的透明状态栏 、沉浸式相关),我们可以看到,在onApplyWindowInsets() 中最终是发送一个事件到flutter端,如下代码所示。

  private void sendViewportMetricsToFlutter() {
    if (!isAttachedToFlutterEngine()) {
      Log.w(
          TAG,
          "Tried to send viewport metrics from Android to Flutter but this "
              + "FlutterView was not attached to a FlutterEngine.");
      return;
    }

    viewportMetrics.devicePixelRatio = getResources().getDisplayMetrics().density;
    flutterEngine.getRenderer().setViewportMetrics(viewportMetrics);
  }

那么对于Api 19就可以对相关数据进行反射调用,之后再讲数据发送到flutter端即可,那么大致逻辑如下所示:

   public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
            flutterView.requestApplyInsets();
        } else {
            adapterStatusBarBelowApi20();
        }
    }

总结:

1、这个问题在官方的FlutterFragment中也存在,但不知道为什么没有修复,可能他们真的不太重视混合开发吧,一心在纯flutter开发中。 2、关于为什么Fragment 相关操作逻辑只在第一个被Fragment被添加,这里涉及到了太多底层的东西,这里没有赘述,打算深入研究,写一篇新到blog中去介绍。 3、Flutter坑实在是太多了,很多问题都与Android原生机制相关,这不得不让人对原生系统机制进行深入学习。

展开阅读全文
打赏
2
1 收藏
分享
加载中
打赏
0 评论
1 收藏
2
分享
返回顶部
顶部