扩展的小部件成为新的 Flutter 应用程序

问题描述 投票:0回答:1

我有一个 Flutter 应用程序和一个自定义输入字段。在其中,可以添加文本和标签。我有两个问题:

  1. 最初,文本字段被包裹在 Expanded 小部件中。通过设置 maxLines 和 minLines,如果文本超过父宽度,我可以将文本分成两行。但是,当我添加标签时,整个文本都会出现在第一行,这是意料之外的。标签应放置在第二行文本旁边。
  2. 添加的最后一个文本字段被展开,但是,这会将文本字段放在新行上。它应该用文本和标签填充当前行的剩余宽度,如果超出父宽度,文本应该换行。

这是完整的代码:

import 'package:flutter/material.dart';
import 'package:tagfield_demo/tag_textfield.dart';

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

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final GlobalKey<TagTextFieldState> _key = GlobalKey();

  @override
  Widget build(BuildContext context) {
    Future.delayed(const Duration(milliseconds: 100)).then((val) {
      setState(() {
        print(val);
      });
    });
    return TextFieldTapRegion(
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 10),
                child: TagTextField(
                  key: _key,
                  initialText: "",
                  tagTextBuilder: (BuildContext context, String tag) {
                    switch (tag) {
                      case '[Amount]':
                        return 'Amount';
                      case '[Location]':
                        return 'Location';
                    }

                    return tag;
                  },
                ),
              ),
              Row(
                children: [
                  TextButton(
                    child: const Text("Add Location tag"),
                    onPressed: () {
                      if (_key.currentState != null) {
                        _key.currentState!.addTag('[Location]');
                      }
                    },
                  ),
                  TextButton(
                    child: const Text("Add Amount tag"),
                    onPressed: () {
                      if (_key.currentState != null) {
                        _key.currentState!.addTag('[Amount]');
                      }
                    },
                  )
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}

自定义标签输入字段:

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

class TagTextField extends StatefulWidget {
  final String initialText;
  final String Function(BuildContext context, String tag)? tagTextBuilder;
  final String? hint;

  const TagTextField({
    super.key,
    this.initialText = '',
    this.tagTextBuilder,
    this.hint,
  });

  @override
  State<TagTextField> createState() => TagTextFieldState();
}

class TagTextFieldState extends State<TagTextField> {
  late String _text;
  late List<Widget> _children;
  late List<TextEditingController?> _textControllers;
  late List<GlobalKey<_TagState>?> _tagKeys;
  late List<FocusNode?> _focusNodes;
  final FocusNode _mainFocusNode = FocusNode(); // Add this focus node

  @override
  void initState() {
    super.initState();

    _text = widget.initialText;

    _children = [];
    _textControllers = [];
    _focusNodes = [];
    _tagKeys = [];

    SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
      _refreshChildren();
    });
  }

  @override
  void dispose() {
    for (var node in _focusNodes) {
      if (node != null) {
        node.dispose();
      }
    }

    for (var controller in _textControllers) {
      if (controller != null) {
        controller.dispose();
      }
    }

    for (var tag in _tagKeys) {
      if (tag != null && tag.currentState != null) {
        tag.currentState!.dispose();
      }
    }

    _mainFocusNode.dispose(); // Dispose the main focus node

    super.dispose();
  }

  addTag(String tag) {
    //Adds tag on cursor position
    int idx = 0;
    int position = 0;
    for (var node in _focusNodes) {
      if (node != null) {
        var controller = _textControllers[idx];

        if (node.hasFocus) {
          var cursorPos = controller!.selection.base.offset;
          position += cursorPos;
          break;
        } else {
          position += controller!.text.length;
        }
      } else {
        var tag = _tagKeys[idx];
        if (tag != null && tag.currentState != null) {
          position += tag.currentState!.getTag().length;
        }
      }

      idx++;
    }

    //Add tag in text
    _text = _text.replaceRange(position, position, tag);

    for (var node in _focusNodes) {
      if (node != null) {
        node.unfocus();
      }
    }

    setState(() {
      _refreshChildren();
    });
  }

  getText() {
    int idx = 0;

    _text = '';

    for (var controller in _textControllers) {
      if (controller != null) {
        _text += controller.text;
      } else {
        var tag = _tagKeys[idx];
        if (tag != null && tag.currentState != null) {
          _text += tag.currentState!.getTag();
        }
      }

      idx++;
    }

    return _text;
  }

  void _onFocusChange(FocusNode focusNode) {
    if (focusNode.hasFocus) {
      for (var node in _focusNodes) {
        if (node != null && node != focusNode) {
          node.unfocus();
        }
      }
    }
  }

  bool _onKeyEvent(KeyEvent event, FocusNode focusNode) {
    if (event is KeyDownEvent &&
        event.logicalKey == LogicalKeyboardKey.backspace) {
      final idx = _focusNodes.indexOf(focusNode);
      if (idx > 0 && _textControllers[idx]!.selection.base.offset == 0) {
        final prevIdx = idx - 1;
        if (_textControllers[prevIdx] == null && _tagKeys[prevIdx] != null) {
          final tag = _tagKeys[prevIdx]!.currentState!.getTag();
          _text = _text.replaceRange(
              _tagKeys[prevIdx]!.currentState!.widget.startPos,
              _tagKeys[prevIdx]!.currentState!.widget.startPos + tag.length,
              '');
          for (var node in _focusNodes) {
            if (node != null) {
              node.unfocus();
            }
          }

          setState(() {
            _refreshChildren();
          });
        }
      }
      return true;
    }
    return false;
  }

  _createTextField({String? text, bool showHint = false, bool isLast = false}) {
    _textControllers.add(TextEditingController(text: text));

    _textControllers.last!.addListener(() {
      setState(() {
        getText();
      });
    });

    _focusNodes.add(FocusNode());
    _focusNodes.last!.addListener(() {
      _onFocusChange(_focusNodes.last!);
    });

    // Add key event listener
    _focusNodes.last!.addListener(() {
      if (_focusNodes.last!.hasFocus) {
        HardwareKeyboard.instance.addHandler((event) {
          return _onKeyEvent(event, _focusNodes.last!);
        });
      } else {
        HardwareKeyboard.instance.removeHandler((event) {
          return _onKeyEvent(event, _focusNodes.last!);
        });
      }
    });

    //Add empty elements, so indexes match
    _tagKeys.add(null);

    Widget textField = TextField(
      focusNode: _focusNodes.last,
      controller: _textControllers.last,
      decoration: InputDecoration(
        hintText: showHint ? widget.hint : null,
        border: InputBorder.none,
      ),
    );

    if (showHint || isLast) {
      textField = Expanded(
        child: TextField(
          focusNode: _focusNodes.last,
          controller: _textControllers.last,
          minLines: 1,
          maxLines: null,
          decoration: InputDecoration(
            hintText: showHint ? widget.hint : null,
            border: InputBorder.none,
          ),
        ),
      );
    } else {
      textField = IntrinsicWidth(child: textField);
    }

    _children.add(
      Baseline(
        baseline: 20.0,
        baselineType: TextBaseline.alphabetic,
        child: textField,
      ),
    );
  }

  _createTag(String tag, int startPos) {
    if (_children.isEmpty) {
      _createTextField();
    }

    String label = tag;
    if (widget.tagTextBuilder != null) {
      label = widget.tagTextBuilder!(context, tag);
    }

    //Add empty elements, so indexes match
    _textControllers.add(null);
    _focusNodes.add(null);
    _tagKeys.add(GlobalKey<_TagState>());

    _children.add(
      Baseline(
        baseline: 20.0,
        baselineType: TextBaseline.alphabetic,
        child: Tag(
          key: _tagKeys.last,
          label: label,
          tag: tag,
          startPos: startPos,
          onDelete: () {
            _text = _text.replaceRange(startPos, startPos + tag.length, '');
            for (var node in _focusNodes) {
              if (node != null) {
                node.unfocus();
              }
            }

            setState(() {
              _refreshChildren();
            });
          },
        ),
      ),
    );
  }

  _refreshChildren() {
    _children.clear();
    _textControllers.clear();
    _focusNodes.clear();
    _tagKeys.clear();

    final regex = RegExp(r'(\[(.*?)\])');
    final matches = regex.allMatches(_text);
    var currentIndex = 0;

    if (_text.isEmpty) {
      //Only one starting text field
      _createTextField(showHint: true);
      return;
    }

    for (final match in matches) {
      final beforeText = _text.substring(currentIndex, match.start);
      if (beforeText.isNotEmpty) {
        _createTextField(text: beforeText);
      } else {
        _createTextField();
      }

      final tag = match.group(1);

      _createTag(tag!, match.start);

      currentIndex = match.end;
    }

    final afterText = _text.substring(currentIndex);
    if (afterText.isNotEmpty) {
      _createTextField(text: afterText, isLast: true);
    } else {
      _createTextField(isLast: true);
    }
  }

  @override
  Widget build(BuildContext context) {
    return KeyboardListener(
      focusNode: _mainFocusNode, // Set the main focus node
      child: Container(
        decoration: BoxDecoration(border: Border.all(color: Colors.blueAccent)),
        child: Wrap(
          children: _children,
        ),
      ),
      onKeyEvent: (event) {
        _onKeyEvent(event, _mainFocusNode);
      },
    );
  }
}

class Tag extends StatefulWidget {
  final String tag;
  final String label;
  final int startPos;
  final VoidCallback onDelete;

  const Tag(
      {super.key,
      required this.tag,
      required this.label,
      required this.startPos,
      required this.onDelete});

  @override
  State<Tag> createState() => _TagState();
}

class _TagState extends State<Tag> {
  String getTag() {
    return widget.tag;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: const Color(0xFF2A2C42),
        borderRadius: BorderRadius.circular(90),
      ),
      margin: const EdgeInsets.symmetric(horizontal: 2),
      padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            widget.label.toUpperCase(),
            style: const TextStyle(
              fontSize: 12,
              color: Colors.white,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(width: 5),
          SizedBox(
            width: 20,
            height: 20,
            child: FloatingActionButton(
              onPressed: widget.onDelete,
              backgroundColor: const Color(0xFF2A2C42),
              mini: true,
              child: const Icon(Icons.close),
            ),
          )
        ],
      ),
    );
  }
}
flutter
1个回答
0
投票

您需要删除

if/else
中的这个
_createTextField
语句:

if (showHint || isLast)

并且总是用

textField
包裹
IntrinsicWidth

Widget textField = IntrinsicWidth(
          child: TextField(
            focusNode: _focusNodes.last,
            controller: _textControllers.last,
            maxLines: null,
            decoration: InputDecoration(
              hintText: showHint ? widget.hint : null,
              border: InputBorder.none,
              //suffixIcon: Icon(Icons.pending),
            ),
          ),
        );

这可能是因为

TextField
小部件总是会自然地扩展到最大可用宽度空间,因此从宽度更大的新行开始。

有边框的容器会缩小,可以通过添加参数来修复:

width: double.infinity

现在唯一的问题是您需要按文本字段的最左角才能向上移动光标/焦点。因此,为了解决这个问题,我添加了一个

GestureDetector
,以请求在点击容器内的任意位置时将焦点放在最后一个
focusNode
上。

return KeyboardListener(
      focusNode: _mainFocusNode, // Set the main focus node
      child: Container(
        width: double.infinity,
        decoration: BoxDecoration(border: Border.all(color: Colors.blueAccent)),
        child: GestureDetector(
          behavior: HitTestBehavior.translucent,
          onTap: () {
            setState(() {
              FocusScope.of(context).requestFocus(_focusNodes.last);
            });
          },
          child: Wrap(
            children: _children,
          ),
        ),
      ),
      onKeyEvent...

DartPad 上的演示:https://dartpad.dev/?id=05d8f3eed9e6f541514477e52a2e3ccd

© www.soinside.com 2019 - 2024. All rights reserved.