如何在Flutter中建立芯片输入字段?

时间:2018-09-03 19:19:40

标签: flutter flutter-layout

3 个答案:

答案 0 :(得分:6)

您可以在此处找到Chip Input Field类型小部件的实现:

最新:https://gist.github.com/slightfoot/c6c0f1f1baca326a389a9aec47886ad6

import 'dart:async';

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

// See: https://twitter.com/shakil807/status/1042127387515858949
// https://github.com/pchmn/MaterialChipsInput/tree/master/library/src/main/java/com/pchmn/materialchips
// https://github.com/BelooS/ChipsLayoutManager

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

class ChipsDemoApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primaryColor: Colors.indigo,
        accentColor: Colors.pink,
      ),
      home: DemoScreen(),
    );
  }
}

class DemoScreen extends StatefulWidget {
  @override
  _DemoScreenState createState() => _DemoScreenState();
}

class _DemoScreenState extends State<DemoScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Material Chips Input'),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              decoration: const InputDecoration(hintText: 'normal'),
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: ChipsInput<AppProfile>(
                decoration: InputDecoration(prefixIcon: Icon(Icons.search), hintText: 'Profile search'),
                findSuggestions: _findSuggestions,
                onChanged: _onChanged,
                chipBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
                  return InputChip(
                    key: ObjectKey(profile),
                    label: Text(profile.name),
                    avatar: CircleAvatar(
                      backgroundImage: NetworkImage(profile.imageUrl),
                    ),
                    onDeleted: () => state.deleteChip(profile),
                    onSelected: (_) => _onChipTapped(profile),
                    materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                  );
                },
                suggestionBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
                  return ListTile(
                    key: ObjectKey(profile),
                    leading: CircleAvatar(
                      backgroundImage: NetworkImage(profile.imageUrl),
                    ),
                    title: Text(profile.name),
                    subtitle: Text(profile.email),
                    onTap: () => state.selectSuggestion(profile),
                  );
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  void _onChipTapped(AppProfile profile) {
    print('$profile');
  }

  void _onChanged(List<AppProfile> data) {
    print('onChanged $data');
  }

  Future<List<AppProfile>> _findSuggestions(String query) async {
    if (query.length != 0) {
      return mockResults.where((profile) {
        return profile.name.contains(query) || profile.email.contains(query);
      }).toList(growable: false);
    } else {
      return const <AppProfile>[];
    }
  }
}

// -------------------------------------------------

