Flutter: TextFieldを別クラスに配置すると、キーボード位置(viewInsets.bottom)を取得できない

※当サイトは、アフィリエイト広告を利用しています

結論:「resizeToAvoidBottomInset: false」を設定する

2022/11/24 Flutter エラー・バグ日記

 

TextFieldにフォーカスが当たってキーボードが出たとき、描画範囲オーバーのエラーを防ぐため、

 

MediaQuery.of(context).viewInsets.bottom

 

を用いて、キーボード上端の位置(縦幅)を把握し、TextFieldの縦幅を動的に調整していた。

 

しかし、TextFieldをScaffold内に直接配置せず、別クラスに分離したところ、この調整ができなくなってしまった。

 

本記事はライトな日記思考で書いているので、詳細説明はしておらず、基本、テキストのみで画像とかはあまり載せておりません。。m(_ _)m

解説記事ではないため、解決していない内容や、その時々の間違った解釈を述べてしまっている可能性が大いにありますので、何卒、ご了承ください。

 

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


Dart入門 - Dartの要点をつかむためのクイックツアー

タイトルとURLをコピーしました