🐥

FlutterでTextFieldのサイズを動的に調整する方法

に公開

Flutterアプリでテキストフィールドを使うとき、固定サイズだと使いにくいことがありますよね。
今回は、テキストフィールドのサイズ調整について3つの方法を紹介します。


🚨 問題:テキストフィールドにはサイズ調整機能がない

FlutterのTextFieldウィジェットには、直接的なサイズ調整機能がありません。

  • TextField自体にはheightwidthプロパティがない
  • デフォルトでは内容に応じて自動調整される
  • 固定サイズにしたい場合は別の方法が必要

📏 方法1:Containerで固定サイズを指定

最もシンプルな方法です。ContainerTextFieldを囲んで固定サイズを指定します。

Container(
  // 固定の高さを指定(ピクセル単位)
  height: 128,
  // ボーダーの装飾を設定
  decoration: BoxDecoration(
    // ボーダーの色を指定
    border: Border.all(
      color: Colors.grey.shade300,
      width: 0.4,
    ),
    // 角丸を指定
    borderRadius: BorderRadius.circular(4),
  ),
  // Containerの中身としてTextFieldを配置
  child: TextField(
    // 最大行数をnullにすることで無制限に設定
    maxLines: null,
    // 親コンテナいっぱいに拡張
    expands: true,
    // テキストを上揃えに配置
    textAlignVertical: TextAlignVertical.top,
    // 入力フィールドの装飾
    decoration: const InputDecoration(
      // デフォルトのボーダーを非表示
      border: InputBorder.none,
      // 内側の余白を設定
      contentPadding: EdgeInsets.all(4),
      // プレースホルダーテキスト
      hintText: 'ここにテキストを入力してください...',
    ),
    // テキストのスタイル
    style: const TextStyle(fontSize: 14),
  ),
)

メリット

  • 実装が簡単
  • 確実に固定サイズになる
  • レイアウトが安定する

デメリット

  • テキストが長いと見切れる
  • 短いテキストでも大きなスペースを取る
  • ユーザビリティが悪い

🔄 方法2:テキスト量に応じて動的調整

テキストの量に応じて自動的にサイズを調整する方法です。

実装のポイント

  1. TextPainterでテキストサイズを計算
// テキストの描画に必要なサイズを計算するためのツール
final textPainter = TextPainter(
  // 計算したいテキストとスタイルを指定
  text: TextSpan(text: text, style: TextStyle(fontSize: 14)),
  // テキストの方向を指定(左から右)
  textDirection: TextDirection.ltr,
  // 行数制限なし(改行で自動的に複数行になる)
  maxLines: null,
);
// 利用可能な幅を指定してレイアウトを計算
// 32は左右のパディング分を引いた幅
textPainter.layout(maxWidth: MediaQuery.of(context).size.width - 32);
// 計算された高さを取得
final height = textPainter.height;
  1. AnimatedContainerでスムーズな変更
// アニメーション付きのコンテナ
AnimatedContainer(
  // アニメーションの時間を200ミリ秒に設定
  duration: const Duration(milliseconds: 200),
  // 計算された高さを適用
  height: calculatedHeight,
  // 中身としてTextFieldを配置
  child: TextField(...),
)
  1. 最小・最大サイズの制限
// 計算された高さに余白を追加し、最小・最大値の範囲内に制限
// clamp(最小値, 最大値)で範囲を制限し、toDouble()でdouble型に変換
final newHeight = (textPainter.height + 20).clamp(80, 200).toDouble();

メリット

  • テキストの量に応じて最適なサイズになる
  • ユーザビリティが良い
  • スクロールが不要

デメリット

  • 実装が複雑
  • レイアウトが頻繁に変わる
  • パフォーマンスへの影響がある

✅ 3つの方法を比較した完全版コード

DartPadでそのまま動かせる、3つの方法を比較できるコードです。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'テキストフィールドサイズ調整デモ',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TextFieldComparisonDemo(),
    );
  }
}

class TextFieldComparisonDemo extends StatefulWidget {
  const TextFieldComparisonDemo({super.key});

  
  State<TextFieldComparisonDemo> createState() => _TextFieldComparisonDemoState();
}

class _TextFieldComparisonDemoState extends State<TextFieldComparisonDemo> {
  // 動的テキストフィールドの高さ
  double _dynamicHeight = 80;
  // 動的テキストフィールド用のコントローラー
  final TextEditingController _dynamicController = TextEditingController();

  
  void initState() {
    super.initState();
    // 動的テキストフィールドの高さ更新リスナーを追加
    _dynamicController.addListener(_updateDynamicHeight);
  }

  
  void dispose() {
    // リスナーを削除
    _dynamicController.removeListener(_updateDynamicHeight);
    // コントローラーを破棄
    _dynamicController.dispose();
    super.dispose();
  }

  // 動的テキストフィールドの高さを更新するメソッド
  void _updateDynamicHeight() {
    final text = _dynamicController.text;
    final textSpan = TextSpan(
      text: text.isEmpty ? 'A' : text,
      style: const TextStyle(fontSize: 14),
    );
    
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: TextDirection.ltr,
      maxLines: null,
    );
    
    // 画面幅から余白を引いた幅で計算
    textPainter.layout(maxWidth: MediaQuery.of(context).size.width - 64);
    
