Flutter - how to make TextField width fit its text ("wrap content")

Over a whole year has passed since I asked and forgot about this question... I gave it a little bit more thoughts today, and took a different approach this time.

The key problem is that, we are not able to let TextField occupy just the right amount of space. So this approach uses a simple Text to display the text content, and use a very thin TextField (at 4 px) just to make it render the blinking cursor, shown in red:

widget composition diagram

Feel free to use this approach as a starting point if it helps anyone.

Usage:

TextChip()

Demo:

Code: (draft, works as demoed above; should only be used as a starting point)

class TextChip extends StatefulWidget {
  @override
  _TextChipState createState() => _TextChipState();
}

class _TextChipState extends State<TextChip> {
  final _focus = FocusNode();
  final _controller = TextEditingController();
  String _text = "";

  @override
  Widget build(BuildContext context) {
    return InputChip(
      onPressed: () => FocusScope.of(context).requestFocus(_focus),
      label: Stack(
        alignment: Alignment.centerRight,
        overflow: Overflow.visible,
        children: [
          Text(_text),
          Positioned(
            right: 0,
            child: SizedBox(
              width: 4, // we only want to show the blinking caret
              child: TextField(
                scrollPadding: EdgeInsets.all(0),
                focusNode: _focus,
                controller: _controller,
                style: TextStyle(color: Colors.transparent),
                decoration: InputDecoration(
                  border: InputBorder.none,
                ),
                onChanged: (_) {
                  setState(() {
                    _text = _controller.text;
                  });
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

I tried but failed. I have issues figuring out when the TextField overflows. This solution cannot work with dynamically changing chips since tp.layout(maxWidth: constraints.maxWidth/2); is hard coded.

There are two options to fix this solution:

  • TextController has a overflow flag

  • In tp.layout(maxWidth: constraints.maxWidth/2), LayoutBuilder can figure out the width left over from chips.

Here is my attempt

enter image description here

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _controller;
  String _text = "";
  bool _textOverflow = false;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    _textOverflow = false;
    _controller = TextEditingController();
    _controller.addListener((){
      setState(() {
        _text = _controller.text;
      });
    });
  }
  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    _controller.dispose();
  }

  Widget chooseChipInput(BuildContext context, bool overflow, List<Widget> chips) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      children: <Widget>[
        overflow ? Wrap(children: chips, alignment: WrapAlignment.start,): Container(),
        Container(
          color: Colors.red,
          child: TextField( 
            controller: _controller,
            maxLines: overflow ? null : 1,
            decoration:  InputDecoration(icon: overflow ? Opacity(opacity: 0,) : Wrap(children: chips,)),
          ),
        )

      ]
    );
  }

  @override
  Widget build(BuildContext context) {
    const _counter = 0;
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),

            LayoutBuilder(builder: (context, constraints){
                var textStyle = DefaultTextStyle.of(context).style;
                var span = TextSpan(
                  text: _text,
                  style: textStyle,
                );
                // Use a textpainter to determine if it will exceed max lines
                var tp = TextPainter(
                  maxLines: 1,
                  textAlign: TextAlign.left,
                  textDirection: TextDirection.ltr,
                  text: span,
                );
                // trigger it to layout
                tp.layout(maxWidth: constraints.maxWidth/2);

                // whether the text overflowed or not
                print("****** ${tp.didExceedMaxLines} ${constraints.maxWidth}");
                return chooseChipInput(
                  context, 
                  tp.didExceedMaxLines, 
                  <Widget>[Chip(label: Text("chip1"),), 
                      Chip(label: Text("chip2")),]
                );
            },),

          ],
        ),
      ),
    );
  }
}

This attempt comprised of a few parts:

  • Checking when TextField overflows with this hack https://stackoverflow.com/a/52272545
  • Uses ternary operators to ensure Flutter does not rebuild TextField in order to maintain cursor position.
  • Enable multiline TextField when text overflows https://docs.flutter.io/flutter/material/TextField/maxLines.html
  • Changing the layout between column and InputDecoration to sure the correct position of chips.

Edit3: Added picture when you add tons of chips and fix the Column(Warp) enter image description here enter image description here

Like I said, the largest problem is that I cannot figure out when the text box overflows.

Anyone else wants try? I think this question needs a custom plugin to solve

Edit2: I found the library but I did not test it https://github.com/danvick/flutter_chips_input


Use IntrinsicWidth widget to size a child to the child's maximum intrinsic width. In this case, effectively shrink wrapping the TextField:

IntrinsicWidth(
  child: TextField(),
)

However, this will make the TextField too small when it's empty. To fix that, we can use ConstrainedBox to force a minimum width constraint. For example:

ConstrainedBox(
  constraints: BoxConstraints(minWidth: 48),
  child: IntrinsicWidth(
    child: TextField(),
  ),
)

End result:

enter image description here

Tags:

Flutter