【Flutter实用组件】自适应宽度的输入框

Flutter组件的TextField默认宽度为撑满容器,有些场景需要文本靠右,并且使用前缀,如果不限制宽度,前缀会在最左边,文本在最右边,宽度大了间距太开不好看,宽度设置小了,容易填满,文本就滚动到前缀下面隐藏了。

经过一翻调试,封装了个组件,实现在文本变化时自动调整输入框容器的宽度。

实现原理就是在输入框内容变化时,使用TextPainter绘制出来文本并获取绘制出来的尺寸,然后赋给TextField外的SizedBox。为了保证尺寸准确,TextPainter和TextField使用了相同的style。

效果如下:

2022-04-26-21-19-03.gif

以下为组件代码

class AmountInput extends StatefulWidget {
  final double? value;
  final String symbol;
  final bool isDecimal;
  final bool autoFocus;
  final FocusNode? focusNode;
  final double? spacer;
  final String? hintText;
  final TextStyle? textStyle;
  final void Function(double? value)? onChanged;
  const AmountInput({
    Key? key,
    this.value,
    this.symbol = r'$',
    this.hintText,
    this.textStyle,
    this.focusNode,
    this.onChanged,
    this.spacer,
    this.autoFocus = false,
    this.isDecimal = true,
  }) : super(key: key);

  @override
  State<AmountInput> createState() => _AmountInputState();
}

class _AmountInputState extends State<AmountInput> {
  String amount = '';
  late TextEditingController editingController;

  @override
  void initState() {
    super.initState();
    amount = widget.value?.toString() ?? '';
    editingController = TextEditingController(text: amount);
  }

  Size getTextSize(String text, [TextStyle? style]) {
    TextPainter painter = TextPainter(
      text: TextSpan(text: text, style: style),
      textDirection: TextDirection.ltr,
      maxLines: 1,
      ellipsis: '...',
    );
    painter.layout();
    return painter.size;
  }

  @override
  Widget build(BuildContext context) {
    final textStyle =
        widget.textStyle ?? Theme.of(context).textTheme.bodyMedium;
    return SizedBox(
      width: getTextSize(
            '${widget.symbol} ${amount.isEmpty ? (widget.hintText ?? '') : amount}',
            textStyle,
          ).width +
          (widget.spacer ?? 3.w),
      child: TextField(
        textAlign: TextAlign.end,
        controller: editingController,
        style: textStyle,
        autofocus: true,
        focusNode: widget.focusNode,
        inputFormatters: [
          FilteringTextInputFormatter.allow(
            RegExp(widget.isDecimal ? r'[0-9\.]' : r'[0-9]'),
          ),
        ],
        onChanged: (newValue) {
          setState(() {
            amount = newValue;
          });
          widget.onChanged?.call(double.tryParse(newValue));
        },
        decoration: InputDecoration(
          prefix: Text(widget.symbol),
          border: InputBorder.none,
        ),
      ),
    );
  }
}


另外,由于初始化时宽度比较窄,为了方便操作,建议在组件外层增加一个tap事件来获取焦点

部分代码:

GestureDetector(
  onTap: () {
    focusNode.requestFocus();
  },
  child: Card(
    child: Padding(
      padding: EdgeInsets.all(14.w),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text('SGD', style: theme.textTheme.titleLarge),
              const Spacer(),
              AmountInput(
                focusNode: focusNode,
                onChanged:(newValue){
                    print(newValue);
                },
              ),
            ],
          ),
          Text(
            'Last 30 days: S\$0',
            style: theme.textTheme.titleSmall,
          ),
        ],
      ),
    ),
  ),
),