Skip to content

Commit 8901869

Browse files
committed
Started working on ChipsInput
1 parent 9bfd906 commit 8901869

File tree

6 files changed

+561
-134
lines changed

6 files changed

+561
-134
lines changed

.idea/workspace.xml

Lines changed: 233 additions & 132 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>BuildSystemType</key>
6+
<string>Original</string>
7+
</dict>
8+
</plist>

flutter_form_builder.iml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<module type="JAVA_MODULE" version="4">
3+
<component name="FacetManager">
4+
<facet type="android" name="Android">
5+
<configuration>
6+
<option name="ALLOW_USER_CONFIGURATION" value="false" />
7+
</configuration>
8+
</facet>
9+
</component>
310
<component name="NewModuleRootManager" inherit-compiler-output="true">
411
<exclude-output />
512
<content url="file://$MODULE_DIR$">
613
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
714
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
15+
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
816
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
917
<excludeFolder url="file://$MODULE_DIR$/.idea" />
1018
<excludeFolder url="file://$MODULE_DIR$/.pub" />
1119
<excludeFolder url="file://$MODULE_DIR$/build" />
1220
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
1321
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
1422
<excludeFolder url="file://$MODULE_DIR$/example/build" />
23+
<excludeFolder url="file://$MODULE_DIR$/example/ios/Flutter/flutter_assets/packages" />
1524
</content>
1625
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
1726
<orderEntry type="sourceFolder" forTests="false" />