const mockResults = <AppProfile>[
  AppProfile('Stock Man', 'stock@man.com', 'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg'),
  AppProfile('Paul', 'paul@google.com', 'https://mbtskoudsalg.com/images/person-stock-image-png.png'),
  AppProfile('Fred', 'fred@google.com',
      'https://media.istockphoto.com/photos/feeling-great-about-my-corporate-choices-picture-id507296326'),
  AppProfile('Bera', 'bera@flutter.io',
      'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
  AppProfile('John', 'john@flutter.io',
      'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
  AppProfile('Thomas', 'thomas@flutter.io',
      'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
  AppProfile('Norbert', 'norbert@flutter.io',
      'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
  AppProfile('Marina', 'marina@flutter.io',
      'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
];

class AppProfile {
  final String name;
  final String email;
  final String imageUrl;

  const AppProfile(this.name, this.email, this.imageUrl);

  @override
  bool operator ==(Object other) =>
      identical(this, other) || other is AppProfile && runtimeType == other.runtimeType && name == other.name;

  @override
  int get hashCode => name.hashCode;

  @override
  String toString() {
    return 'Profile{$name}';
  }
}

// -------------------------------------------------

typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(BuildContext context, ChipsInputState<T> state, T data);

class ChipsInput<T> extends StatefulWidget {
  const ChipsInput({
    Key key,
    this.decoration = const InputDecoration(),
    @required this.chipBuilder,
    @required this.suggestionBuilder,
    @required this.findSuggestions,
    @required this.onChanged,
    this.onChipTapped,
  }) : super(key: key);

  final InputDecoration decoration;
  final ChipsInputSuggestions findSuggestions;
  final ValueChanged<List<T>> onChanged;
  final ValueChanged<T> onChipTapped;
  final ChipsBuilder<T> chipBuilder;
  final ChipsBuilder<T> suggestionBuilder;

  @override
  ChipsInputState<T> createState() => ChipsInputState<T>();
}

class ChipsInputState<T> extends State<ChipsInput<T>> implements TextInputClient {
  static const kObjectReplacementChar = 0xFFFC;

  Set<T> _chips = Set<T>();
  List<T> _suggestions;
  int _searchId = 0;

  FocusNode _focusNode;
  TextEditingValue _value = TextEditingValue();
  TextInputConnection _connection;

  String get text => String.fromCharCodes(
        _value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
      );

  bool get _hasInputConnection => _connection != null && _connection.attached;

  void requestKeyboard() {
    if (_focusNode.hasFocus) {
      _openInputConnection();
    } else {
      FocusScope.of(context).requestFocus(_focusNode);
    }
  }

  void selectSuggestion(T data) {
    setState(() {
      _chips.add(data);
      _updateTextInputState();
      _suggestions = null;
    });
    widget.onChanged(_chips.toList(growable: false));
  }

  void deleteChip(T data) {
    setState(() {
      _chips.remove(data);
      _updateTextInputState();
    });
    widget.onChanged(_chips.toList(growable: false));
  }

  @override
  void initState() {
    super.initState();
    _focusNode = FocusNode();
    _focusNode.addListener(_onFocusChanged);
  }

  void _onFocusChanged() {
    if (_focusNode.hasFocus) {
      _openInputConnection();
    } else {
      _closeInputConnectionIfNeeded();
    }
    setState(() {
      // rebuild so that _TextCursor is hidden.
    });
  }

  @override
  void dispose() {
    _focusNode?.dispose();
    _closeInputConnectionIfNeeded();
    super.dispose();
  }

  void _openInputConnection() {
    if (!_hasInputConnection) {
      _connection = TextInput.attach(this, TextInputConfiguration());
      _connection.setEditingState(_value);
    }
    _connection.show();
  }

  void _closeInputConnectionIfNeeded() {
    if (_hasInputConnection) {
      _connection.close();
      _connection = null;
    }
  }

  @override
  Widget build(BuildContext context) {
    var chipsChildren = _chips
        .map<Widget>(
          (data) => widget.chipBuilder(context, this, data),
        )
        .toList();

    final theme = Theme.of(context);

    chipsChildren.add(
      Container(
        height: 32.0,
        child: Row(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Text(
              text,
              style: theme.textTheme.subhead.copyWith(
                height: 1.5,
              ),
            ),
            _TextCaret(
              resumed: _focusNode.hasFocus,
            ),
          ],
        ),
      ),
    );

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      //mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        GestureDetector(
          behavior: HitTestBehavior.opaque,
          onTap: requestKeyboard,
          child: InputDecorator(
            decoration: widget.decoration,
            isFocused: _focusNode.hasFocus,
            isEmpty: _value.text.length == 0,
            child: Wrap(
              children: chipsChildren,
              spacing: 4.0,
              runSpacing: 4.0,
            ),
          ),
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _suggestions?.length ?? 0,
            itemBuilder: (BuildContext context, int index) {
              return widget.suggestionBuilder(context, this, _suggestions[index]);
            },
          ),
        ),
      ],
    );
  }

  @override
  void updateEditingValue(TextEditingValue value) {
    final oldCount = _countReplacements(_value);
    final newCount = _countReplacements(value);
    setState(() {
      if (newCount < oldCount) {
        _chips = Set.from(_chips.take(newCount));
      }
      _value = value;
    });
    _onSearchChanged(text);
  }

  int _countReplacements(TextEditingValue value) {
    return value.text.codeUnits.where((ch) => ch == kObjectReplacementChar).length;
  }

  @override
  void performAction(TextInputAction action) {
    _focusNode.unfocus();
  }

  void _updateTextInputState() {
    final text = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
    _value = TextEditingValue(
      text: text,
      selection: TextSelection.collapsed(offset: text.length),
      composing: TextRange(start: 0, end: text.length),
    );
    _connection.setEditingState(_value);
  }

  void _onSearchChanged(String value) async {
    final localId = ++_searchId;
    final results = await widget.findSuggestions(value);
    if (_searchId == localId && mounted) {
      setState(() => _suggestions = results.where((profile) => !_chips.contains(profile)).toList(growable: false));
    }
  }
}

class _TextCaret extends StatefulWidget {
  const _TextCaret({
    Key key,
    this.duration = const Duration(milliseconds: 500),
    this.resumed = false,
  }) : super(key: key);

  final Duration duration;
  final bool resumed;

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

class _TextCursorState extends State<_TextCaret> with SingleTickerProviderStateMixin {
  bool _displayed = false;
  Timer _timer;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(widget.duration, _onTimer);
  }

  void _onTimer(Timer timer) {
    setState(() => _displayed = !_displayed);
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return FractionallySizedBox(
      heightFactor: 0.7,
      child: Opacity(
        opacity: _displayed && widget.resumed ? 1.0 : 0.0,
        child: Container(
          width: 2.0,
          color: theme.primaryColor,
        ),
      ),
    );
  }
}

答案 1 :(得分:2)

我实现了一个标签,当在 TextField 中接收到用户输入并输入一个分隔符时,将创建一个标签。

我尝试通过参考包 flutter_chips_input

来实现它

最新:https://gist.github.com/battlecook/2afbc23e17d4d77069681e21c862b692 .


import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';


class TextCursor extends StatefulWidget {
  const TextCursor({
    Key key,
    this.duration = const Duration(milliseconds: 500),
    this.resumed = false,
  }) : super(key: key);

  final Duration duration;
  final bool resumed;

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

class _TextCursorState extends State<TextCursor> with SingleTickerProviderStateMixin {
  bool _displayed = false;
  Timer _timer;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(widget.duration, _onTimer);
  }

  void _onTimer(Timer timer) {
    setState(() => _displayed = !_displayed);
  }

  @override
  void dispose() {
    TextField();
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return FractionallySizedBox(
      heightFactor: 0.7,
      child: Opacity(
        opacity: _displayed && widget.resumed ? 1.0 : 0.0,
        child: Container(
          width: 2.0,
          color: theme.cursorColor,
        ),
      ),
    );
  }
}

typedef ChipsBuilder<T> = Widget Function(BuildContext context, ChipsInputState<T> state, T data);
typedef ChipTextValidator = int Function(String value);

const kObjectReplacementChar = 0xFFFD;

extension on TextEditingValue {
  String get normalCharactersText => String.fromCharCodes(
    text.codeUnits.where((ch) => ch != kObjectReplacementChar),
  );

  List<int> get replacementCharacters => text.codeUnits.where((ch) => ch == kObjectReplacementChar).toList(growable: false);

  int get replacementCharactersCount => replacementCharacters.length;
}

class ChipsInput<T> extends StatefulWidget {
  ChipsInput({
    Key key,
    this.decoration = const InputDecoration(),
    this.enabled = true,
    this.width,
    @required this.chipBuilder,
    this.addChip,
    this.deleteChip,
    this.onChangedTag,
    this.initialTags = const <String>[],
    this.separator = ' ',
    this.chipTextValidator,
    this.chipSpacing = 6,
    this.maxChips = 5,
    this.maxTagSize = 10,
    this.maxTagColor = Colors.red,
    this.textStyle,
    this.countTextStyle = const TextStyle(color: Colors.black),
    this.countMaxTextStyle = const TextStyle(color: Colors.red),
    this.inputType = TextInputType.text,
    this.textOverflow = TextOverflow.clip,
    this.obscureText = false,
    this.autocorrect = true,
    this.actionLabel,
    this.inputAction = TextInputAction.done,
    this.keyboardAppearance = Brightness.light,
    this.textCapitalization = TextCapitalization.none,
    this.autofocus = false,
    this.focusNode,
  })  : assert(initialTags.length <= maxChips),
        assert(separator.length == 1),
        assert(chipSpacing > 0),
        super(key: key);

  final InputDecoration decoration;
  final TextStyle textStyle;
  final double width;
  final bool enabled;
  final ChipsBuilder<T> chipBuilder;
  final ValueChanged<String> addChip;
  final Function() deleteChip;
  final Function() onChangedTag;
  final String separator;
  final ChipTextValidator chipTextValidator;
  final double chipSpacing;
  final int maxTagSize;
  final Color maxTagColor;

  final List<String> initialTags;
  final int maxChips;
  final TextStyle countTextStyle;
  final TextStyle countMaxTextStyle;

  final TextInputType inputType;
  final TextOverflow textOverflow;
  final bool obscureText;
  final bool autocorrect;
  final String actionLabel;
  final TextInputAction inputAction;
  final Brightness keyboardAppearance;
  final bool autofocus;
  final FocusNode focusNode;

  final TextCapitalization textCapitalization;

  @override
  ChipsInputState<T> createState() => ChipsInputState<T>();
}

class ChipsInputState<T> extends State<ChipsInput<T>> implements TextInputClient {
  Set<T> _chips = Set<T>();
  TextEditingValue _value = TextEditingValue();
  TextInputConnection _textInputConnection;
  Size size;
  Map<T, String> _enteredTexts = {};
  List<String> _enteredTags = [];

  FocusNode _focusNode;

  FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());

  TextInputConfiguration get textInputConfiguration => TextInputConfiguration(
    inputType: widget.inputType,
    obscureText: widget.obscureText,
    autocorrect: widget.autocorrect,
    actionLabel: widget.actionLabel,
    inputAction: widget.inputAction,
    keyboardAppearance: widget.keyboardAppearance,
    textCapitalization: widget.textCapitalization,
  );

  bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;

  ScrollController _chipScrollController = new ScrollController();

  ScrollController _inputTextScrollController = new ScrollController();

  double _inputTextSize;
  double _countSizeBox;
  double _chipBoxSize;

  bool _isMaxTag = false;

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

    widget.initialTags.forEach((String tag) {
      //widget.addChip(tag);
    });

    _enteredTags.addAll(widget.initialTags);

    _effectiveFocusNode.addListener(_handleFocusChanged);

    final String initText = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));

    TextEditingValue initValue = new TextEditingValue(text: initText);

    initValue = initValue.copyWith(
      text: initText,
      selection: TextSelection.collapsed(offset: initText.length),
    );

    if (_textInputConnection == null) {
      _textInputConnection = TextInput.attach(this, textInputConfiguration)..setEditingState(initValue);
    }

    _updateTextInput(putText: _value.normalCharactersText);

    _scrollToEnd(_inputTextScrollController);

    _chipBoxSize = widget.width * 0.7;
    _inputTextSize = widget.width * 0.1;
    _countSizeBox = widget.width * 0.1;

    _chipScrollController.addListener(() {
      if (_chipScrollController.position.viewportDimension + _inputTextScrollController.position.viewportDimension > widget.width * 0.8) {
        _inputTextSize = _inputTextScrollController.position.viewportDimension;
        _chipBoxSize = widget.width * 0.8 - _inputTextSize;
        setState(() {});
      }
    });

    WidgetsBinding.instance.addPostFrameCallback((_) async {
      if (mounted && widget.autofocus && _effectiveFocusNode != null) {
        FocusScope.of(context).autofocus(_effectiveFocusNode);
      }
    });
  }

  void _handleFocusChanged() {
    if (_effectiveFocusNode.hasFocus) {
      _openInputConnection();
    } else {
      _closeInputConnectionIfNeeded();
    }
    if (mounted) {
      setState(() {});
    }
  }

  void _openInputConnection() {
    if (!_hasInputConnection) {
      _textInputConnection = TextInput.attach(this, textInputConfiguration)..setEditingState(_value);
    }
    _textInputConnection.show();

    Future.delayed(Duration(milliseconds: 100), () {
      WidgetsBinding.instance.addPostFrameCallback((_) async {
        RenderBox renderBox = context.findRenderObject();
        Scrollable.of(context)?.position?.ensureVisible(renderBox);
      });
    });
  }

  void _closeInputConnectionIfNeeded() {
    if (_hasInputConnection) {
      _textInputConnection.close();
      _textInputConnection = null;
    }
  }

  List<String> getTags() {
    List<String> tags = [];
    _chips.forEach((element) {
      tags.add(element.toString());
    });

    return tags;
  }

  void deleteChip(T data) {
    if (widget.enabled) {
      _chips.remove(data);
      if (_enteredTexts.containsKey(data)) {
        _enteredTexts.remove(data);
      }
      _updateTextInput(putText: _value.normalCharactersText);
    }
    if (widget.deleteChip != null) {
      widget.deleteChip();
    }
  }

  @override
  void connectionClosed() {}

  @override
  TextEditingValue get currentTextEditingValue => _value;

  @override
  void performAction(TextInputAction action) {
    switch (action) {
      case TextInputAction.done:
      case TextInputAction.go:
      case TextInputAction.send:
      case TextInputAction.search:
      default:
        break;
    }
  }

  @override
  void updateEditingValue(TextEditingValue value) {
    if (_chipScrollController.hasClients) {
      _inputTextSize = _inputTextScrollController.position.viewportDimension + 20;
      _chipBoxSize = widget.width * 0.8 - _inputTextScrollController.position.viewportDimension;
    }

    _isMaxTag = false;

    int index = widget.chipTextValidator(value.text);
    if (index == -1) {

    }

    var _newTextEditingValue = value;
    var _oldTextEditingValue = _value;

    if (_newTextEditingValue.replacementCharactersCount >= _oldTextEditingValue.replacementCharactersCount && _chips.length >= widget.maxChips) {
      _updateTextInput();
      _textInputConnection.setEditingState(_value);
      return;
    }

    if (_newTextEditingValue.text != _oldTextEditingValue.text) {
      if(_newTextEditingValue.text == widget.separator) {
        _updateTextInput();
        return;
      }

      setState(() {
        _value = value;
      });

      if (_newTextEditingValue.replacementCharactersCount < _oldTextEditingValue.replacementCharactersCount) {
        _chips = Set.from(_chips.take(_newTextEditingValue.replacementCharactersCount));
      }
      _updateTextInput(putText: _value.normalCharactersText);
    }

    String tagText = _value.normalCharactersText;
    if (tagText.length > 0) {
      String lastString = tagText.substring(tagText.length - 1);
      if (tagText.length >= widget.maxTagSize && lastString != widget.separator) {
        _isMaxTag = true;
        _updateTextInput(putText: tagText.substring(0, widget.maxTagSize));
        return;
      }

      if (lastString == widget.separator) {
        String newTag = tagText.substring(0, tagText.length - 1);
        if(newTag.isEmpty) {
          _updateTextInput();
          return;
        }
        _chips.add(newTag as T);
        if (widget.onChangedTag != null) {
          widget.onChangedTag();
        }
        _enteredTags.add(newTag);
        _updateTextInput();
      }
    }

    //_textInputConnection.setEditingState(_value);
  }

  void addChip(T data) {
    String enteredText = _value.normalCharactersText ?? '';
    if (enteredText.isNotEmpty) _enteredTexts[data] = enteredText;
    _chips.add(data);
  }

  void _updateTextInput({String putText = ''}) {
    final String updatedText = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar)) + putText;
    setState(() {
      _value = _value.copyWith(
        text: updatedText,
        selection: TextSelection.collapsed(offset: updatedText.length),
      );
    });

    if (_textInputConnection == null) {
      _textInputConnection = TextInput.attach(this, textInputConfiguration);
    }

    _textInputConnection.setEditingState(_value);
  }

  @override
  void updateFloatingCursor(RawFloatingCursorPoint point) {}

  void _scrollToEnd(ScrollController controller) {
    Timer(Duration(milliseconds: 100), () {
      controller.jumpTo(controller.position.maxScrollExtent);
    });
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> chipsChildren = _chips.map<Widget>((data) => widget.chipBuilder(context, this, data)).toList();
    Widget chipsBox = ConstrainedBox(
      constraints: BoxConstraints(
        maxWidth: _chipBoxSize,
      ),
      child: SingleChildScrollView(
        controller: _chipScrollController,
        scrollDirection: Axis.horizontal,
        child: Wrap(
          spacing: widget.chipSpacing,
          children: chipsChildren,
        ),
      ),
    );

    int maxCount = widget.maxChips;
    int currentCount = chipsChildren.length;

    List<String> tagAll = [];
    _chips.forEach((element) {
      tagAll.add(element.toString());
    });

    _scrollToEnd(_chipScrollController);
    _scrollToEnd(_inputTextScrollController);

    Widget countWidget = SizedBox.shrink();
    TextStyle countWidgetTextStyle = widget.countTextStyle;
    if (widget.maxChips != null) {
      if (widget.maxChips <= chipsChildren.length) {
        countWidgetTextStyle = widget.countMaxTextStyle;
      }
      countWidget = Text(currentCount.toString() + "/" + maxCount.toString(), style: countWidgetTextStyle);
    }

    double leftPaddingSize = 0;
    if (_chips.length > 0) {
      leftPaddingSize = widget.chipSpacing;
    }

    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () {
        FocusScope.of(context).requestFocus(_effectiveFocusNode);
        _textInputConnection?.show();
      },
      child: InputDecorator(
        decoration: widget.decoration,
        isFocused: _effectiveFocusNode.hasFocus,
        isEmpty: _value.text.length == 0 && _chips.length == 0,
        child: Row(
          children: <Widget>[
            chipsBox,
            Padding(
              padding: EdgeInsets.only(left: leftPaddingSize),
            ),
            ConstrainedBox(
              constraints: BoxConstraints(
                maxWidth: _inputTextSize,
                maxHeight: 32.0,
              ),
              child: SingleChildScrollView(
                controller: _inputTextScrollController,
                scrollDirection: Axis.horizontal,
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: <Widget>[
                    Flexible(
                      flex: 1,
                      child: Center(
                        child: Text(
                          _value.normalCharactersText,
                          maxLines: 1,
                          overflow: widget.textOverflow,
                          style: widget.textStyle,
                          //style: TextStyle(height: _textStyle.height, color: c, fontFamily: _textStyle.fontFamily, fontSize: _textStyle.fontSize),
                        ),
                      ),
                    ),
                    Flexible(flex: 0, child: TextCursor(resumed: _effectiveFocusNode.hasFocus)),
                  ],
                ),
              ),
            ),
            Spacer(),
            SizedBox(
                width: _countSizeBox,
                child: Row(
                  children: <Widget>[
                    Padding(
                      padding: const EdgeInsets.only(left: 8),
                    ),
                    countWidget,
                  ],
                )),
          ],
        ),
      ),
    );
  }

  @override
  // TODO: implement currentAutofillScope
  AutofillScope get currentAutofillScope => throw UnimplementedError();

  @override
  void showAutocorrectionPromptRect(int start, int end) {
    // TODO: implement showAutocorrectionPromptRect
  }

  @override
  void performPrivateCommand(String action, Map<String, dynamic> data) {
    // TODO: implement performPrivateCommand
  }
}




class SampleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: HomeWidget());
  }
}

class HomeWidget extends StatelessWidget {
  GlobalKey<ChipsInputState> _chipKey = GlobalKey();

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Container(
            child: ChipsInput(
              key: _chipKey,
              keyboardAppearance: Brightness.dark,
              textCapitalization: TextCapitalization.words,
              width: MediaQuery.of(context).size.width,
              enabled: true,
              maxChips: 5,
              separator: ' ',
              decoration: InputDecoration(
                hintText: 'Enter Tag...',
              ),
              initialTags: [],
              autofocus: true,
              chipTextValidator: (String value) {
                value.contains('!');
                return -1;
              },
              chipBuilder: (context, state, String tag) {
                return InputChip(
                  labelPadding: const EdgeInsets.only(left: 8.0, right: 3),
                  backgroundColor: Colors.white,
                  shape: StadiumBorder(side: BorderSide(width: 1.8, color: Color.fromRGBO(228, 230, 235, 1))),
                  shadowColor: Colors.grey,
                  key: ObjectKey(tag),
                  label: Text(
                    "# " + tag.toString(),
                    textAlign: TextAlign.center,
                  ),
                  onDeleted: () => state.deleteChip(tag),
                  deleteIconColor: Color.fromRGBO(138, 145, 151, 1),
                  materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
                );
              },
            ),
          ),
        ),
      ),
    );
  }
}

