背景:
最近有在做关于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原生机制相关,这不得不让人对原生系统机制进行深入学习。