Flutter:在使用 TextFormField 时打开键盘时动态调整小部件的大小

Flutter: Dynamically resize widgets when keyboard is opened when using TextFormField

打开键盘时,Flutter 报告溢出问题。这是打开键盘或验证表单或字段时我的视图的外观和行为:

有没有办法让顶部(“大标题”和“lorem ipsum”)小部件在发生这种情况时自动调整大小,以便表单字段和提交按钮可见?

这发生在元素位于 Column 小部件内的视图中。 我已经知道 resizeToAvoidBottomInset 但我不想使用它,因为我不想挡住键盘下方的任何小部件。 我也知道我可以使用 SingleChildScrollView 但我也不想使用它,因为我希望“提交”按钮在键盘启动时可见。

您可以查看 gist 以查看此确切代码:

import 'package:flutter/material.dart';
import 'dart:math';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _hasError = false;
  bool _usernameHasError = false;
  bool _passwordHasError = false;

  _handleFormSubmission() {
    final rand = Random();

    setState(() {
      _hasError = rand.nextBool();
    });

    print('Sumbmitted');
  }

  _handleUsernameChange(String value) {
    setState(() {
      _usernameHasError = value.isEmpty;
    });
  }

  _handlePasswordChange(String value) {
    setState(() {
      _passwordHasError = value.isEmpty;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Container(
      padding: const EdgeInsets.all(10.0).add(const EdgeInsets.only(top: 30.0)),
      child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Container(
              child: Column(children: [
                Text('Large title',
                    style: Theme.of(context).textTheme.headline2),
                Text(
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
                    'sed do eiusmod tempor incididunt ut labore et dolore magna'
                    ' aliqua. Ut enim ad minim veniam, quis nostrud exercitation '
                    'ullamco laboris nisi ut aliquip ex ea commodo consequat.',
                    style: Theme.of(context).textTheme.headline5),
              ]),
            ),
            Container(
              padding: const EdgeInsets.only(top: 10.0),
              child: Column(children: [
                Container(
                    alignment: Alignment.centerLeft,
                    padding: const EdgeInsets.only(bottom: 20.0),
                    child: Text('Additional information is placed here')),
                TextFormField(
                  decoration: InputDecoration(
                      labelText: 'Username',
                      errorText: _usernameHasError ? 'Invalid username' : null),
                  onChanged: _handleUsernameChange,
                ),
                TextFormField(
                  obscureText: true,
                  decoration: InputDecoration(
                      labelText: 'Password',
                      errorText: _passwordHasError ? 'Invalid password' : null),
                  onChanged: _handlePasswordChange,
                ),
                _hasError
                    ? Container(
                        padding: const EdgeInsets.symmetric(vertical: 20.0),
                        child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Icon(Icons.error,
                                  color: Theme.of(context).errorColor),
                              Text('An error occurred!',
                                  style: TextStyle(
                                      color: Theme.of(context).errorColor))
                            ]))
                    : Container()
              ]),
            ),
            Container(
                padding: const EdgeInsets.only(top: 10.0),
                child: Column(children: [
                  Container(
                      width: double.infinity,
                      child: ElevatedButton(
                        child: Text('Submit'),
                        onPressed: _handleFormSubmission,
                      )),
                ])),
          ]),
    ));
  }
}

当您查看我的 GIF 时,您还可以看到键盘弹起并不是唯一可以调整视图大小的东西,还有可能弹出的错误消息。

我认为一个好的解决方案是动态调整顶部文本“大标题”和“Lorem ipsum”文本的大小,使表单和按钮 space。

我不确定是否有可靠的方法来检测键盘何时启动,但我在 this question 中阅读过有关检查 MediaQuery.of(context).viewInsets.bottom 的内容。当键盘打开时,我可以调整文本的大小(或隐藏它)。这是可行的解决方案吗?

或者可能有一些我不知道的布局?为顶部的两个小部件设置动画效果会很好,这样会逐渐消失 and/or 减小尺寸(也许 AnimatedContainer)?这仍然需要以编程方式检查打开的键盘。

如有任何建议,我们将不胜感激!