lib/src/chips_input.dart

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import 'dart:async';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter/services.dart';
4+
5+
typedef ChipsInputSuggestions<T> = Future<List<T>> Function(String query);
6+
typedef ChipSelected<T> = void Function(T data, bool selected);
7+
typedef ChipsBuilder<T> = Widget Function(BuildContext context, ChipsInputState<T> state, T data);
8+
9+
class ChipsInput<T> extends StatefulWidget {
10+
const ChipsInput({
11+
Key key,
12+
this.decoration = const InputDecoration(),
13+
@required this.chipBuilder,
14+
@required this.suggestionBuilder,
15+
@required this.findSuggestions,
16+
@required this.onChanged,
17+
this.onChipTapped,
18+
}) : super(key: key);
19+
20+
final InputDecoration decoration;
21+
final ChipsInputSuggestions findSuggestions;
22+
final ValueChanged<List<T>> onChanged;
23+
final ValueChanged<T> onChipTapped;
24+
final ChipsBuilder<T> chipBuilder;
25+
final ChipsBuilder<T> suggestionBuilder;
26+
27+
@override
28+
ChipsInputState<T> createState() => ChipsInputState<T>();
29+
}
30+
31+
class ChipsInputState<T> extends State<ChipsInput<T>> implements TextInputClient {
32+
static const kObjectReplacementChar = 0xFFFC;
33+
34+
Set<T> _chips = Set<T>();
35+
List<T> _suggestions;
36+
int _searchId = 0;
37+
38+
FocusNode _focusNode;
39+
TextEditingValue _value = TextEditingValue();
40+
TextInputConnection _connection;
41+
42+
String get text => String.fromCharCodes(
43+
_value.text.codeUnits.where((ch) => ch != kObjectReplacementChar),
44+
);
45+
46+
bool get _hasInputConnection => _connection != null && _connection.attached;
47+
48+
void requestKeyboard() {
49+
if (_focusNode.hasFocus) {
50+
_openInputConnection();
51+
} else {
52+
FocusScope.of(context).requestFocus(_focusNode);
53+
}
54+
}
55+
56+
void selectSuggestion(T data) {
57+
setState(() {
58+
_chips.add(data);
59+
_updateTextInputState();
60+
_suggestions = null;
61+
});
62+
widget.onChanged(_chips.toList(growable: false));
63+
}
64+
65+
void deleteChip(T data) {
66+
setState(() {
67+
_chips.remove(data);
68+
_updateTextInputState();
69+
});
70+
widget.onChanged(_chips.toList(growable: false));
71+
}
72+
73+
@override
74+
void initState() {
75+
super.initState();
76+
_focusNode = FocusNode();
77+
_focusNode.addListener(_onFocusChanged);
78+
}
79+
80+
void _onFocusChanged() {
81+
if (_focusNode.hasFocus) {
82+
_openInputConnection();
83+
} else {
84+
_closeInputConnectionIfNeeded();
85+
}
86+
setState(() {
87+
// rebuild so that _TextCursor is hidden.
88+
});
89+
}
90+
91+
@override
92+
void dispose() {
93+
_focusNode?.dispose();
94+
_closeInputConnectionIfNeeded();
95+
super.dispose();
96+
}
97+
98+
void _openInputConnection() {
99+
if (!_hasInputConnection) {
100+
_connection = TextInput.attach(this, TextInputConfiguration());
101+
_connection.setEditingState(_value);
102+
}
103+
_connection.show();
104+
}
105+
106+
void _closeInputConnectionIfNeeded() {
107+
if (_hasInputConnection) {
108+
_connection.close();
109+
_connection = null;
110+
}
111+
}
112+
113+
@override
114+
Widget build(BuildContext context) {
115+
var chipsChildren = _chips
116+
.map<Widget>(
117+
(data) => widget.chipBuilder(context, this, data),
118+
)
119+
.toList();
120+
121+
final theme = Theme.of(context);
122+
123+
chipsChildren.add(
124+
Container(
125+
height: 32.0,
126+
child: Row(
127+
mainAxisSize: MainAxisSize.min,
128+
crossAxisAlignment: CrossAxisAlignment.stretch,
129+
children: <Widget>[
130+
Text(
131+
text,
132+
style: theme.textTheme.subhead.copyWith(
133+
height: 1.5,
134+
),
135+
),
136+
_TextCaret(
137+
resumed: _focusNode.hasFocus,
138+
),
139+
],
140+
),
141+
),
142+
);
143+
144+
return Column(
145+
crossAxisAlignment: CrossAxisAlignment.stretch,
146+
//mainAxisSize: MainAxisSize.min,
147+
children: <Widget>[
148+
GestureDetector(
149+
behavior: HitTestBehavior.opaque,
150+
onTap: requestKeyboard,
151+
child: InputDecorator(
152+
decoration: widget.decoration,
153+
isFocused: _focusNode.hasFocus,
154+
isEmpty: _value.text.length == 0,
155+
child: Wrap(
156+
children: chipsChildren,
157+
spacing: 4.0,
158+
runSpacing: 4.0,
159+
),
160+
),
161+
),
162+
Expanded(
163+
child: ListView.builder(
164+
itemCount: _suggestions?.length ?? 0,
165+
itemBuilder: (BuildContext context, int index) {
166+
return widget.suggestionBuilder(context, this, _suggestions[index]);
167+
},
168+
),
169+
),
170+
],
171+
);
172+
}
173+
174+
@override
175+
void updateEditingValue(TextEditingValue value) {
176+
final oldCount = _countReplacements(_value);
177+
final newCount = _countReplacements(value);
178+
setState(() {
179+
if (newCount < oldCount) {
180+
_chips = Set.from(_chips.take(newCount));
181+
}
182+
_value = value;
183+
});
184+
_onSearchChanged(text);
185+
}
186+
187+
int _countReplacements(TextEditingValue value) {
188+
return value.text.codeUnits.where((ch) => ch == kObjectReplacementChar).length;
189+
}
190+
191+
@override
192+
void performAction(TextInputAction action) {
193+
_focusNode.unfocus();
194+
}
195+
196+
void _updateTextInputState() {
197+
final text = String.fromCharCodes(_chips.map((_) => kObjectReplacementChar));
198+
_value = TextEditingValue(
199+
text: text,
200+
selection: TextSelection.collapsed(offset: text.length),
201+
composing: TextRange(start: 0, end: text.length),
202+
);
203+
_connection.setEditingState(_value);
204+
}
205+
206+
void _onSearchChanged(String value) async {
207+
final localId = ++_searchId;
208+
final results = await widget.findSuggestions(value);
209+
if (_searchId == localId && mounted) {
210+
setState(() => _suggestions = results.where((profile) => !_chips.contains(profile)).toList(growable: false));
211+
}
212+
}
213+
}
214+
215+
class _TextCaret extends StatefulWidget {
216+
const _TextCaret({
217+
Key key,
218+
this.duration = const Duration(milliseconds: 500),
219+
this.resumed = false,
220+
}) : super(key: key);
221+
222+
final Duration duration;
223+
final bool resumed;
224+
225+
@override
226+
_TextCursorState createState() => _TextCursorState();
227+
}
228+
229+
class _TextCursorState extends State<_TextCaret> with SingleTickerProviderStateMixin {
230+
bool _displayed = false;
231+
Timer _timer;
232+
233+
@override
234+
void initState() {
235+
super.initState();
236+
_timer = Timer.periodic(widget.duration, _onTimer);
237+
}
238+
239+
void _onTimer(Timer timer) {
240+
setState(() => _displayed = !_displayed);
241+
}
242+
243+
@override
244+
void dispose() {
245+
_timer.cancel();
246+
super.dispose();
247+
}
248+
249+
@override
250+
Widget build(BuildContext context) {
251+
final theme = Theme.of(context);
252+
return FractionallySizedBox(
253+
heightFactor: 0.7,
254+
child: Opacity(
255+
opacity: _displayed && widget.resumed ? 1.0 : 0.0,
256+
child: Container(
257+
width: 2.0,
258+
color: theme.primaryColor,
259+
),
260+
),
261+
);
262+
}
263+
}

