SteveM
SteveM

Reputation: 772

Flutter TextField width should match width of contained text

In flutter, TextField doesn't have an intrinsic width; it only knows how to size itself to the full width of its parent container. How do I set the width to the width of the contained text.

I tried putting the TextField inside a container

As outlined here How to update flutter TextField's height and width?

new Container(              
  width: 100.0,
  child: new TextField()
)

I expect the width of the TextField to match the width of the text it contains. The TextField should grow wider as text is typed, and shrink narrower as text is deleted.

Upvotes: 25

Views: 17381

Answers (3)

Philip Ch'ng
Philip Ch'ng

Reputation: 681

Flutter has IntrinsicWidth to do the calculation for you. Just wrap your TextField or TextFormField in it as follow:

IntrinsicWidth(child: TextField())

Upvotes: 54

Alexander Tee
Alexander Tee

Reputation: 135

I modified SteveM's answer a little bit to fix the bug when widget is not expanding correctly. We should respect these two small things when calculating widget's width:

  1. TextField merges given textStyle parameter and current theme's TextStyle. This has to be done in custom widget too:
final ThemeData themeData = Theme.of(context);
final TextStyle style = themeData.textTheme.subtitle1.merge(textStyle);
  1. TextField's cursorWidth has to be taken into widget's width calculations. Since there's no way to get default cursor width from TextField class, I checked its code and added a new constant to FitsTextField class. Don't forget to pass it to TextField's constructor:
final textWidth = max(widget.minWidth, tp.width + _CURSOR_WIDTH);

// When building a TextField:
child: TextField(
        cursorWidth: _CURSOR_WIDTH,

Full code:

import 'dart:math';

import 'package:flutter/material.dart';

class FitTextField extends StatefulWidget {
  final String initialValue;
  final double minWidth;

  const FitTextField({
    Key key,
    this.initialValue,
    this.minWidth: 30,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() => new FitTextFieldState();
}

class FitTextFieldState extends State<FitTextField> {
  // 2.0 is the default from TextField class
  static const _CURSOR_WIDTH = 2.0;

  TextEditingController txt = TextEditingController();

  // We will use this text style for the TextPainter used to calculate the width
  // and for the TextField so that we calculate the correct size for the text
  // we are actually displaying
  TextStyle textStyle = TextStyle(
    color: Colors.grey[600],
    fontSize: 16,
  );

  initState() {
    super.initState();
    // Set the text in the TextField to our initialValue
    txt.text = widget.initialValue;
  }

  @override
  Widget build(BuildContext context) {
    // TextField merges given textStyle with text style from current theme
    // Do the same to get final TextStyle
    final ThemeData themeData = Theme.of(context);
    final TextStyle style = themeData.textTheme.subtitle1.merge(textStyle);

    // Use TextPainter to calculate the width of our text
    TextSpan ts = new TextSpan(style: style, text: txt.text);
    TextPainter tp = new TextPainter(
      text: ts,
      textDirection: TextDirection.ltr,
    );
    tp.layout();

    // Enforce a minimum width
    final textWidth = max(widget.minWidth, tp.width + _CURSOR_WIDTH);

    return Container(
      width: textWidth,
      child: TextField(
        cursorWidth: _CURSOR_WIDTH,
        style: style,
        controller: txt,
        onChanged: (text) {
          // Redraw the widget
          setState(() {});
        },
      ),
    );
  }
}

Upvotes: 5

SteveM
SteveM

Reputation: 772

I was able to achieve the expected result by using a TextPainter to compute the desired width of the text. I then used that width as the width of the Container containing the TextField.

Remember to call setState() in your TextFields onChanged method. This tells the widget to redraw itself causing the TextField to adjust to the new width of its content.

import 'package:flutter/material.dart';

class FitTextField extends StatefulWidget {

  final String initialValue;
  final double minWidth;

  const FitTextField({Key key, this.initialValue, this.minWidth: 30}): super(key: key);

  @override
  State<StatefulWidget> createState() => new FitTextFieldState();
}

class FitTextFieldState extends State<FitTextField>{

  TextEditingController txt = TextEditingController();

  // We will use this text style for the TextPainter used to calculate the width
  // and for the TextField so that we calculate the correct size for the text
  // we are actually displaying
  TextStyle textStyle = TextStyle(color: Colors.grey[600]);

  initState() {
    super.initState();
    // Set the text in the TextField to our initialValue
    txt.text = widget.initialValue;
  }

  @override
  Widget build(BuildContext context) {
    // Use TextPainter to calculate the width of our text
    TextSpan ts = new TextSpan(style: textStyle, text: txt.text);
    TextPainter tp = new TextPainter(text: ts, textDirection: TextDirection.ltr);
    tp.layout();
    var textWidth = tp.width; // We will use this width for the container wrapping our TextField

    // Enforce a minimum width
    if ( textWidth < widget.minWidth ) {
      textWidth = widget.minWidth;
    }

    return Container(
      width: textWidth,
      child: TextField(
        style: textStyle,
        controller: txt,
        onChanged: (text) {
          // Tells the framework to redraw the widget
          // The widget will redraw with a new width
          setState(() {});
        },
      ),
    );
  }
}

Upvotes: 7

Related Questions