flutter_flow_drop_down.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import 'package:dropdown_button2/dropdown_button2.dart';
  2. import 'package:flutter/foundation.dart';
  3. import 'form_field_controller.dart';
  4. import 'package:flutter/material.dart';
  5. class FlutterFlowDropDown<T> extends StatefulWidget {
  6. const FlutterFlowDropDown({
  7. super.key,
  8. this.controller,
  9. this.multiSelectController,
  10. this.hintText,
  11. this.searchHintText,
  12. required this.options,
  13. this.optionLabels,
  14. this.onChanged,
  15. this.onMultiSelectChanged,
  16. this.icon,
  17. this.width,
  18. this.height,
  19. this.maxHeight,
  20. this.fillColor,
  21. this.searchHintTextStyle,
  22. this.searchTextStyle,
  23. this.searchCursorColor,
  24. required this.textStyle,
  25. required this.elevation,
  26. required this.borderWidth,
  27. required this.borderRadius,
  28. required this.borderColor,
  29. this.focusBorderColor,
  30. required this.margin,
  31. this.hidesUnderline = false,
  32. this.disabled = false,
  33. this.isOverButton = false,
  34. this.menuOffset,
  35. this.isSearchable = false,
  36. this.isMultiSelect = false,
  37. this.labelText,
  38. this.labelTextStyle,
  39. this.optionsHasValueKeys = false,
  40. }) : assert(
  41. isMultiSelect
  42. ? (controller == null &&
  43. onChanged == null &&
  44. multiSelectController != null &&
  45. onMultiSelectChanged != null)
  46. : (controller != null &&
  47. onChanged != null &&
  48. multiSelectController == null &&
  49. onMultiSelectChanged == null),
  50. );
  51. final FormFieldController<T?>? controller;
  52. final FormFieldController<List<T>?>? multiSelectController;
  53. final String? hintText;
  54. final String? searchHintText;
  55. final List<T> options;
  56. final List<String>? optionLabels;
  57. final Function(T?)? onChanged;
  58. final Function(List<T>?)? onMultiSelectChanged;
  59. final Widget? icon;
  60. final double? width;
  61. final double? height;
  62. final double? maxHeight;
  63. final Color? fillColor;
  64. final TextStyle? searchHintTextStyle;
  65. final TextStyle? searchTextStyle;
  66. final Color? searchCursorColor;
  67. final TextStyle textStyle;
  68. final double elevation;
  69. final double borderWidth;
  70. final double borderRadius;
  71. final Color borderColor;
  72. final Color? focusBorderColor;
  73. final EdgeInsetsGeometry margin;
  74. final bool hidesUnderline;
  75. final bool disabled;
  76. final bool isOverButton;
  77. final Offset? menuOffset;
  78. final bool isSearchable;
  79. final bool isMultiSelect;
  80. final String? labelText;
  81. final TextStyle? labelTextStyle;
  82. final bool optionsHasValueKeys;
  83. @override
  84. State<FlutterFlowDropDown<T>> createState() => _FlutterFlowDropDownState<T>();
  85. }
  86. class _FlutterFlowDropDownState<T> extends State<FlutterFlowDropDown<T>> {
  87. bool get isMultiSelect => widget.isMultiSelect;
  88. FormFieldController<T?> get controller => widget.controller!;
  89. FormFieldController<List<T>?> get multiSelectController =>
  90. widget.multiSelectController!;
  91. T? get currentValue {
  92. final value = isMultiSelect
  93. ? multiSelectController.value?.firstOrNull
  94. : controller.value;
  95. return widget.options.contains(value) ? value : null;
  96. }
  97. Set<T> get currentValues {
  98. if (!isMultiSelect || multiSelectController.value == null) {
  99. return {};
  100. }
  101. return widget.options
  102. .toSet()
  103. .intersection(multiSelectController.value!.toSet());
  104. }
  105. Map<T, String> get optionLabels => Map.fromEntries(
  106. widget.options.asMap().entries.map(
  107. (option) => MapEntry(
  108. option.value,
  109. widget.optionLabels == null ||
  110. widget.optionLabels!.length < option.key + 1
  111. ? option.value.toString()
  112. : widget.optionLabels![option.key],
  113. ),
  114. ),
  115. );
  116. EdgeInsetsGeometry get horizontalMargin => widget.margin.clamp(
  117. EdgeInsetsDirectional.zero,
  118. const EdgeInsetsDirectional.symmetric(horizontal: double.infinity),
  119. );
  120. late void Function() _listener;
  121. final TextEditingController _textEditingController = TextEditingController();
  122. late final FocusNode _focusNode = FocusNode()
  123. ..addListener(() => setState(() => _isFocused = _focusNode.hasFocus));
  124. bool _isFocused = false;
  125. @override
  126. void initState() {
  127. super.initState();
  128. if (isMultiSelect) {
  129. _listener =
  130. () => widget.onMultiSelectChanged!(multiSelectController.value);
  131. multiSelectController.addListener(_listener);
  132. } else {
  133. _listener = () => widget.onChanged!(controller.value);
  134. controller.addListener(_listener);
  135. }
  136. }
  137. @override
  138. void dispose() {
  139. if (isMultiSelect) {
  140. multiSelectController.removeListener(_listener);
  141. } else {
  142. controller.removeListener(_listener);
  143. }
  144. _focusNode.dispose();
  145. super.dispose();
  146. }
  147. @override
  148. Widget build(BuildContext context) {
  149. final dropdownWidget = _buildDropdownWidget();
  150. final effectiveBorderColor = _isFocused && widget.focusBorderColor != null
  151. ? widget.focusBorderColor!
  152. : widget.borderColor;
  153. return SizedBox(
  154. width: widget.width,
  155. height: widget.height,
  156. child: DecoratedBox(
  157. decoration: BoxDecoration(
  158. borderRadius: BorderRadius.circular(widget.borderRadius),
  159. border: Border.all(
  160. color: effectiveBorderColor,
  161. width: widget.borderWidth,
  162. ),
  163. color: widget.fillColor,
  164. ),
  165. child: Padding(
  166. padding: _useDropdown2() ? EdgeInsets.zero : widget.margin,
  167. child: widget.hidesUnderline
  168. ? DropdownButtonHideUnderline(child: dropdownWidget)
  169. : dropdownWidget,
  170. ),
  171. ),
  172. );
  173. }
  174. bool _useDropdown2() =>
  175. widget.isMultiSelect ||
  176. widget.isSearchable ||
  177. !widget.isOverButton ||
  178. widget.maxHeight != null;
  179. Widget _buildDropdownWidget() =>
  180. _useDropdown2() ? _buildDropdown() : _buildLegacyDropdown();
  181. Widget _buildLegacyDropdown() {
  182. return DropdownButtonFormField<T>(
  183. value: currentValue,
  184. hint: _createHintText(),
  185. items: _createMenuItems(),
  186. elevation: widget.elevation.toInt(),
  187. onChanged: widget.disabled ? null : (value) => controller.value = value,
  188. icon: widget.icon,
  189. isExpanded: true,
  190. dropdownColor: widget.fillColor,
  191. focusColor: Colors.transparent,
  192. decoration: InputDecoration(
  193. labelText: widget.labelText == null || widget.labelText!.isEmpty
  194. ? null
  195. : widget.labelText,
  196. labelStyle: widget.labelTextStyle,
  197. border: widget.hidesUnderline
  198. ? InputBorder.none
  199. : const UnderlineInputBorder(),
  200. ),
  201. );
  202. }
  203. Text? _createHintText() => widget.hintText != null
  204. ? Text(widget.hintText!, style: widget.textStyle)
  205. : null;
  206. ValueKey _getItemKey(T option) {
  207. final widgetKey = (widget.key as ValueKey).value;
  208. return ValueKey('$widgetKey ${widget.options.indexOf(option)}');
  209. }
  210. List<DropdownMenuItem<T>> _createMenuItems() => widget.options
  211. .map(
  212. (option) => DropdownMenuItem<T>(
  213. key: widget.optionsHasValueKeys ? _getItemKey(option) : null,
  214. value: option,
  215. child: Padding(
  216. padding: _useDropdown2() ? horizontalMargin : EdgeInsets.zero,
  217. child: Text(optionLabels[option] ?? '', style: widget.textStyle),
  218. )),
  219. )
  220. .toList();
  221. List<DropdownMenuItem<T>> _createMultiselectMenuItems() => widget.options
  222. .map(
  223. (item) => DropdownMenuItem<T>(
  224. key: widget.optionsHasValueKeys ? _getItemKey(item) : null,
  225. value: item,
  226. // Disable default onTap to avoid closing menu when selecting an item
  227. enabled: false,
  228. child: StatefulBuilder(
  229. builder: (context, menuSetState) {
  230. final isSelected =
  231. multiSelectController.value?.contains(item) ?? false;
  232. return InkWell(
  233. onTap: () {
  234. multiSelectController.value ??= [];
  235. isSelected
  236. ? multiSelectController.value!.remove(item)
  237. : multiSelectController.value!.add(item);
  238. multiSelectController.update();
  239. // This rebuilds the StatefulWidget to update the button's text.
  240. setState(() {});
  241. // This rebuilds the dropdownMenu Widget to update the check mark.
  242. menuSetState(() {});
  243. },
  244. child: Container(
  245. height: double.infinity,
  246. padding: horizontalMargin,
  247. child: Row(
  248. children: [
  249. if (isSelected)
  250. const Icon(Icons.check_box_outlined)
  251. else
  252. const Icon(Icons.check_box_outline_blank),
  253. const SizedBox(width: 16),
  254. Expanded(
  255. child: Text(
  256. optionLabels[item]!,
  257. style: widget.textStyle,
  258. ),
  259. ),
  260. ],
  261. ),
  262. ));
  263. },
  264. ),
  265. ),
  266. )
  267. .toList();
  268. Widget _buildDropdown() {
  269. final overlayColor = WidgetStateProperty.resolveWith<Color?>((states) =>
  270. states.contains(WidgetState.focused) ? Colors.transparent : null);
  271. final menuItemOverlayColor = kIsWeb ? null : overlayColor;
  272. final iconStyleData = widget.icon != null
  273. ? IconStyleData(icon: widget.icon!)
  274. : const IconStyleData();
  275. return DropdownButton2<T>(
  276. value: currentValue,
  277. hint: _createHintText(),
  278. items: isMultiSelect ? _createMultiselectMenuItems() : _createMenuItems(),
  279. focusNode: _focusNode,
  280. iconStyleData: iconStyleData,
  281. buttonStyleData: ButtonStyleData(
  282. elevation: widget.elevation.toInt(),
  283. overlayColor: overlayColor,
  284. padding: widget.margin,
  285. ),
  286. menuItemStyleData: MenuItemStyleData(
  287. padding: EdgeInsets.zero,
  288. overlayColor: menuItemOverlayColor,
  289. ),
  290. dropdownStyleData: DropdownStyleData(
  291. elevation: widget.elevation.toInt(),
  292. decoration: BoxDecoration(
  293. borderRadius: BorderRadius.circular(4.0),
  294. color: widget.fillColor,
  295. ),
  296. isOverButton: widget.isOverButton,
  297. offset: widget.menuOffset ?? Offset.zero,
  298. maxHeight: widget.maxHeight,
  299. padding: EdgeInsets.zero,
  300. ),
  301. onChanged: widget.disabled
  302. ? null
  303. : (isMultiSelect ? (_) {} : (val) => widget.controller!.value = val),
  304. isExpanded: true,
  305. selectedItemBuilder: (context) => widget.options
  306. .map(
  307. (item) => Align(
  308. alignment: AlignmentDirectional.centerStart,
  309. child: Text(
  310. isMultiSelect
  311. ? currentValues
  312. .where((v) => optionLabels.containsKey(v))
  313. .map((v) => optionLabels[v])
  314. .join(', ')
  315. : optionLabels[item]!,
  316. style: widget.textStyle,
  317. maxLines: 1,
  318. )),
  319. )
  320. .toList(),
  321. dropdownSearchData: widget.isSearchable
  322. ? DropdownSearchData<T>(
  323. searchController: _textEditingController,
  324. searchInnerWidgetHeight: 50,
  325. searchInnerWidget: Container(
  326. height: 50,
  327. padding: const EdgeInsets.only(
  328. top: 8,
  329. bottom: 4,
  330. right: 8,
  331. left: 8,
  332. ),
  333. child: TextFormField(
  334. expands: true,
  335. maxLines: null,
  336. controller: _textEditingController,
  337. cursorColor: widget.searchCursorColor,
  338. style: widget.searchTextStyle,
  339. decoration: InputDecoration(
  340. isDense: true,
  341. contentPadding: const EdgeInsets.symmetric(
  342. horizontal: 10,
  343. vertical: 8,
  344. ),
  345. hintText: widget.searchHintText,
  346. hintStyle: widget.searchHintTextStyle,
  347. border: OutlineInputBorder(
  348. borderRadius: BorderRadius.circular(8),
  349. ),
  350. ),
  351. ),
  352. ),
  353. searchMatchFn: (item, searchValue) {
  354. return (optionLabels[item.value] ?? '')
  355. .toLowerCase()
  356. .contains(searchValue.toLowerCase());
  357. },
  358. )
  359. : null,
  360. // This is to clear the search value when you close the menu
  361. onMenuStateChange: (isOpen) {
  362. if (!isOpen) {
  363. if (widget.isSearchable) {
  364. _textEditingController.clear();
  365. }
  366. _focusNode.requestFocus();
  367. }
  368. },
  369. );
  370. }
  371. }