Flutter - 滚动视图中的表单防遮挡 🗒
# 一、概述
最近遇到的一个需求上的优化点,在一个详情页中存在提交反馈信息的表单模块,这里简单的模拟了一下,这是运行后的效果
设计师验收的时候觉得这里有一点需要做下优化,那就是表单视图并不大,提交按钮需要完整的显示出来,讲道理要手动去计算偏移量还是挺麻烦的,但是如果结合我的 LinXunFeng/flutter_scrollview_observer (opens new window) 这个库就完全难不倒我们,咔咔两下就搞定了,优化后的效果如下:
接下来我讲一下具体的实现步骤。
# 二、布局
布局比较容易,简单过一下
# 滚动视图
使用 ListView
构建滚动视图
ScrollController scrollController = ScrollController();
Widget _buildScrollView() {
Widget resultWidget = ListView.builder(
controller: scrollController,
itemBuilder: (context, index) {
if (formIndex == index) {
return _buildForm();
}
return _buildImage();
},
itemCount: 10,
);
...
return resultWidget;
}
# 表单模块
FocusNode formFocusNode = FocusNode();
Widget _buildForm() {
Widget resultWidget = Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text(
'Feedback',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
// 输入框
TextField(
focusNode: formFocusNode,
),
// 提交按钮
Container(
width: double.infinity,
color: Colors.white,
alignment: Alignment.center,
margin: const EdgeInsets.only(top: 10.0),
child: TextButton(
child: const Text('Submit'),
onPressed: () {
// 使输入框失去焦点
formFocusNode.unfocus();
},
),
),
],
),
);
...
return resultWidget;
}
# 三、实战
由于滚动视图使用的是 ListView
,所以我们使用对应 ListViewObserver
将其包裹一层做观察
// 将滚动视图的 ScrollController 传给 ListObserverController
ListObserverController observerController = ListObserverController(controller: scrollController);
resultWidget = ListViewObserver(
controller: observerController,
// 关闭自动触发功能
autoTriggerObserveTypes: const [],
child: _buildScrollView(),
// 不实现观察回调
// onObserve: (resultModel) {}
);
注:请留意一下注释掉的
onObserve
回调,下面会做相关的版本更新说明。
// 表单模块的下标
final int formIndex = 3;
// 监听输入框的焦点状态
formFocusNode.addListener(handleFormFocus);
...
handleFormFocus() async {
// 我们只处理获得焦点的情况
if (!formFocusNode.hasFocus) return;
// 获得焦点后,等待键盘完全展示出来
await Future.delayed(const Duration(milliseconds: 600));
// 触发观察滚动视图
final result = await observerController.dispatchOnceObserve(
isForce: true, // 强制观察,不用对比上次的观察结果
isDependObserveCallback: false, // 不依赖观察回调,这样就可以正常返回观察结果
);
// 如果观察不成功则不用继续下去了
if (!result.isSuccess) return;
// 根据下标从观察结果中找出表单对应的数据
final formResultModel =
result.observeResult?.displayingChildModelList.firstWhere((element) {
return element.index == formIndex;
});
if (formResultModel == null) return;
// 进行滚动,使表单视图底部紧贴键盘显示出来
observerController.controller?.animateTo(
// 相减的逻辑在正文内说明
formResultModel.scrollOffset - formResultModel.trailingMarginToViewport,
duration: const Duration(milliseconds: 200),
curve: Curves.ease,
);
}
一些额外说明:
- 等待键盘完全展示出来的时长(
600 ms
)是测试出来的同时适用于iOS和安卓的一个值,如果你有更好的方式,可以留言分享一下 😁 - 在
1.16.0
版本之前,如果对应的观察回调(如:onObserve
)没有实现的话,内部的观察逻辑就无法进行下去,提前结束。而在1.16.0
版本之后对dispatchOnceObserve
方法进行了增强,可以通过对isDependObserveCallback
设置为false
避开这个逻辑,并且返回值也改造成了Future<ListViewOnceObserveNotificationResult>
,可直接拿到观察结果,相当方便! scrollOffset
是当前滚动视图的偏移量trailingMarginToViewport
是指当前item
到viewport
的底部距离,即表单模块视图的底部
到滚动视图的视窗底部
的间距。- 由于上述功能我们是依赖于键盘弹出来的视图高度发生变化的特性,键盘出来时视窗大小受到挤压缩小,所以
Scaffold
这里的resizeToAvoidBottomInset
属性请不要设置为false
。如果设置了false
,则滚动的偏移量需要手动加上键盘的高度~
return Scaffold(
resizeToAvoidBottomInset: true,
body: ...,
);
在理解完上面的内容后,我们再来看一下最终偏移量计算的逻辑,即
observerController.controller?.animateTo(
formResultModel.scrollOffset - formResultModel.trailingMarginToViewport,
duration: const Duration(milliseconds: 200),
curve: Curves.ease,
);
咱们配图食用,当键盘完全展示出来后,滚动视图的的视窗变小了, 由红色区域变成了蓝色区域,而此时表单模块也完全展示出来了,其底部到视窗之间的间距就是 trailingMarginToViewport
,为正数,但是我们需要让它紧贴底部,所以此时滚动视图的偏移量是多了的,需要减去这个 trailingMarginToViewport
,这便是最终偏移量计算的逻辑。
- 01
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08