yamada0088
@yamada0088

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

【Flutter】TextFieldに横罫線を表示させる方法(Widgetなど)はあるのでしょうか?

解決したいこと

Flutterでメモ帳や大学ノートなどの横罫線を表示させたいです。
以下のサイトを参考にさせていただいているのですが、動作がおかしい(まったく罫線が表示されなかったり下の方の罫線が2重となっていたり)です。「flutter clean」実行後、再度ビルドすると一旦問題なく表示されるのですが、画面遷移などをするとまた動作がおかしくなってしまいます。
なので、以下のサイトではなく別の方法で横罫線が表示できないかと思い質問させていただきました。
どうすれば、メモ帳や大学ノートのような横罫線を表示することができるのでしょうか?
(なげやりな質問で申し訳ありません。ちょっとした情報でもあれば教えていただけると嬉しいです)
https://halzoblog.com/flutter-textfield-ruled-lines/#toc12

該当するソースコード

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

// Text Widgetの位置把握のためGlobalKey型のグローバル変数を定義
GlobalKey globalKeyTextWidgetO = GlobalKey();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Test",
      theme: ThemeData.light(),
      home: SampleScreenO(),
    );
  }
}

class SampleScreenO extends StatefulWidget {
  @override
  _SampleScreenOState createState() => _SampleScreenOState();
}

class _SampleScreenOState extends State<SampleScreenO> {

  // Text Widgetの2次元情報を入れるためのRenderBox型の変数を定義
  // null safety対応でlateをつける
  late RenderBox textWidgetO;

  // Text WidgetのY座標開始位置と高さ(縦幅)を入れる変数を定義
  // 初回ビルド時のnullエラーを防ぐため、初期値を設定しておく
  double textWidgetDyO = 0;
  double textWidgetHeightO = 0;

  // 画面全体のパディング、フォントサイズ、1行の高さを決める係数を設定
  static const double standardPaddingO = 8.0;
  static const double fontSizeO = 18.0;
  static const double lineSpaceO = 1.2;

  // TextField上にデフォルトで設定される上部パディングの変更値。ここでは0に設定
  static const double contentPaddingO = 0.0;

  // TextFieldに設置するFocusNodeのインスタンス作成
  FocusNode focusNodeTextFieldO = FocusNode();

  // 初期値設定等を行う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をつける
      textWidgetO = globalKeyTextWidgetO.currentContext?.findRenderObject() as RenderBox;

      // 2次元情報からText Widgetの縦方向の上端位置(Y座標)と高さ(縦幅)を取得
      textWidgetDyO = textWidgetO.localToGlobal(Offset.zero).dy;
      textWidgetHeightO = textWidgetO.size.height;

      // 確認のため、取得した位置と高さをDebugウィンドウに表示
      print("TextWidgetの上端位置 $textWidgetDyO、TextWidgetの高さ $textWidgetHeightO");

      // Text Widgetの位置と高さを取得後、setStateメソッドで全体を再描画する
      setState(() {});
    });

    super.initState();
  }

  @override
  Widget build(BuildContext context) {

    // 画面全体の縦幅と横幅を取得するためのMediaQuery.ofメソッド
    // ※画面の縦幅・横幅は変化しないためfinalで宣言
    final screenHeightO = MediaQuery.of(context).size.height;
    final screenWidthO = MediaQuery.of(context).size.width;

    // 画面の下端位置(キーボード出現時はキーボードの上端位置)を取得するためのMediaQuery.ofメソッド
    var textFieldBottomO = MediaQuery.of(context).viewInsets.bottom;

    // TextFieldの縦幅を柔軟に設定する計算式
    // 画面全体に設定している下部パディングも引く必要あり
    var textFieldVerticalRangeO=
        screenHeightO
            - (textWidgetDyO + textWidgetHeightO)
            - textFieldBottomO
            - standardPaddingO;

    return Scaffold(
      appBar: AppBar(
        title: Text("TestApp"),
      ),
      body: Padding(
        padding: const EdgeInsets.all(standardPaddingO),
          child: Column(
            children: [
              Text(
                "↓TextField",

                // GlobalKeyをText Widgetのkeyプロパティに設定
                key: globalKeyTextWidgetO,

              ),
              Container(

                // 分かりやすくするために枠線を付けるが、内側に自動で1.0のパディングが発生する点に注意
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey),
                ),

                // 初回描画時点では、GlobalKey未取得のため、計算式から高さを求めると
                // 画面サイズオーバーのエラーになる。これを回避するため、GlobalKeyの取得が
                // 未了の段階(=初回描画時点)では高さを0.0にしておく
                height: (textWidgetDyO == 0)
                    ? 0.0
                    : textFieldVerticalRangeO,

                  child: Scrollbar(

                    // スクロール時に罫線も動かすために、SingleChildScrollViewが必要
                    child: SingleChildScrollView(

                      // Stackを使って、罫線を引くCustomPaintの上に、TextFieldを重ね表示する
                      child: Stack(
                        children: <Widget>[

                          // 描画するCustomPaintクラスの呼び出し
                          CustomPaint(

                            // painter属性では、外部に作成したクラスに処理を飛ばす
                            // 画面の座標・幅情報を引数として渡す
                            painter: DisplayRuledLinesO(
                              textFieldVerticalRangeO: textFieldVerticalRangeO,
                              screenWidthO: screenWidthO,
                              focusNodeTextFieldO: focusNodeTextFieldO,
                              textWidgetDyO: textWidgetDyO,
                              textWidgetHeightO: textWidgetHeightO,
                            ),
                          ),

                          TextField(

                            // FocusNodeのインスタンスを設置
                            focusNode: focusNodeTextFieldO,

                            // フォントサイズを設定
                            style: TextStyle(fontSize: fontSizeO),

                            strutStyle: const StrutStyle(

                              // 1行の高さをフォントサイズに対する倍数値で設定
                              height: lineSpaceO,

                              // 改行とともにズレるのを防ぐため、行間強制を設定
                              forceStrutHeight: true,

                            ),

                            autofocus: true,

                            decoration: InputDecoration(

                              // TextFieldの上端から書き始められるように設定
                              isDense: true,

                              border: InputBorder.none,

                              // デフォルトの上部パディング(12.0)を変更するために設定
                              contentPadding:
                              const EdgeInsets.only(top: contentPaddingO),
                            ),

                            keyboardType: TextInputType.multiline,

                            // 行数は上限なしで設定
                            maxLines: null,
                          ),
                        ],
                      ),
                    ),
                  ),
              ),
            ],
          ),
      ),
    );
  }
}