    // 最小80px、最大200pxの範囲で高さを制限
    final newHeight = (textPainter.height + 20).clamp(80, 200).toDouble();
    
    if (newHeight != _dynamicHeight) {
      setState(() {
        _dynamicHeight = newHeight;
      });
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('テキストフィールドサイズ比較'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // 方法1: Containerで固定サイズ
            _buildSectionTitle('方法1: Containerで固定サイズ'),
            _buildFixedSizeTextField(),
            const SizedBox(height: 24),
            
            // 方法2: 動的サイズ調整
            _buildSectionTitle('方法2: テキスト量に応じて動的調整'),
            _buildDynamicTextField(),
            Text('現在の高さ: ${_dynamicHeight.toStringAsFixed(1)}px'),
            const SizedBox(height: 24),
            
            // 方法3: デフォルトのTextField
            _buildSectionTitle('方法3: デフォルトのTextField'),
            _buildDefaultTextField(),
            const SizedBox(height: 24),
            
            // 比較説明
            _buildComparisonInfo(),
          ],
        ),
      ),
    );
  }

  // セクションタイトルを表示するウィジェット
  Widget _buildSectionTitle(String title) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Text(
        title,
        style: const TextStyle(
          fontSize: 16,
          fontWeight: FontWeight.bold,
          color: Colors.blue,
        ),
      ),
    );
  }

  // 方法1: Containerで固定サイズのテキストフィールド
  Widget _buildFixedSizeTextField() {
    return Container(
      // 固定の高さを128pxに設定
      height: 128,
      // ボーダーの装飾
      decoration: BoxDecoration(
        border: Border.all(
          color: Colors.grey.shade300,
          width: 0.4,
        ),
        borderRadius: BorderRadius.circular(4),
      ),
      child: TextField(
        // 複数行入力可能
        maxLines: null,
        // 親コンテナいっぱいに拡張
        expands: true,
        // テキストを上揃え
        textAlignVertical: TextAlignVertical.top,
        // 入力フィールドの装飾
        decoration: const InputDecoration(
          border: InputBorder.none,
          contentPadding: EdgeInsets.all(4),
          hintText: '固定サイズ(128px)のテキストフィールド',
        ),
        style: const TextStyle(fontSize: 14),
      ),
    );
  }

  // 方法2: 動的サイズ調整のテキストフィールド
  Widget _buildDynamicTextField() {
    return AnimatedContainer(
      // アニメーション時間を200msに設定
      duration: const Duration(milliseconds: 200),
      // 計算された高さを適用
      height: _dynamicHeight,
      // ボーダーの装飾
      decoration: BoxDecoration(
        border: Border.all(
          color: Colors.grey.shade300,
          width: 0.4,
        ),
        borderRadius: BorderRadius.circular(4),
      ),
      child: TextField(
        controller: _dynamicController,
        maxLines: null,
        expands: true,
        textAlignVertical: TextAlignVertical.top,
        decoration: const InputDecoration(
          border: InputBorder.none,
          contentPadding: EdgeInsets.all(4),
          hintText: '動的サイズ調整(80-200px)のテキストフィールド',
        ),
        style: const TextStyle(fontSize: 14),
      ),
    );
  }

  // 方法3: デフォルトのTextField
  Widget _buildDefaultTextField() {
    return Container(
      // ボーダーの装飾
      decoration: BoxDecoration(
        border: Border.all(
          color: Colors.grey.shade300,
          width: 0.4,
        ),
        borderRadius: BorderRadius.circular(4),
      ),
      child: TextField(
        // デフォルトでは単一行
        // maxLines: null を設定すると複数行になる
        decoration: const InputDecoration(
          border: InputBorder.none,
          contentPadding: EdgeInsets.all(4),
          hintText: 'デフォルトのテキストフィールド(単一行)',
        ),
        style: const TextStyle(fontSize: 14),
      ),
    );
  }

  // 比較情報を表示するウィジェット
  Widget _buildComparisonInfo() {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.blue.shade50,
        borderRadius: BorderRadius.circular(8),
        border: Border.all(color: Colors.blue.shade200),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text(
            '📊 比較表',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8),
          Text('• 方法1: 固定サイズ - 実装簡単、レイアウト安定'),
          Text('• 方法2: 動的調整 - ユーザビリティ良好、実装複雑'),
          Text('• 方法3: デフォルト - シンプル、機能制限あり'),
        ],
      ),
    );
  }
}

🎯 使い分けのポイント

方法1(固定サイズ)を使う場合

  • レイアウトの安定性が重要な場合
  • 実装を簡単に済ませたい場合
  • テキストの長さが予測可能な場合

方法2(動的調整)を使う場合

  • ユーザビリティを重視する場合
  • テキストの長さが予測できない場合
  • モダンなUIを目指す場合

方法3(デフォルト)を使う場合

  • 単純な入力フィールドが必要な場合
  • 複数行入力が不要な場合
  • 最小限の実装で済ませたい場合

🧭 おわりに

Flutterでテキストフィールドを使う際は、用途に応じて適切なサイズ調整方法を選択することが重要です。

今回紹介した3つの方法を参考に、自分のアプリに最適なテキストフィールドを実装してみてください!

Discussion