Skip to content

Instantly share code, notes, and snippets.

@JRamos29
Last active June 7, 2020 15:04
Show Gist options
  • Save JRamos29/63fa6048918d63c5db1bdef79e56bdc3 to your computer and use it in GitHub Desktop.
Save JRamos29/63fa6048918d63c5db1bdef79e56bdc3 to your computer and use it in GitHub Desktop.
An adaptation of danvick/flutter_chips_input package to add chips rather than selecting from suggestions.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chips_input/src/suggestions_box_controller.dart';
typedef CustomChipsInputSuggestions<T> = FutureOr<List<T>> Function(
String query);
typedef ChipSelected<T> = void Function(T data, bool selected);
typedef ChipsBuilder<T> = Widget Function(
BuildContext context, CustomChipsInputState<T> state, T data);
typedef EntryBuilder<T> = Widget Function(
BuildContext context, CustomChipsInputState<T> state);
class CustomChipsInput<T> extends StatefulWidget {
CustomChipsInput({
Key key,
this.initialValue = const [],
this.decoration = const InputDecoration(),
this.enabled = true,
@required this.chipBuilder,
@required this.onChanged,
this.suggestionBuilder,
this.findSuggestions,
this.enableSuggestions,
this.addEntryBuilder,
this.onChipTapped,
this.maxChips,
this.minChips,
this.textStyle,
this.suggestionsBoxMaxHeight,
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,
}) : assert(maxChips == null || initialValue.length <= maxChips),
assert(enableSuggestions != null),
super(key: key);
final InputDecoration decoration;
final TextStyle textStyle;
final bool enabled;
final ValueChanged<List<T>> onChanged;
@Deprecated("Will be removed in the next major version")
final ValueChanged<T> onChipTapped;
final EntryBuilder<T> addEntryBuilder;
final ChipsBuilder<T> chipBuilder;
final ChipsBuilder<T> suggestionBuilder;
final CustomChipsInputSuggestions findSuggestions;
final bool enableSuggestions;
final List<T> initialValue;
final int maxChips;
final int minChips;
final double suggestionsBoxMaxHeight;
final TextInputType inputType;
final TextOverflow textOverflow;
final bool obscureText;
final bool autocorrect;
final String actionLabel;
final TextInputAction inputAction;
final Brightness keyboardAppearance;
// final Color cursorColor;
final TextCapitalization textCapitalization;
@override
CustomChipsInputState<T> createState() =>
CustomChipsInputState<T>(textOverflow);
}
class CustomChipsInputState<T> extends State<CustomChipsInput<T>>
implements TextInputClient {
static const kObjectReplacementChar = 0xFFFC;
Set<T> _chips = Set<T>();
List<T> _suggestions;
StreamController<List<T>> _suggestionsStreamController;
int _searchId = 0;
double _suggestionBoxHeight;
FocusNode _focusNode;
TextEditingValue _value = TextEditingValue();
TextInputConnection _connection;
SuggestionsBoxController _suggestionsBoxController;
LayerLink _layerLink = LayerLink();
Size size;
TextOverflow textOverflow;
CustomChipsInputState(TextOverflow textOverflow) {
this.textOverflow = textOverflow;
}
String get text => String.fromCharCodes(
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
);
bool get _hasInputConnection => _connection != null && _connection.attached;
@override
void initState() {
super.initState();
_chips.addAll(widget.initialValue);
_updateTextInputState();
_initSuggestionsController();
_initFocusNode();
}
_initSuggestionsController() {
this._suggestionsBoxController = SuggestionsBoxController(context);
this._suggestionsStreamController = StreamController<List<T>>.broadcast();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_initOverlayEntry();
});
}
_initFocusNode() {
if (widget.enabled &&
(widget.maxChips == null || _chips.length < widget.maxChips)) {
// this._suggestionsBoxController.close();
if (_focusNode == null ||
_focusNode.runtimeType == AlwaysDisabledFocusNode) {
this._focusNode = FocusNode();
this._focusNode.addListener(_onFocusChanged);
}
this._focusNode.requestFocus();
} else {
if (_focusNode.runtimeType != AlwaysDisabledFocusNode)
this._focusNode = AlwaysDisabledFocusNode();
}
}
void _onFocusChanged() {
if (_focusNode.hasFocus) {
_openInputConnection();
this._suggestionsBoxController.open();
} else {
_closeInputConnectionIfNeeded(true);
this._suggestionsBoxController.close();
}
setState(() {
/*rebuild so that _TextCursor is hidden.*/
});
}
_recalculateSuggestionsBoxHeight() {
setState(() {
_suggestionBoxHeight = MediaQuery.of(context).size.height -
MediaQuery.of(context).viewInsets.bottom;
});
}
RenderBox _getRenderBox() {
return context.findRenderObject();
}
void _initOverlayEntry() {
// this._suggestionsBoxController.close();
this._suggestionsBoxController.overlayEntry = OverlayEntry(
builder: (context) {
RenderBox renderBox = _getRenderBox();
var size = renderBox.size;
var offset = renderBox.localToGlobal(Offset.zero);
var top = offset.dy + size.height + 5.0;
return Positioned(
left: offset.dx,
top: top,
width: size.width,
child: StreamBuilder(
stream: _suggestionsStreamController.stream,
builder: (
BuildContext context,
AsyncSnapshot<List<dynamic>> snapshot,
) {
if (snapshot.hasData && snapshot.data?.length != 0) {
return CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(0.0, size.height + 5.0),
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: widget.suggestionsBoxMaxHeight ??
(_suggestionBoxHeight - top > 0
? _suggestionBoxHeight - top
: 400),
),
child: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
return widget.suggestionBuilder(
context,
this,
_suggestions[index],
);
},
),
),
),
);
}
return Container();
},
),
);
},
);
}
@override
void dispose() {
_focusNode?.dispose();
_closeInputConnectionIfNeeded(false);
_suggestionsStreamController.close();
_suggestionsBoxController.close();
super.dispose();
}
void requestKeyboard() {
if (_focusNode.hasFocus) {
_openInputConnection();
} else {
FocusScope.of(context).requestFocus(_focusNode);
}
_recalculateSuggestionsBoxHeight();
}
void selectSuggestion(T data) {
setState(() {
if (widget.maxChips == null || widget.maxChips > _chips.length) {
_chips.add(data);
_initFocusNode();
_updateTextInputState();
_suggestions = null;
_suggestionsStreamController.add(_suggestions);
if (widget.maxChips == _chips.length) _suggestionsBoxController.close();
} else
_suggestionsBoxController.close();
});
widget.onChanged(_chips.toList(growable: false));
}
void addChip(T data) {
if (widget.enabled) {
setState(() {
_chips.add(data);
_updateTextInputState();
});
_initFocusNode();
widget.onChanged(_chips.toList(growable: false));
}
}
void deleteChip(T data) {
if (widget.enabled) {
setState(() {
_chips.remove(data);
_updateTextInputState();
});
_initFocusNode();
widget.onChanged(_chips.toList(growable: false));
}
}
void _openInputConnection() {
if (!_hasInputConnection) {
_connection = TextInput.attach(
this,
TextInputConfiguration(
inputType: widget.inputType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
actionLabel: widget.actionLabel,
inputAction: widget.inputAction,
keyboardAppearance: widget.keyboardAppearance,
textCapitalization: widget.textCapitalization,
));
_connection.setEditingState(_value);
}
_connection.show();
_recalculateSuggestionsBoxHeight();
Future.delayed(Duration(milliseconds: 100), () {
WidgetsBinding.instance.addPostFrameCallback((_) async {
RenderBox renderBox = context.findRenderObject();
Scrollable.of(context)?.position?.ensureVisible(renderBox);
});
});
}
void _closeInputConnectionIfNeeded(bool recalculate) {
if (_hasInputConnection) {
_connection.close();
_connection = null;
}
if (recalculate) _recalculateSuggestionsBoxHeight();
}
@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>[
// Flexible(
// flex: 1,
// child: Text(
// text,
// maxLines: 1,
// overflow: this.textOverflow,
// style: widget.textStyle ??
// theme.textTheme.subhead.copyWith(height: 1.5),
// ),
// ),
Container(child: widget.addEntryBuilder(context, this)),
Flexible(
flex: 0,
child: _TextCaret(
resumed: _focusNode.hasFocus,
),
),
],
),
),
);
return NotificationListener<SizeChangedLayoutNotification>(
onNotification: (SizeChangedLayoutNotification val) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_suggestionsBoxController.overlayEntry.markNeedsBuild();
});
return true;
},
child: SizeChangedLayoutNotifier(
child: CompositedTransformTarget(
link: this._layerLink,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: requestKeyboard,
child: InputDecorator(
decoration: widget.decoration,
isFocused: _focusNode.hasFocus,
isEmpty: _value.text.length == 0 && _chips.length == 0,
child: Wrap(
children: chipsChildren,
spacing: 4.0,
runSpacing: 4.0,
),
),
),
),
),
);
}
@override
void updateEditingValue(TextEditingValue value) {
final oldCount = _countReplacements(_value);
final newCount = _countReplacements(value);
setState(() {
if (newCount < oldCount) {
_chips = Set.from(_chips.take(newCount));
widget.onChanged(_chips.toList(growable: false));
}
_value = value;
});
if (widget.enableSuggestions) _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),
);
if (_connection == null) {
_connection = TextInput.attach(
this,
TextInputConfiguration(
inputType: widget.inputType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
actionLabel: widget.actionLabel,
inputAction: widget.inputAction,
keyboardAppearance: widget.keyboardAppearance,
textCapitalization: widget.textCapitalization,
));
}
if (_connection.attached) _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));
}
_suggestionsStreamController.add(_suggestions);
}
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
print(point);
}
@override
void connectionClosed() {
print('TextInputClient.connectionCLosed()');
}
@override
TextEditingValue get currentTextEditingValue => _value;
@override
void showAutocorrectionPromptRect(int start, int end) {}
}
class AlwaysDisabledFocusNode extends FocusNode {
@override
bool get hasFocus => 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.cursorColor,
),
),
);
}
}
import 'package:app_tests/chips/custom_chips_input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chips_input/flutter_chips_input.dart';
import 'package:flutter/foundation.dart';
// void main() => runApp(MyApp());
class InputChipObjectDemo extends StatefulWidget {
InputChipObjectDemo({Key key}) : super(key: key);
@override
_InputChipObjectDemoState createState() => _InputChipObjectDemoState();
}
class _InputChipObjectDemoState extends State<InputChipObjectDemo> {
@override
Widget build(BuildContext context) {
List mockResults = <AppProfile>[
AppProfile('John Doe', 'jdoe@flutter.io',
'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://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Brian', 'brian@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('Nelly', 'nelly@flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Marie', 'marie@flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Charlie', 'charlie@flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Diana', 'diana@flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Ernie', 'ernie@flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
AppProfile('Gina', 'fred@flutter.io',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'),
];
_buildQueryList(String query) {
print("Query: '$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: true)
..sort((a, b) => a.name
.toLowerCase()
.indexOf(lowercaseQuery)
.compareTo(b.name.toLowerCase().indexOf(lowercaseQuery)));
}
// return <AppProfile>[];
return mockResults;
}
AppProfile initialValue = AppProfile('John Doe', 'jdoe@flutter.io',
'https://d2gg9evh47fn9z.cloudfront.net/800px_COLOURBOX4057996.jpg');
AppProfile newProfile = AppProfile('', '', '');
_addNewProfile(String name) {
setState(() {
newProfile = AppProfile(name, 'email@email',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png');
});
print(newProfile.name);
}
return Scaffold(
appBar: AppBar(
title: Text('Flutter Chips Input Example'),
),
resizeToAvoidBottomInset: false,
body: Padding(
padding: EdgeInsets.all(20),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
alignment: Alignment.topRight,
height: 30,
width: double.maxFinite,
),
CustomChipsInput(
initialValue: [
initialValue,
],
keyboardAppearance: Brightness.dark,
textCapitalization: TextCapitalization.words,
enabled: true,
maxChips: 5,
textStyle:
TextStyle(height: 1.5, fontFamily: "Roboto", fontSize: 16),
decoration: InputDecoration(
// prefixIcon: Icon(Icons.search),
// hintText: formControl.hint,
labelText: "Select People",
// enabled: false,
// errorText: field.errorText,
),
onChanged: (data) {
print(data);
},
addEntryBuilder: (context, state) {
AppProfile newProfile = AppProfile(state.text, 'email',
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png');
if (state.text != null && state.text.isNotEmpty) {
return InputChip(
key: ObjectKey(newProfile),
label: Text(state.text),
avatar: CircleAvatar(
backgroundImage: NetworkImage(newProfile.imageUrl),
),
onPressed: () => state.addChip(newProfile),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
} else {
return Text('');
}
},
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,
);
},
enableSuggestions: false,
// findSuggestions: _buildQueryList,
// 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),
// );
// },
),
Container(
height: 600,
width: double.maxFinite,
),
],
),
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class AppProfile {
String name;
String email;
String imageUrl;
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;
setName(value) => name = value;
@override
String toString() {
return name;
}
}
import 'package:app_tests/chips/custom_chips_input.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chips_input/flutter_chips_input.dart';
import 'package:flutter/foundation.dart';
// void main() => runApp(MyApp());
class InputChipStringDemo extends StatefulWidget {
InputChipStringDemo({Key key}) : super(key: key);
@override
_InputChipStringDemoState createState() => _InputChipStringDemoState();
}
class _InputChipStringDemoState extends State<InputChipStringDemo> {
@override
Widget build(BuildContext context) {
List selectedList = <String>[];
_buildQueryList(String query) {
print("Query: '$query'");
if (query.length != 0) {
var lowercaseQuery = query.toLowerCase();
return selectedList.where((profile) {
return profile.name.toLowerCase().contains(query.toLowerCase()) ||
profile.email.toLowerCase().contains(query.toLowerCase());
}).toList(growable: true)
..sort((a, b) => a.name
.toLowerCase()
.indexOf(lowercaseQuery)
.compareTo(b.name.toLowerCase().indexOf(lowercaseQuery)));
}
// return <AppProfile>[];
return selectedList;
}
String initialValue = 'Example';
return Scaffold(
appBar: AppBar(
title: Text('Flutter Chips Input Example'),
),
resizeToAvoidBottomInset: false,
body: Padding(
padding: EdgeInsets.all(20),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
Container(
alignment: Alignment.topRight,
height: 30,
width: double.maxFinite,
),
CustomChipsInput(
initialValue: [
initialValue,
],
keyboardAppearance: Brightness.dark,
textCapitalization: TextCapitalization.words,
enabled: true,
maxChips: 10,
textStyle:
TextStyle(height: 1.5, fontFamily: "Roboto", fontSize: 16),
decoration: InputDecoration(
// prefixIcon: Icon(Icons.search),
// hintText: formControl.hint,
labelText: "Select People",
// enabled: false,
// errorText: field.errorText,
),
onChanged: (data) {
selectedList = data;
print(data);
},
addEntryBuilder: (context, state) {
if (state.text != null && state.text.isNotEmpty) {
String newString = state.text;
return InputChip(
key: ObjectKey(newString),
label: Text(newString),
onPressed: () => state.addChip(newString),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
} else {
return Text('');
}
},
chipBuilder: (context, state, word) {
return InputChip(
key: ObjectKey(word),
label: Text(word),
backgroundColor: Colors.green[200],
onDeleted: () => state.deleteChip(word),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
},
enableSuggestions: false,
// findSuggestions: _buildQueryList,
// 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),
// );
// },
),
Container(
height: 600,
width: double.maxFinite,
),
],
),
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class AppProfile {
String name;
String email;
String imageUrl;
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;
setName(value) => name = value;
@override
String toString() {
return name;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment