flutter/examples/api/lib/material/input_chip/input_chip.1.dart
Sangam Shrestha d261411b4c
Remove redundant useMaterial3: true (#163376)
<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

This PR removes redundant useMaterial3: true as described in
https://github.com/flutter/flutter/issues/162818

*List which issues are fixed by this PR. You must list at least one
issue. An issue is not required if the PR fixes something trivial like a
typo.*

- https://github.com/flutter/flutter/issues/162818

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md

---------

Co-authored-by: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com>
2025-03-14 17:50:20 +00:00

345 lines
9.5 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
const List<String> _pizzaToppings = <String>[
'Olives',
'Tomato',
'Cheese',
'Pepperoni',
'Bacon',
'Onion',
'Jalapeno',
'Mushrooms',
'Pineapple',
];
void main() => runApp(const EditableChipFieldApp());
class EditableChipFieldApp extends StatelessWidget {
const EditableChipFieldApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: EditableChipFieldExample());
}
}
class EditableChipFieldExample extends StatefulWidget {
const EditableChipFieldExample({super.key});
@override
EditableChipFieldExampleState createState() {
return EditableChipFieldExampleState();
}
}
class EditableChipFieldExampleState extends State<EditableChipFieldExample> {
final FocusNode _chipFocusNode = FocusNode();
List<String> _toppings = <String>[_pizzaToppings.first];
List<String> _suggestions = <String>[];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Editable Chip Field Sample')),
body: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ChipsInput<String>(
values: _toppings,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.local_pizza_rounded),
hintText: 'Search for toppings',
),
strutStyle: const StrutStyle(fontSize: 15),
onChanged: _onChanged,
onSubmitted: _onSubmitted,
chipBuilder: _chipBuilder,
onTextChanged: _onSearchChanged,
),
),
if (_suggestions.isNotEmpty)
Expanded(
child: ListView.builder(
itemCount: _suggestions.length,
itemBuilder: (BuildContext context, int index) {
return ToppingSuggestion(_suggestions[index], onTap: _selectSuggestion);
},
),
),
],
),
);
}
Future<void> _onSearchChanged(String value) async {
final List<String> results = await _suggestionCallback(value);
setState(() {
_suggestions = results.where((String topping) => !_toppings.contains(topping)).toList();
});
}
Widget _chipBuilder(BuildContext context, String topping) {
return ToppingInputChip(topping: topping, onDeleted: _onChipDeleted, onSelected: _onChipTapped);
}
void _selectSuggestion(String topping) {
setState(() {
_toppings.add(topping);
_suggestions = <String>[];
});
}
void _onChipTapped(String topping) {}
void _onChipDeleted(String topping) {
setState(() {
_toppings.remove(topping);
_suggestions = <String>[];
});
}
void _onSubmitted(String text) {
if (text.trim().isNotEmpty) {
setState(() {
_toppings = <String>[..._toppings, text.trim()];
});
} else {
_chipFocusNode.unfocus();
setState(() {
_toppings = <String>[];
});
}
}
void _onChanged(List<String> data) {
setState(() {
_toppings = data;
});
}
FutureOr<List<String>> _suggestionCallback(String text) {
if (text.isNotEmpty) {
return _pizzaToppings.where((String topping) {
return topping.toLowerCase().contains(text.toLowerCase());
}).toList();
}
return const <String>[];
}
}
class ChipsInput<T> extends StatefulWidget {
const ChipsInput({
super.key,
required this.values,
this.decoration = const InputDecoration(),
this.style,
this.strutStyle,
required this.chipBuilder,
required this.onChanged,
this.onChipTapped,
this.onSubmitted,
this.onTextChanged,
});
final List<T> values;
final InputDecoration decoration;
final TextStyle? style;
final StrutStyle? strutStyle;
final ValueChanged<List<T>> onChanged;
final ValueChanged<T>? onChipTapped;
final ValueChanged<String>? onSubmitted;
final ValueChanged<String>? onTextChanged;
final Widget Function(BuildContext context, T data) chipBuilder;
@override
ChipsInputState<T> createState() => ChipsInputState<T>();
}
class ChipsInputState<T> extends State<ChipsInput<T>> {
@visibleForTesting
late final ChipsInputEditingController<T> controller;
String _previousText = '';
TextSelection? _previousSelection;
@override
void initState() {
super.initState();
controller = ChipsInputEditingController<T>(<T>[...widget.values], widget.chipBuilder);
controller.addListener(_textListener);
}
@override
void dispose() {
controller.removeListener(_textListener);
controller.dispose();
super.dispose();
}
void _textListener() {
final String currentText = controller.text;
if (_previousSelection != null) {
final int currentNumber = countReplacements(currentText);
final int previousNumber = countReplacements(_previousText);
final int cursorEnd = _previousSelection!.extentOffset;
final int cursorStart = _previousSelection!.baseOffset;
final List<T> values = <T>[...widget.values];
// If the current number and the previous number of replacements are different, then
// the user has deleted the InputChip using the keyboard. In this case, we trigger
// the onChanged callback. We need to be sure also that the current number of
// replacements is different from the input chip to avoid double-deletion.
if (currentNumber < previousNumber && currentNumber != values.length) {
if (cursorStart == cursorEnd) {
values.removeRange(cursorStart - 1, cursorEnd);
} else {
if (cursorStart > cursorEnd) {
values.removeRange(cursorEnd, cursorStart);
} else {
values.removeRange(cursorStart, cursorEnd);
}
}
widget.onChanged(values);
}
}
_previousText = currentText;
_previousSelection = controller.selection;
}
static int countReplacements(String text) {
return text.codeUnits
.where((int u) => u == ChipsInputEditingController.kObjectReplacementChar)
.length;
}
@override
Widget build(BuildContext context) {
controller.updateValues(<T>[...widget.values]);
return TextField(
minLines: 1,
maxLines: 3,
textInputAction: TextInputAction.done,
style: widget.style,
strutStyle: widget.strutStyle,
controller: controller,
onChanged: (String value) => widget.onTextChanged?.call(controller.textWithoutReplacements),
onSubmitted: (String value) => widget.onSubmitted?.call(controller.textWithoutReplacements),
);
}
}
class ChipsInputEditingController<T> extends TextEditingController {
ChipsInputEditingController(this.values, this.chipBuilder)
: super(text: String.fromCharCode(kObjectReplacementChar) * values.length);
// This constant character acts as a placeholder in the TextField text value.
// There will be one character for each of the InputChip displayed.
static const int kObjectReplacementChar = 0xFFFE;
List<T> values;
final Widget Function(BuildContext context, T data) chipBuilder;
/// Called whenever chip is either added or removed
/// from the outside the context of the text field.
void updateValues(List<T> values) {
if (values.length != this.values.length) {
final String char = String.fromCharCode(kObjectReplacementChar);
final int length = values.length;
value = TextEditingValue(
text: char * length,
selection: TextSelection.collapsed(offset: length),
);
this.values = values;
}
}
String get textWithoutReplacements {
final String char = String.fromCharCode(kObjectReplacementChar);
return text.replaceAll(RegExp(char), '');
}
String get textWithReplacements => text;
@override
TextSpan buildTextSpan({
required BuildContext context,
TextStyle? style,
required bool withComposing,
}) {
final Iterable<WidgetSpan> chipWidgets = values.map(
(T v) => WidgetSpan(child: chipBuilder(context, v)),
);
return TextSpan(
style: style,
children: <InlineSpan>[
...chipWidgets,
if (textWithoutReplacements.isNotEmpty) TextSpan(text: textWithoutReplacements),
],
);
}
}
class ToppingSuggestion extends StatelessWidget {
const ToppingSuggestion(this.topping, {super.key, this.onTap});
final String topping;
final ValueChanged<String>? onTap;
@override
Widget build(BuildContext context) {
return ListTile(
key: ObjectKey(topping),
leading: CircleAvatar(child: Text(topping[0].toUpperCase())),
title: Text(topping),
onTap: () => onTap?.call(topping),
);
}
}
class ToppingInputChip extends StatelessWidget {
const ToppingInputChip({
super.key,
required this.topping,
required this.onDeleted,
required this.onSelected,
});
final String topping;
final ValueChanged<String> onDeleted;
final ValueChanged<String> onSelected;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(right: 3),
child: InputChip(
key: ObjectKey(topping),
label: Text(topping),
avatar: CircleAvatar(child: Text(topping[0].toUpperCase())),
onDeleted: () => onDeleted(topping),
onSelected: (bool value) => onSelected(topping),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: const EdgeInsets.all(2),
),
);
}
}