// 横罫線の描画処理をするCustomPainterの拡張クラス
class DisplayRuledLinesO extends CustomPainter {

  final double textFieldVerticalRangeO;
  final double screenWidthO;
  final FocusNode focusNodeTextFieldO;
  final double textWidgetDyO;
  final double textWidgetHeightO;

  // 引数から初期値を受け取るコンストラクタ
  DisplayRuledLinesO({
    required this.textFieldVerticalRangeO,
    required this.screenWidthO,
    required this.focusNodeTextFieldO,
    required this.textWidgetDyO,
    required this.textWidgetHeightO,
  });



  @override
  void paint(Canvas canvas, Size size) {

    // 罫線を描画する範囲(高さ)を計算
    // 見えているTextFieldの高さ(キーボードの出現有無で可変)に、スクロールで上に動いた分を加算
    // これにより、スクロールとともに追加で罫線を描画し、罫線が無くなることを防ぐ
    final drawingVerticalRangeO =

        // TextFieldの縦幅
        textFieldVerticalRangeO
        + (
            // GlobalKeyのY座標位置を加算
            // Containerに枠線設定をすることで発生する上部パディング1.0を加える
            // ※枠線を無くしたときは、この「+1.0」は削除が必要
            // さらに、TextField上部のデフォルトのパディングを変更した値が、0より大きいときは、その加算も必要
            (textWidgetDyO + textWidgetHeightO + 1.0 + _SampleScreenOState.contentPaddingO)

            // FocusNodeのY座標位置(スクロールによる移動をトレースしたTextField上端位置)を減算
            - focusNodeTextFieldO.offset.dy
        );

    // 確認のため、Debugウィンドウに表示
    print("TextFieldの縦幅: $textFieldVerticalRangeO");
    print("GlobalKeyの位置: ${textWidgetDyO + textWidgetHeightO + 1.0 + _SampleScreenOState.contentPaddingO}");
    print("FocusNodeの位置: ${focusNodeTextFieldO.offset.dy}");

    // フォントサイズ×行間係数(strutStyleで規定)で1行の高さを算出し(ズレの蓄積を防ぐため四捨五入して整数化する)
    // ※切上げではなく四捨五入に修正(2021.12.26修正)
    // 上で求めた描画範囲(高さ)を割って、描画本数を計算する(切捨て)
    final ruledLineSpaceO = (_SampleScreenOState.fontSizeO * _SampleScreenOState.lineSpaceO).round();
    print("ruledLineSpaceO: $ruledLineSpaceO");
    final int ruledLineNumberO = (drawingVerticalRangeO / ruledLineSpaceO).floor();
    print("ruledLineNumberO: $ruledLineNumberO");

    // 罫線の横幅を計算
    // MediaQueryで把握済のTextFieldの幅から、左右に設定したパディングを引いて計算
    final ruledLineWidthO = screenWidthO - _SampleScreenOState.standardPaddingO * 2;

    // 罫線の描画
    final paint = Paint()
      ..color = Colors.grey
      ..strokeWidth = 0.7;
    for (var i = 1; i <= ruledLineNumberO; i++) {
      canvas.drawLine(

        // Offset(x, y)からOffset(x', y')へ線を引き、それを「ruledLineNumberO」回繰り返す
        // TextField上部のデフォルトのパディングを変更した値が、0より大きいときは、Y座標に加算する必要あり
        Offset(0, ruledLineSpaceO * i + _SampleScreenOState.contentPaddingO),
        Offset(ruledLineWidthO, ruledLineSpaceO * i + _SampleScreenOState.contentPaddingO),
        paint,
      );
    }
  }

  // 一度描画した横罫線の再描画は不要なため、false
  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;

}
0

No Answers yet.

Your answer might help someone💌