简体   繁体   中英

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

I'm trying to do a "search contact list" feature with some chips representing selected contacts, and a user can type on text field to filter and add more contacts:

期望的结果

This is done with a Wrap widget, wrapping a list of Chip widgets, and ending the list with a Container of a TextField widget.

What I've tried:

If I do not set the width of the TextField , it defaults to occupy a whole line. Let's make it red for clarity:

默认宽度为整行

I do not want a whole line for it, so I set it to a small value, 50. But this doesn't work if the text is long:

固定宽度隐藏长文本

Question:

Is it possible to make the TextField starts small, and auto expands to a whole line when needed? I've tried "minWidth" in BoxConstraint but since the TextField defaults to a whole line, that doesn't work. Is using Wrap and TextField the correct way here?

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:

在此处输入图片说明

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

在此处输入图片说明

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:

Edit3: Added picture when you add tons of chips and fix the Column(Warp) 在此处输入图片说明 在此处输入图片说明

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

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:

小部件组成图

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;
                  });
                },
              ),
            ),
          ),
        ],
      ),
    );
  }
}

If you also want to the decoration has the same size with textfield, use

isCollapsed

In my case, the app just allows the user input maximum 8 characters and do not need to show counter text or error widgets. Here is an example:

ConstrainedBox(
            constraints: const BoxConstraints(minWidth: 50),
            child: IntrinsicWidth(
              child: TextField(
                controller: _textController,
                keyboardType: TextInputType.number,
                maxLength: 8,
                cursorColor: MyTheme.grey2,
                decoration: const InputDecoration(
                  border: textFieldBorder,
                  focusedBorder: textFieldBorder,
                  counterText: '',
                  contentPadding:
                      EdgeInsets.symmetric(vertical: 4, horizontal: 6),
                  isCollapsed: true,
                ),
                style: Theme.of(context)
                    .textTheme
                    .labelSmall
                    ?.copyWith(color: MyTheme.grey2),
              ),
            ),
          ),

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM