You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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与路由切换、键盘弹出的布局动画叠加导致的布局计算偏差,具体拆解为:

  1. TimeDilation的影响:timeDilation会放慢所有Flutter动画的速度,包括路由切换的过渡动画和键盘弹出时的布局调整动画。当动画被拉长时,Flutter的布局系统在计算视图位置时,可能会使用动画过程中的中间值(而非最终稳定值),导致页面预留的键盘空白区域计算错误。
  2. 路由跳转时机不当:FirstScreen在initState中直接通过addPostFrameCallback跳转,此时FirstScreen自身的布局还未完全稳定,路由切换动画和SecondScreen的键盘弹出动画同时触发,两种动画的时间缩放叠加,进一步加剧了布局计算的不同步。
  3. FocusNode的错误管理:在SecondScreen的build方法中每次都创建新的FocusNode,会导致焦点状态频繁变更,触发多次布局重绘,和TimeDilation的缩放效果叠加后,空白区域的计算错误被放大。
  4. 空白高度的阈值特性:当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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 10:58:12