void main() {
  runApp(SampleWidget());
}

您可以通过将代码复制到dartpad来检查操作。

答案 2 :(得分:0)

您可以使用包flutter_chips_input
https://pub.dartlang.org/packages/flutter_chips_input
只想提供另一个选择。
您可以在下面查看示例:
enter image description here

ChipsInput(
initialValue: [
    AppProfile('John Doe', 'jdoe@flutter.io', 'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg')
],
decoration: InputDecoration(
    labelText: "Select People",
),
maxChips: 3,
findSuggestions: (String query) {
    if (query.length != 0) {
        var lowercaseQuery = query.toLowerCase();
        return mockResults.where((profile) {
            return profile.name.toLowerCase().contains(query.toLowerCase()) || profile.email.toLowerCase().contains(query.toLowerCase());
        }).toList(growable: false)
            ..sort((a, b) => a.name
                .toLowerCase()
                .indexOf(lowercaseQuery)
                .compareTo(b.name.toLowerCase().indexOf(lowercaseQuery)));
    } else {
        return const <AppProfile>[];
    }
},
onChanged: (data) {
    print(data);
},
chipBuilder: (context, state, profile) {
    return InputChip(
        key: ObjectKey(profile),
        label: Text(profile.name),
        avatar: CircleAvatar(
            backgroundImage: NetworkImage(profile.imageUrl),
        ),
        onDeleted: () => state.deleteChip(profile),
        materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
    );
},
suggestionBuilder: (context, state, profile) {
    return ListTile(
        key: ObjectKey(profile),
        leading: CircleAvatar(
            backgroundImage: NetworkImage(profile.imageUrl),
        ),
        title: Text(profile.name),
        subtitle: Text(profile.email),
        onTap: () => state.selectSuggestion(profile),
    );
},