因为你想让提交按钮在打开键盘时可见,你可以像下面那样做。我对您的代码本身进行了一些更改

    return Scaffold(
    body: Column(
  children: [
    Expanded(
      child: SingleChildScrollView(
        child: Container(
          padding: const EdgeInsets.all(10.0)
              .add(const EdgeInsets.only(top: 30.0)),
          child: Column(
              mainAxisSize: MainAxisSize.max,
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Container(
                  child: Column(children: [
                    Text('Large title',
                        style: Theme.of(context).textTheme.headline2),
                    Text(
                        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
                        'sed do eiusmod tempor incididunt ut labore et dolore magna'
                        ' aliqua. Ut enim ad minim veniam, quis nostrud exercitation '
                        'ullamco laboris nisi ut aliquip ex ea commodo consequat.',
                        style: Theme.of(context).textTheme.headline5),
                  ]),
                ),
                Container(
                  padding: const EdgeInsets.only(top: 10.0),
                  child: Column(children: [
                    Container(
                        alignment: Alignment.centerLeft,
                        padding: const EdgeInsets.only(bottom: 20.0),
                        child:
                            Text('Additional information is placed here')),
                    TextFormField(
                      decoration: InputDecoration(
                          labelText: 'Username',
                          errorText: _usernameHasError
                              ? 'Invalid username'
                              : null),
                      onChanged: _handleUsernameChange,
                    ),
                    TextFormField(
                      obscureText: true,
                      decoration: InputDecoration(
                          labelText: 'Password',
                          errorText: _passwordHasError
                              ? 'Invalid password'
                              : null),
                      onChanged: _handlePasswordChange,
                    ),
                    _hasError
                        ? Container(
                            padding:
                                const EdgeInsets.symmetric(vertical: 20.0),
                            child: Row(
                                mainAxisAlignment: MainAxisAlignment.center,
                                children: [
                                  Icon(Icons.error,
                                      color: Theme.of(context).errorColor),
                                  Text('An error occurred!',
                                      style: TextStyle(
                                          color:
                                              Theme.of(context).errorColor))
                                ]))
                        : Container()
                  ]),
                ),
              ]),
        ),
      ),
    ),
    Container(
        padding: const EdgeInsets.only(top: 10.0),
        child: Column(children: [
          Container(
              width: double.infinity,
              child: ElevatedButton(
                child: Text('Submit'),
                onPressed: _handleFormSubmission,
              )),
        ])),
  ],
));

希望对您有所帮助。

我考虑了 Neela 的回答,然后开始尝试一些不同的方法。 我发现足够的一个是隐藏 lorem ipsum 文本,因为在我的上下文中,当表单字段被聚焦时它并不重要。

我使用 flutter_keyboard_visibility 插件来检测键盘何时启动,然后有条件地删除了文本小部件。

完整代码(也在gist中):

import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'dart:math';
import 'dart:async';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key}) : super(key: key);
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _hasError = false;
  bool _usernameHasError = false;
  bool _passwordHasError = false;
  bool _keyboardVisible = false;

  final KeyboardVisibilityController _keyboardVisibilityController =
      KeyboardVisibilityController();
  StreamSubscription<bool>? _keyboardVisibilitySubscription;

  @override
  void initState() {
    super.initState();
    _keyboardVisibilitySubscription = _keyboardVisibilityController.onChange
        .listen(_handleKeyboardVisibilityChange);
  }

  @override
  void dispose() {
    _keyboardVisibilitySubscription?.pause();
    _keyboardVisibilitySubscription?.cancel();
    super.dispose();
  }

  void _handleKeyboardVisibilityChange(bool visible) {
    setState(() {
      _keyboardVisible = visible;
    });
  }


  _handleFormSubmission() {
    final rand = Random();

    setState(() {
      _hasError = rand.nextBool();
    });

    print('Sumbmitted');
  }

  _handleUsernameChange(String value) {
    setState(() {
      _usernameHasError = value.isEmpty;
    });
  }

  _handlePasswordChange(String value) {
    setState(() {
      _passwordHasError = value.isEmpty;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('\nkeyboard is visible $_keyboardVisible');
    return Scaffold(
        body: Container(
      padding: const EdgeInsets.all(10.0).add(const EdgeInsets.only(top: 30.0)),
      child: Column(
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Container(
              child: Column(children: [
                Text('Large title',
                    style: Theme.of(context).textTheme.headline2),
                _keyboardVisible ? Container() : Text(
                    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
                    'sed do eiusmod tempor incididunt ut labore et dolore magna'
                    ' aliqua. Ut enim ad minim veniam, quis nostrud exercitation '
                    'ullamco laboris nisi ut aliquip ex ea commodo consequat.',
                    style: Theme.of(context).textTheme.headline5),
              ]),
            ),
            Container(
              padding: const EdgeInsets.only(top: 10.0),
              child: Column(children: [
                Container(
                    alignment: Alignment.centerLeft,
                    padding: const EdgeInsets.only(bottom: 20.0),
                    child: Text('Additional information is placed here')),
                TextFormField(
                  decoration: InputDecoration(
                      labelText: 'Username',
                      errorText: _usernameHasError ? 'Invalid username' : null),
                  onChanged: _handleUsernameChange,
                ),
                TextFormField(
                  obscureText: true,
                  decoration: InputDecoration(
                      labelText: 'Password',
                      errorText: _passwordHasError ? 'Invalid password' : null),
                  onChanged: _handlePasswordChange,
                ),
                _hasError
                    ? Container(
                        padding: const EdgeInsets.symmetric(vertical: 20.0),
                        child: Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Icon(Icons.error,
                                  color: Theme.of(context).errorColor),
                              Text('An error occurred!',
                                  style: TextStyle(
                                      color: Theme.of(context).errorColor))
                            ]))
                    : Container()
              ]),
            ),
            Container(
                padding: const EdgeInsets.only(top: 10.0),
                child: Column(children: [
                  Container(
                      width: double.infinity,
                      child: ElevatedButton(
                        child: Text('Submit'),
                        onPressed: _handleFormSubmission,
                      )),
                ])),
          ]),
    ));
  }
}

我相信这可以做得更好,例如当小部件从小部件树中删除时将其动画化。这是基本的并且有效。

如您所见,溢出不再发生,因为现在有更多的垂直 space: