Flutter: TextFieldを別クラスに配置すると、キーボード位置(viewInsets.bottom)を取得できない
結論:「resizeToAvoidBottomInset: false」を設定する
2022/11/24 Flutter エラー・バグ日記
TextFieldにフォーカスが当たってキーボードが出たとき、描画範囲オーバーのエラーを防ぐため、
MediaQuery.of(context).viewInsets.bottom
を用いて、キーボード上端の位置(縦幅)を把握し、TextFieldの縦幅を動的に調整していた。
しかし、TextFieldをScaffold内に直接配置せず、別クラスに分離したところ、この調整ができなくなってしまった。
Scaffold直下に配置していた場合
元々は以下のようなコードだった。
// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの) // の名前の末尾には、大文字のオー「O」をつけています // ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、 // 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため import 'package:flutter/material.dart'; void main() => runApp(MyApp()); // 画面の長さ情報のプロパティを定義(簡易化のためグローバル変数で定義) late double screenHeightO; // 画面の縦幅 late double statusBarHeightO; // ステータスバーの高さ late double appBarHeightO; // AppBarの高さ const double containerHeightO = 50.0; // TextField上部のContainerの高さ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Test", theme: ThemeData.light(), // 変動することのない画面の縦幅、AppBarの高さ、ステータスバーの高さを先に取得しておく builder: (BuildContext context, Widget? child) { screenHeightO = MediaQuery.of(context).size.height; statusBarHeightO = MediaQuery.of(context).padding.top; appBarHeightO = AppBar().preferredSize.height; return child!; }, home: SampleScreen(), ); } } class SampleScreen extends StatefulWidget { @override _SampleScreenState createState() => _SampleScreenState(); } class _SampleScreenState extends State<SampleScreen> { @override Widget build(BuildContext context) { debugPrint("全体を描画"); // 動的にキーボードの位置(縦幅)を取得 double textFieldBottomO = MediaQuery.of(context).viewInsets.bottom; debugPrint("textFieldBottomO = $textFieldBottomO"); return Scaffold( appBar: AppBar( title: Text("TestApp"), ), body: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Container( child: Text("TextField"), alignment: Alignment.center, color: Colors.green[200], height: containerHeightO, ), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), // TextFieldの高さを計算して算出 // 画面の縦幅 - ステータスバーの高さ - AppBarの高さ - 上下パディング - 上部Containerの高さ - キーボードの縦幅 height: screenHeightO - statusBarHeightO - appBarHeightO - 8.0 * 2 - containerHeightO - textFieldBottomO, child: TextField( decoration: InputDecoration( border: InputBorder.none, ), keyboardType: TextInputType.multiline, maxLines: null, ), ), ], ), ), ); } }
画面の縦幅から、ステータスバーの高さ、AppBarの高さ、上下パディング、上部Containerの高さ、キーボードの縦幅を差し引くことで、TextFieldの縦幅を計算している。
キーボードが出現すると、「MediaQuery.of」メソッドが呼び出されて、
キーボードの縦幅 = MediaQuery.of(context).viewInsets.bottom
を取得し、TextFieldの縦幅が再計算・再描画される仕様。
※下記公式サイトに説明があるとおり、「MediaQuery.of」メソッドを発動すると、buildメソッドが再実行(Widgetを再描画)される。
下位クラスに分離した場合
ただ、上記コードだと、キーボードが出るたびに、「Scaffold」以下の全てのWidgetが再描画されてしまう。
そこで、再描画範囲を限定するために、TextField部分を別クラスに切り出し、「MediaQuery.of」メソッドも別クラス側に配置することにした。
しかし、、この方法でビルドすると、キーボード出現時にエラーになってしまった。
コンソールで数値を確認したところ、「viewInsets.bottom」の値が「0」のままで、キーボード縦幅を取得できていない模様。
実は以前から解決策の情報があった模様(GitHub、StackOverflow)
調べたところ、下記に情報があった。もしかすると常識だったのかもしれない。。
Scaffoldの「resizeToAvoidBottomInset」プロパティを「false」に設定すれば良いとのこと(デフォルトだと「true」に設定されている)。
スクリーンキーボードとWidgetが重ならないように、サイズ調整するか否かを決めるプロパティらしい。
確かに、この1行を入れるだけで、別クラスに配置したTextFieldであっても、その縦幅をキーボード位置に応じて変化できるようになった。
追記した後の全コードは以下のとおり。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); late double screenHeightO; late double statusBarHeightO; late double appBarHeightO; const double containerHeightO = 50.0; class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Test", theme: ThemeData.light(), builder: (BuildContext context, Widget? child) { screenHeightO = MediaQuery.of(context).size.height; statusBarHeightO = MediaQuery.of(context).padding.top; appBarHeightO = AppBar().preferredSize.height; return child!; }, home: SampleScreen(), ); } } class SampleScreen extends StatefulWidget { @override _SampleScreenState createState() => _SampleScreenState(); } class _SampleScreenState extends State<SampleScreen> { @override Widget build(BuildContext context) { debugPrint("全体を描画"); return Scaffold( /// ↓これを追加する ※これがないと、viewInsets.bottom の値を取得できない resizeToAvoidBottomInset: false, appBar: AppBar( title: Text("TestApp"), ), body: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Container( child: Text("TextField"), alignment: Alignment.center, color: Colors.green[200], height: containerHeightO, ), // TextField部分を別クラス化 TextFieldWidgetO(), ], ), ), ); } } /// 別クラス化した TextField を含むウィジェット class TextFieldWidgetO extends StatefulWidget { const TextFieldWidgetO({Key? key}) : super(key: key); @override State<TextFieldWidgetO> createState() => _TextFieldWidgetOState(); } class _TextFieldWidgetOState extends State<TextFieldWidgetO> { @override Widget build(BuildContext context) { debugPrint("TextFieldWidgetOを描画"); // 動的にキーボードの位置(縦幅)を取得 double textFieldBottomO = MediaQuery.of(context).viewInsets.bottom; debugPrint("textFieldBottomO = $textFieldBottomO"); return Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), // TextFieldの高さを計算して算出 // 画面の縦幅 - ステータスバーの高さ - AppBarの高さ - 上下パディング - 上部Containerの高さ - キーボードの縦幅 height: screenHeightO - statusBarHeightO - appBarHeightO - 8.0 * 2 - containerHeightO - textFieldBottomO, child: TextField( decoration: InputDecoration( border: InputBorder.none, ), keyboardType: TextInputType.multiline, maxLines: null, ), ); } }
(考察)contextを渡しても解消するが、全体が再描画されてしまう
以前、下記日記にあるとおり、「Scaffold」の下位クラスだと、ステータスバーの高さを示す「padding.top」を取得できなかった。
このときは、contextを下位クラスに引数で渡すことで解消できた。
そのため、「viewInsets.bottom」が取得できない原因も同じだと考え、contextを引数として「TextFieldWidgetO」に渡してみたところ、予想どおり「resizeToAvoidBottomInset: false」の設定をせずとも、問題を解消できた。
しかし、この方法だと、「Scaffold」直下のbuildメソッドも回ってしまい、画面全体が再描画されてしまった。
そのため、「resizeToAvoidBottomInset: false」を用いる方がシンプルで、描画効率も良いと思われる。
\一般的なエラー対処法をまとめた記事はこちら/
リリースしたアプリ(全てFlutterで開発)
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/