※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の基礎文法を素早くインプットできる/









コメント