lib/src/form_builder.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import 'package:sy_flutter_widgets/sy_flutter_widgets.dart';
55

66
import './form_builder_input.dart';
77
import './form_builder_type_ahead.dart';
8+
import './chips_input.dart';
89

10+
//TODO: Refactor this spaghetti code
911
class FormBuilder extends StatefulWidget {
1012
final BuildContext context;
1113
final VoidCallback onChanged;
@@ -698,6 +700,36 @@ class _FormBuilderState extends State<FormBuilder> {
698700
);
699701
}));
700702
break;
703+
/*case FormBuilderInput.TYPE_CHIPS_INPUT:
704+
formControlsList.add(ChipsInput(
705+
decoration: InputDecoration(prefixIcon: Icon(Icons.search), hintText: 'Profile search', labelText: ),
706+
// findSuggestions: _findSuggestions,
707+
// onChanged: _onChanged,
708+
chipBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
709+
return InputChip(
710+
key: ObjectKey(profile),
711+
label: Text(profile.name),
712+
avatar: CircleAvatar(
713+
backgroundImage: NetworkImage(profile.imageUrl),
714+
),
715+
onDeleted: () => state.deleteChip(profile),
716+
onSelected: (_) => _onChipTapped(profile),
717+
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
718+
);
719+
},
720+
suggestionBuilder: (BuildContext context, ChipsInputState<AppProfile> state, AppProfile profile) {
721+
return ListTile(
722+
key: ObjectKey(profile),
723+
leading: CircleAvatar(
724+
backgroundImage: NetworkImage(profile.imageUrl),
725+
),
726+
title: Text(profile.name),
727+
subtitle: Text(profile.email),
728+
onTap: () => state.selectSuggestion(profile),
729+
);
730+
},
731+
));
732+
break;*/
701733
}
702734
}
703735

lib/src/form_builder_input.dart

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import 'package:flutter_typeahead/flutter_typeahead.dart';
33

44
import './form_builder_input_option.dart';
55

6-
//TODO: Consider adding code_input - https://pub.dartlang.org/packages/code_input#-readme-tab-
7-
//TODO: Consider adding Input Chips - https://stackoverflow.com/questions/52155254/how-can-i-build-a-chip-input-field-in-flutter
6+
//TODO: Consider adding RangeSlider - https://pub.dartlang.org/packages/flutter_range_slider
7+
//TODO: Consider adding ColorPicker - https://pub.dartlang.org/packages/flutter_colorpicker
8+
//TODO: Consider adding masked_text - https://pub.dartlang.org/packages/code_input#-readme-tab- (Not Important)
9+
//TODO: Consider adding code_input - https://pub.dartlang.org/packages/flutter_masked_text#-changelog-tab- (Not Important)
810
//TODO: Add autovalidate attribute type
911
class FormBuilderInput {
1012
static const String TYPE_TEXT = "Text";
@@ -26,6 +28,7 @@ class FormBuilderInput {
2628
static const String TYPE_STEPPER = "Stepper";
2729
static const String TYPE_RATE = "Rate";
2830
static const String TYPE_SEGMENTED_CONTROL = "SegmentedControl";
31+
static const String TYPE_CHIPS_INPUT = "ChipsInput";
2932

3033
String label;
3134
String attribute;
@@ -229,6 +232,17 @@ class FormBuilderInput {
229232
type = FormBuilderInput.TYPE_TIME_PICKER;
230233
}
231234

235+
FormBuilderInput.chipsInput({
236+
@required this.label,
237+
@required this.attribute,
238+
this.hint,
239+
this.value,
240+
this.require = false,
241+
this.validator,
242+
}) {
243+
type = FormBuilderInput.TYPE_CHIPS_INPUT;
244+
}
245+
232246
hasHint() {
233247
return hint != null;
234248
}

0 commit comments

Comments
 (0)