0%

Android中Flutter容器变化导致拉伸解决方案

纯 Flutter 开发应该不会遇到这个问题,但是如果是已有一个原生工程中集成 Flutter,在 Activity 里面内嵌 FlutterView,当屏幕旋转、键盘弹出等导致 FlutterView 容器大小出现突变时,Flutter 界面会有很明显的拉伸。

先说解决方案

固定宽高

不修改 Flutter 引擎最稳妥的解决方案。宽高不要设置为 MATCH_PARENT (根据实际情况,不会突变的可以设置为 MATCH_PARENT), 而是设置为固定值。在 Flutter 侧底部或者右边留空白 Container,通过动态控制 Container 的宽高来实现业务效果,如业务视图的高度变化。如果是键盘场景,原生通过把键盘高度传递给 Flutter,让空白 Container 的高度跟键盘高度一致来控制输入框的弹起。

示例:

示例中的场景是一个可以拖动大小变化的底部弹出框,Flutter 的内容包含标题栏、聊天内容、输入框

根据以上的解决方案,在 Flutter 的底部加一个空白 (高度为 blankViewHeight

@override
Widget build(BuildContext context) {
Color color = randomLightColor();
return MaterialApp(
debugShowCheckedModeBanner: true,
home: Scaffold(
resizeToAvoidBottomInset: false,
body: Container(
color: color,
child: Builder(
builder: (context) {
return Column(
children: [
// 需要固定不动的 Widget
Container(
height: 50, // 设置适合的高度
color: color, // 为了方便看出效果,我们给它一个颜色
child: Center(child: Text('固定的 Widget')),
),
// 可以滚动的部分
Expanded(
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: List<Widget>.generate(
100,
(index) => Container(
color: color,
child: Text(
'Item $index 大段文字文字文字大段文字文字文字大段文字文字文字大段文字文字文字'),
)),
),
),
),
// 输入框栏
Container(
height: 50, // 输入框栏的高度
color: color, // 设置颜色以区分
child: TextField(
decoration: InputDecoration(hintText: '输入内容...'),
),
),
Container(
height: blankViewHeight, //空白区域
)
],
);
},
),
),
),
);
}

动态控制空白区域的高度:

channel.setMethodCallHandler((call) async {
if (call.method == 'setCellNumber') {
setState(() {
blankViewHeight = call.arguments as double;
});
}
});

原生侧固定 FlutterView 高度固定为 Activity 的高度,BottomSheetDialog 变化时,计算好 Flutter 底部需要留出来的高度,给到 Flutter:

mBottomSheetDialog.behavior.addBottomSheetCallback(object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == STATE_HALF_EXPANDED || newState == STATE_EXPANDED || newState == STATE_COLLAPSED) {

// 获取 container 的高度
val containerHeight = (view.parent as ViewGroup).measuredHeight

// 获取 bottomSheet 的 Y 值
val bottomSheetY = bottomSheet.y.toInt()

// 计算差值
val newHeight = containerHeight - bottomSheetY
if (newHeight < 100) {
return
}
flutterChannel.invokeMethod(
"setCellNumber",
this@MoveActivity.pxToDp(bottomSheet.y)
)
}
}
}

ScrollView 嵌套 FlutterView

还是固定 FlutterView 的高度,通过监听屏幕旋转,addOnGlobalLayoutListener 键盘弹出之后确定 FlutterView 的高度,直接设置即可。这种好处是可以利用 ScrollView 对键盘的处理,键盘不会遮挡 Flutter 输入框,不需要 Flutter 配合处理,缺点是处理 FlutterView 连续变化还是有问题。

其它知识点:

  • 通过查看调试 FlutterView 的源码可以发现 ,触发 onSizeChanged 或者 onApplyWindowInsets 都可以让 Flutter 进行重绘。windowInset 可以实现原生侧让 Flutter 留白,可以按需使用

  • 最常见的 setLayoutParams 可以让 FlutterView 的大小变化,但是这个操作太快的话,Flutter 绘制会异常导致大小突变,如果我们给 setLayoutParams 加一个 16ms 的防抖,FlutterView 可以正常实现动画效果。

    这种方案适合代码主动给 FlutterView 做动画,如果是用户手指拖动导致的变化就不行,因为拖快了之后位移的 x,y太大,还是16ms 才能绘制一帧的话也会界面异常

    例如给 FlutterView 做一个减小 400dp 高度的动画:

    ValueAnimator.ofInt(lp.height, lp.height - dip2px(this@MainActivity, 400))
    var lastUpdateTime = System.currentTimeMillis()
    animator.addUpdateListener { valueAnimator ->
    val currentTime = System.currentTimeMillis()
    if (currentTime - lastUpdateTime < 16) {
    return@addUpdateListener
    }
    lastUpdateTime = currentTime
    val lp1 = binding.rootView.layoutParams
    lp1.height = valueAnimator.animatedValue as Int
    binding.rootView.layoutParams = lp1
    }
    animator.duration = 300
    animator.start()