※2022/12/2追記
縦長のTextFieldを配置すると、キーボードが表示されたときにエラーになったり、TextFieldの上下端が隠れたままで、全ての内容を表示できなかったりする。
解決するにはどうしたらいいの?
自分がメモアプリを作成しているとき、ぶつかった壁です。
試行錯誤して解決したので、サンプルケースを使ってその過程を共有できればと思います。
40代からプログラミング(Flutter)を始めて、GooglePlayとAppStoreにアプリを公開しているhalzo appdevです。
作成したアプリはこちら↓ 全てFlutterで開発したアプリです。
キーボード出現時のエラーはSingleChildScrollViewで対応
サンプルケースとして、上図のように、高さが600pixel、改行可能なTextFieldを配置した場合を考えます。
コードは以下のとおりです(できるだけ説明をコード中に入れてあります)。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Test", theme: ThemeData.light(), home: SampleScreen(), ); } } // ↑ここまでは必ず必要になる決り文句のコード class SampleScreen extends StatefulWidget { @override _SampleScreenState createState() => _SampleScreenState(); } class _SampleScreenState extends State<SampleScreen> { @override Widget build(BuildContext context) { return Scaffold( // AppBarのタイトルを記載 appBar: AppBar( title: Text("TestApp"), ), body: // TextFieldの範囲が分かりやすいように上下左右にPaddingを設定 Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ // TextFieldの上にテキストを表示するWidget(以下、Text Widget)を配置 Text("↓TextField"), // TextFieldに枠を付け、高さ(縦幅)を設定するためにContainerでラップする Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), // 高さ(縦幅)を600pixelで固定 height: 600, child: TextField( // デフォルトで表示される青い下線を削除 decoration: InputDecoration( border: InputBorder.none, ), // 改行して複数行入力が可能なように、キーボードに改行ボタンを表示 keyboardType: TextInputType.multiline, // 改行できる行数を無制限に設定 maxLines: null, ), ), ], ), ), ); } }
この状態でTextField内をタップし、キーボードを出現させると、以下のようなエラーが発生します。
======== Exception caught by rendering library ==============
The following assertion was thrown during layout:
A RenderFlex overflowed by 183 pixels on the bottom.
キーボードが出現したことで、Text Widget(「↓TextField」を表示するWidget)とTextFieldを画面内に表示しきれなくなったためです。
これは、「SingleChildScrollView」で、Widget(描画要素)を包む(ラップする)ことで回避できます。
「SingleChild」なので、画面に表示される全Widget(AppBarは除く)を、1個にまとめているWidget(ここではColumnから上にあるWidget)をラップする必要があります。
ContainerやTextFieldをラップしても、エラーは解消しないので注意が必要です(Text Widgetが対象から漏れてしまっているため)。
追記した部分のコードは以下のとおりです。ここではColumnをラップしていますが、その一つ上のPaddingをラップしても大丈夫です。
body: // ※下のColumnではなくこのPaddingをラップしても可 Padding( padding: const EdgeInsets.all(8.0), // ↓ColumnをSingleChildScrollViewでラップする child: SingleChildScrollView( child: Column( children: [ Text("↓TextField"), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), height: 600, child: Scrollbar( child: TextField( decoration: InputDecoration( border: InputBorder.none, ), keyboardType: TextInputType.multiline, maxLines: null, ), ), ), ], ), ), ),
これでキーボードが出てもエラーは出なくなりました。
SingleChildScrollViewだけでは解決しない問題
これでエラーは解消されますが、TextFiledを扱う上で、別の問題が3つほど発生します。
①TextFieldの上にあるWidgetが見えなくなる
上図のように、TextField内の改行が増えたとき、カーソルをTextFieldの下部に置いた状態でキーボードを出すと、TextFieldの上にあった「↓TextField」の表示(Text Widget)も上にズレて、見えなくなってしまいます。
②TextFieldの上端までスクロールできない
上図のように、カーソルをTextFieldの下部に置いた状態でキーボードを出すと、TextFieldそのものが上に動いてしまうので、TextField内をスワイプしてスクロールさせても上端まで表示できません。
上端まで表示させるには、キーボードをしまう必要があり、利便性が低くなります。
③TextFieldの下端までスクロールできない
②の逆で、カーソルをTextFieldの上部に置いた状態でキーボードを出すと、TextFieldは上にスクロールしません。
その結果、TextFieldの下部(一部分)がキーボードの裏側に隠れたままとなり、TextField内をスワイプしてスクロールさせても下端まで表示できません。
下端まで表示させるには、やはりキーボードをしまう必要があり、利便性が低くなります。
キーボードの縦幅だけTextFieldの高さ(縦幅)を縮める
これらを解消するために、画面サイズを取得できる「MediaQuery.of」メソッドを使い、キーボードの縦幅を把握して、その分だけTextFieldの縦幅を縮めます。
修正後のコードは以下のとおりです(主な修正部分にコメントを入れています)。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Test", theme: ThemeData.light(), home: SampleScreen(), ); } } class SampleScreen extends StatefulWidget { @override _SampleScreenState createState() => _SampleScreenState(); } class _SampleScreenState extends State<SampleScreen> { @override Widget build(BuildContext context) { // 画面の下端位置を把握 // キーボードが出ているときは、画面下端からキーボード上端までの高さ(縦幅)になる var textFieldBottom = MediaQuery.of(context).viewInsets.bottom; return Scaffold( appBar: AppBar( title: Text("TestApp"), ), body: Padding( padding: const EdgeInsets.all(8.0), child: SingleChildScrollView( child: Column( children: [ Text("↓TextField"), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), // TextFieldの高さ(縦幅)を // 固定値の600pixelから、キーボードの高さ(縦幅)を減算して設定 // ※キーボードが出ていないときは、textFieldBottom = 0 height: 600 - textFieldBottom, // スクロール状況が分かりやすいようにScrollbarを設置 child: Scrollbar( child: TextField( decoration: InputDecoration( border: InputBorder.none, ), keyboardType: TextInputType.multiline, maxLines: null, ), ), ), ], ), ), ), ); } }
MediaQueryの使い方は、こちらの記事でも書いていますのでご参考にしてください。
これで、上図のように、TextField内を上下にスクロールさせて上端・下端まで表示できるようになりました。
ただし、元々のTextFieldの縦幅を、画面サイズによらず600pixel固定にしているため、TextFieldとキーボードの間に余白ができています。
逆に、画面の縦幅が短い端末では、600だと大きすぎて、TextFieldの下部がキーボードの裏側に隠れたままになる可能性もあります。
そのため、次の方法で画面サイズに応じて、TextFieldの縦幅を最適に設定します。
画面サイズ・AppBarの高さに応じて最適配置する
AppBarを表示する場合は、画面サイズ(縦幅)だけでなく、AppBarの高さ(縦幅)も端末によって異なるため、この2点を考慮してTextFieldの高さ(縦幅)を設定します。
概略は以下のとおりです。
- GlobalKeyをText Widget(「↓TextField」を表示するWidget)のkeyプロパティに設置
- initStateメソッドを作り、その中でaddPostFrameCallbackメソッドを使い、Text Widgetの描画後に、GlobalKeyからそのy座標開始位置と高さ(縦幅)を取得
- buildメソッド内に「MediaQuery.of」メソッドを追加し、画面の縦幅を取得
- 以下の計算式で、TextFieldをラップしているContainerの高さ(縦幅)を設定
画面の縦幅
ー{Text Widgetのy座標開始位置 + Text Widgetの高さ(縦幅)}
ー キーボードの縦幅
ー 8.0(下部パディング値)※
※修正前の記事では、この部分をTextFieldの上部にデフォルトで設定されているパディング値(12.0)としていましたが、この値はTextFieldの縦幅を設定する上で関係なかったため、削除しました。その代わり画面全体(上下左右)に均等に設定されているパディング値8.0を引く必要があるため、そのように修正しました。
ここでは詳細説明は割愛しますが、画面サイズとAppBarの高さに応じてWidgetを最適配置する方法は、以下の記事で解説していますので、ご参考にしてください。
以下に最終的なコードの全体像を掲載します(追加部分を中心にコメントを入れています)。
なお、この方法を用いると、SingleChildScrollViewは不要になるので(キーボードの表示・非表示に応じてTextFieldのサイズを変更するので、スクロールで対応する必要がなくなる)、ここでは記載を削除しています(ただ、SingleChildScrollViewはあってもエラーにはなりません)。
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); // Text Widgetの位置把握のためGlobalKey型のグローバル変数を定義 GlobalKey globalKeyTextWidget = GlobalKey(); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "Test", theme: ThemeData.light(), home: SampleScreen(), ); } } class SampleScreen extends StatefulWidget { @override _SampleScreenState createState() => _SampleScreenState(); } class _SampleScreenState extends State<SampleScreen> { // Text Widgetの2次元情報を入れるためのRenderBox型の変数を定義 // null safety対応のためlate(今は値を決定できない)をつける late RenderBox textWidget; // Text Widgetのy座標開始位置と高さ(縦幅)を入れる変数を定義 // 初回ビルド時のnullエラーを防ぐため、初期値を設定しておく double textWidgetDy = 0; double textWidgetHeight = 0; // 初期値設定等を行うinitStateメソッドを導入 @override void initState() { // Text Widgetが描画された後でないとGlobalKeyの値が取得できずエラーになるため、 // buildメソッド完了後にGlobalKeyの値取得が行われるよう、 // addPostFrameCallbackメソッドを使用 // null safety対応で?(null以外のみアクセス)をつける WidgetsBinding.instance?.addPostFrameCallback((cb) { // GlobalKeyを通じてText Widgetの2次元情報を取得 // null safety対応で?と右辺にas RenderBoxをつける textWidget = globalKeyTextWidget.currentContext?.findRenderObject() as RenderBox; // 2次元情報からText Widgetの縦方向の上端位置(y座標)と高さ(縦幅)を取得 textWidgetDy = textWidget.localToGlobal(Offset.zero).dy; textWidgetHeight = textWidget.size.height; // 確認のため、取得した位置と高さをDebugウィンドウに表示 print("TextWidgetの上端位置 $textWidgetDy、TextWidgetの高さ $textWidgetHeight"); // Text Widgetの位置と高さを取得後、setStateメソッドで全体を再描画する setState(() {}); }); super.initState(); } @override Widget build(BuildContext context) { var textFieldBottom = MediaQuery.of(context).viewInsets.bottom; // 画面全体の縦幅を取得するため、MediaQuery.ofメソッドを追加 // ※画面の縦幅は変化しないためfinalで宣言 final screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: AppBar( title: Text("TestApp"), ), body: Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ Text( "↓TextField", // GlobalKeyをText Widgetのkeyプロパティに設定 key: globalKeyTextWidget, ), Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), ), // 初回描画時点では、GlobalKey未取得のため、計算式から高さを求めると // 画面サイズオーバーのエラーになる。これを回避するため、GlobalKeyの取得が // 未了の段階(=初回描画時点)では高さを0.0にしておく。 // 計算式では、画面全体に設定している下部パディング8.0も引く必要あり。 height: (textWidgetDy == 0) ? 0.0 : screenHeight - (textWidgetDy + textWidgetHeight) - textFieldBottom - 8.0, child: Scrollbar( child: TextField( decoration: InputDecoration( border: InputBorder.none, ), keyboardType: TextInputType.multiline, maxLines: null, ), ), ), ], ), ), ); } }
これで、画面サイズに応じて、無駄な余白が生じないTextFieldを設定できました。
注意点としては、最後に8.0を引く必要がある点です。
これは、画面全体には上下左右に8.0pixelのパディングが設定されており、TextFieldにはデフォルトで上部パディングが12.0pixel設定されており(←誤りのため削除)、この分も考慮して高さを設定しないと、僅かにTextFieldの下部がキーボードの裏側に隠れてしまうためです。
下図は、8.0を引かずにTextFieldの高さを設定した場合です。
※なお、TextFieldの設定で、デフォルトの上部パディングをゼロにする方法もあるのですが、別の問題が発生するので、ここでは割愛します。別の記事で説明したいと思います。(←関係ない記述だったため削除)
また、Containerの「height:」を設定するとき、単純に上記4.の計算式だけを書いてしまうと、一時的にエラーが発生します。
addPostFrameCallbackメソッドを使っているので、Text WidgetのGlobalKeyを取得する前に、TextFieldの初回描画が行われますが、この時点では{Text Widgetのy座標開始位置 + Text Widgetの高さ(縦幅)}が初期値(つまり「0」)のままなので、TextFieldの高さ(縦幅)が画面サイズを超過してしまうためです。
これを回避するため、Text WidgetのGlobalKeyを取得するまでは、TextFieldの高さ(縦幅)を0.0にしておく調整をしています。
(2022/12/2追記)
TextFieldを「Scaffold」の下位クラスに分離して設置した場合は、TextFieldの高さ(縦幅)調整ができない場合があるため、その対処法を以下に記載しました。同じ現象にぶつかった方には、ご参考になればと思います。
以上、ご参考になれば幸いです。
最後までお読みいただき、ありがとうございました。
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/
コメント