Flutter问题:TimeDilation大于1.0时键盘上方出现异常空白区域
Flutter键盘异常空白问题:TimeDilation>1.0时路由跳转后出现空白区域
问题描述
我遇到了一个Flutter技术问题:当TimeDilation > 1.0时,从其他页面跳转到已唤起键盘的页面,键盘上方会出现一块异常空白区域。该空白区域的高度表现为:
- 在TimeDilation处于1.0到3.0之间时,与时间缩放比例成正比;
- 当TimeDilation ≥3.0时,空白高度保持恒定。
另外,若修改路由逻辑,不生成FirstScreen页面,则该问题不会出现;强制页面重绘也可作为临时解决办法。
问题复现代码
import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show timeDilation; import 'package:keyboard_actions/keyboard_actions.dart'; void main() async { // set the system top-level property [timeDilation] to control the animation speed timeDilation = 3.0; WidgetsFlutterBinding.ensureInitialized(); runApp( MyApp(), ); } class MyApp extends StatefulWidget { MyApp({Key? key}) : super(key: key); @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) { return MaterialApp( initialRoute: '/', onGenerateRoute: (RouteSettings settings) { final routes = <String, WidgetBuilder?>{ '/': (_) => FirstScreen(), '/screen2': (_) => SecondScreen(), }; final builder = routes[settings.name]!; return MaterialPageRoute( builder: builder, settings: settings, ); }, ); } } // ---------------------------------------------------------- class FirstScreen extends StatefulWidget { FirstScreen({Key? key}) : super(key: key); @override _FirstScreenState createState() => _FirstScreenState(); } class _FirstScreenState extends State<FirstScreen> { @override void initState() { WidgetsBinding.instance?.addPostFrameCallback((_) { Navigator.of(context).pushNamed('/screen2'); }); super.initState(); } @override Widget build(BuildContext context) { return Scaffold(); } } // ---------------------------------------------------------- class SecondScreen extends StatefulWidget { @override _SecondScreenState createState() => _SecondScreenState(); } class _SecondScreenState extends State<SecondScreen> with RouteAware, WidgetsBindingObserver { FocusNode? focusNode; ValueNotifier<String>? _stringValueNotifier; @override void initState() { super.initState(); WidgetsBinding.instance!.addObserver(this); this._stringValueNotifier = ValueNotifier<String>(''); this._stringValueNotifier!.addListener(() {}); } @override void dispose() { _stringValueNotifier?.dispose(); WidgetsBinding.instance!.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { this.focusNode = FocusNode(); return Scaffold( body: KeyboardContent( focusNode: this.focusNode!, stringValueNotifier: this._stringValueNotifier!, ), ); } } // ---------------------------------------------------------- class KeyboardContent extends StatelessWidget { final FocusNode focusNode; final ValueNotifier<String> stringValueNotifier; /// A container for an on-screen keyboard /// The widget automatically gets rebuilt on value change, triggered /// when a keyboard button is pressed. KeyboardContent({ required this.focusNode, required this.stringValueNotifier, }); KeyboardActionsConfig _buildConfig(BuildContext context) { List<Widget Function(FocusNode)> toolbarButtons = []; return KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.ALL, keyboardBarColor: Colors.grey[200]!, //nextFocus: false, actions: [ KeyboardActionsItem( focusNode: focusNode, displayActionBar: false, displayDoneButton: false, toolbarButtons: toolbarButtons, footerBuilder: (_) => CueKeyboard(), ) ]); } @override Widget build(BuildContext context) { if (!focusNode.hasPrimaryFocus) focusNode.requestFocus(); return Container( decoration: BoxDecoration(color: Colors.blue), child: KeyboardActions( config: _buildConfig(context), child: Container( padding: EdgeInsets.only(left: 15, right: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ KeyboardCustomInput<String>( focusNode: focusNode, notifier: stringValueNotifier, builder: (context, keyboardTapVal, hasFocus) { return Container( margin: EdgeInsets.only(top: 100), height: 800, width: 100, decoration: BoxDecoration( borderRadius: BorderRadius.circular(32.0), color: Colors.green)); }, ), ], ), ), ), ); } } class CueKeyboard extends StatefulWidget implements PreferredSizeWidget { static final double _keyboardHeight = 184.0; CueKeyboard({ Key? key, }) : super(key: key); @override _CueKeyboardState createState() => _CueKeyboardState(); @override Size get preferredSize => Size.fromHeight(CueKeyboard._keyboardHeight); } class _CueKeyboardState extends State<CueKeyboard> { dispose() { super.dispose(); } @override Widget build(BuildContext context) { return Wrap( children: [ Column( children: [Text("Keyboard")], ), ], ); } }
环境信息
Flutter Doctor 结果
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [√] Flutter (Channel stable, 2.8.1, on Microsoft Windows [Version 10.0.19044.1466], locale en-GB) [√] Android toolchain - develop for Android devices (Android SDK version 32.0.0) [√] Chrome - develop for the web [√] Android Studio (version 2020.3) [√] VS Code (version 1.63.2) [√] Connected device (4 available) • No issues found!
pubspec.yaml 内容
name: ucue_keyboard_bug description: Demo app showing the keyboard bug when time dialation=3 publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.0.1+1 environment: sdk: '>=2.12.0-0 <3.0.0' dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter keyboard_actions: ^3.4.0 pedantic: uuid: ^3.0.1 # Temporary dependencies dependency_overrides: dev_dependencies: flutter_test: sdk: flutter integration_test: sdk: flutter analyzer: strong-mode: true language: enableSuperMixins: true # The following section is specific to Flutter. flutter: uses-material-design: true
问题成因分析
这个问题的核心是全局动画时间缩放timeDilation与路由切换、键盘弹出的布局动画叠加导致的布局计算偏差,具体拆解为:
- TimeDilation的影响:
timeDilation会放慢所有Flutter动画的速度,包括路由切换的过渡动画和键盘弹出时的布局调整动画。当动画被拉长时,Flutter的布局系统在计算视图位置时,可能会使用动画过程中的中间值(而非最终稳定值),导致页面预留的键盘空白区域计算错误。 - 路由跳转时机不当:FirstScreen在
initState中直接通过addPostFrameCallback跳转,此时FirstScreen自身的布局还未完全稳定,路由切换动画和SecondScreen的键盘弹出动画同时触发,两种动画的时间缩放叠加,进一步加剧了布局计算的不同步。 - FocusNode的错误管理:在SecondScreen的
build方法中每次都创建新的FocusNode,会导致焦点状态频繁变更,触发多次布局重绘,和TimeDilation的缩放效果叠加后,空白区域的计算错误被放大。 - 空白高度的阈值特性:当
timeDilation ≥3.0时,空白高度恒定,是因为Flutter的动画帧率有上限,超过一定缩放比例后,动画的中间值不再增加,布局计算的偏差也就稳定下来。
彻底修复方案
针对上述成因,我们可以通过以下几个步骤彻底解决问题:
1. 修正路由跳转时机,确保布局稳定后再跳转
修改FirstScreen的initState,在页面布局完全稳定后再执行路由跳转,避免动画叠加冲突:
class _FirstScreenState extends State<FirstScreen> { @override void initState() { super.initState(); // 先等待当前帧布局完成,再通过微任务延迟到下一帧执行跳转 SchedulerBinding.instance.addPostFrameCallback((_) { Future.microtask(() { Navigator.of(context).pushNamed('/screen2'); }); }); } // ... 其余代码不变 }
2. 正确管理FocusNode的生命周期
将FocusNode的初始化移到SecondScreen的initState中,避免在build中重复创建,同时记得在dispose中释放资源:
class _SecondScreenState extends State<SecondScreen> with RouteAware, WidgetsBindingObserver { late FocusNode focusNode; // 使用late关键字延迟初始化 ValueNotifier<String>? _stringValueNotifier; @override void initState() { super.initState(); WidgetsBinding.instance!.addObserver(this); _stringValueNotifier = ValueNotifier<String>(''); focusNode = FocusNode(); // 在这里初始化FocusNode } @override void dispose() { focusNode.dispose(); // 释放FocusNode资源 _stringValueNotifier?.dispose(); WidgetsBinding.instance!.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: KeyboardContent( focusNode: focusNode, stringValueNotifier: _stringValueNotifier!, ), ); } }
3. 调整焦点请求的时机,避免在build中直接操作
在KeyboardContent中,将焦点请求放在postFrameCallback中,避免在build过程中触发布局重绘:
class KeyboardContent extends StatelessWidget { // ... 成员变量和构造函数不变 @override Widget build(BuildContext context) { // 在当前帧绘制完成后再请求焦点 WidgetsBinding.instance.addPostFrameCallback((_) { if (!focusNode.hasPrimaryFocus) { focusNode.requestFocus(); } }); // ... 其余代码不变 } }
4. 可选:禁用键盘相关动画(极端场景下使用)
如果上述方案仍无法解决问题,可以尝试禁用键盘弹出的动画,避免TimeDilation的影响:
在MaterialApp的theme中设置platform: TargetPlatform.android(Android平台默认无键盘动画):
class _MyAppState extends State<MyApp> { @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(platform: TargetPlatform.android), initialRoute: '/', onGenerateRoute: (RouteSettings settings) { // ... 路由代码不变 }, ); } }
内容的提问来源于stack exchange,提问作者Michael Davey
相关产品推荐
相关产品推荐

