import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; /// A dropdown widget that allows the user to select an option from a list of options. class THFlutterFlowDropDown extends StatefulWidget { const THFlutterFlowDropDown({ super.key, this.controller, this.multiSelectController, this.hintText, this.searchHintText, required this.options, this.optionLabels, this.onChanged, this.onMenuStateChange, this.onMultiSelectChanged, this.icon, this.width, this.height, this.maxHeight, this.fillColor, this.searchHintTextStyle, this.hintTextStyle, this.searchTextStyle, this.searchCursorColor, required this.textStyle, required this.elevation, required this.borderWidth, required this.borderRadius, required this.borderColor, required this.margin, this.hidesUnderline = false, this.disabled = false, this.isOverButton = false, this.menuOffset, this.isSearchable = false, this.isMultiSelect = false, this.labelText, this.labelTextStyle, this.allowEmpty = true, // 新增参数,默认值为 true }) : assert( isMultiSelect ? (controller == null && onChanged == null && multiSelectController != null && onMultiSelectChanged != null) : (controller != null && onChanged != null && multiSelectController == null && onMultiSelectChanged == null), ); /// The controller for the dropdown field. final FormFieldController? controller; /// The controller for the multi-select dropdown field. final FormFieldController?>? multiSelectController; /// The text to display as a hint when no option is selected. final String? hintText; /// The text to display as a hint in the search field. final String? searchHintText; /// The list of options to display in the dropdown. final List options; /// The list of labels corresponding to the options. final List? optionLabels; /// A callback function that is called when the selected option changes. final Function(T?)? onChanged; final Function(bool)? onMenuStateChange; /// A callback function that is called when the selected options change in multi-select mode. final Function(List?)? onMultiSelectChanged; /// The icon to display in the dropdown field. final Widget? icon; /// The width of the dropdown field. final double? width; /// The height of the dropdown field. final double? height; /// The maximum height of the dropdown menu. final double? maxHeight; /// The background color of the dropdown field. final Color? fillColor; /// The text style for the search hint text. final TextStyle? searchHintTextStyle; final TextStyle? hintTextStyle; /// The text style for the search text. final TextStyle? searchTextStyle; /// The color of the search cursor. final Color? searchCursorColor; /// The text style for the dropdown field. final TextStyle textStyle; /// The elevation of the dropdown menu. final double elevation; /// The width of the dropdown field's border. final double borderWidth; /// The border radius of the dropdown field. final double borderRadius; /// The color of the dropdown field's border. final Color borderColor; /// The margin around the dropdown field. final EdgeInsetsGeometry margin; /// Whether to hide the underline of the dropdown field. final bool hidesUnderline; /// Whether the dropdown is disabled. final bool disabled; /// Whether the dropdown menu is displayed over the button. final bool isOverButton; /// The offset of the dropdown menu. final Offset? menuOffset; /// Whether the dropdown is searchable. final bool isSearchable; /// Whether the dropdown is in multi-select mode. final bool isMultiSelect; /// The label text for the dropdown field. final String? labelText; /// The text style for the label text. final TextStyle? labelTextStyle; /// Whether the dropdown allows empty selection. final bool allowEmpty; @override State> createState() => _FlutterFlowDropDownState(); } class _FlutterFlowDropDownState extends State> { bool get isMultiSelect => widget.isMultiSelect; FormFieldController get controller => widget.controller!; FormFieldController?> get multiSelectController => widget.multiSelectController!; T? get currentValue { final value = isMultiSelect ? multiSelectController.value?.firstOrNull : controller.value; return widget.options.contains(value) ? value : null; } Set get currentValues { if (!isMultiSelect || multiSelectController.value == null) { return {}; } return widget.options .toSet() .intersection(multiSelectController.value!.toSet()); } Map get optionLabels => Map.fromEntries( widget.options.asMap().entries.map( (option) => MapEntry( option.value, widget.optionLabels == null || widget.optionLabels!.length < option.key + 1 ? option.value.toString() : widget.optionLabels![option.key], ), ), ); EdgeInsetsGeometry get horizontalMargin => widget.margin.clamp( EdgeInsetsDirectional.zero, const EdgeInsetsDirectional.symmetric(horizontal: double.infinity), ); late void Function() _listener; final TextEditingController _textEditingController = TextEditingController(); List? _previousSelectedValues; @override void initState() { super.initState(); if (isMultiSelect) { _listener = () { if (!widget.allowEmpty && (multiSelectController.value == null || multiSelectController.value!.isEmpty) && (widget.options != null && widget.options.isNotEmpty)) { // showToast("请至少选择一项", color: color_warning); multiSelectController.value = _previousSelectedValues; } else { _previousSelectedValues = List.from(multiSelectController.value!); } widget.onMultiSelectChanged!(multiSelectController.value); }; multiSelectController.addListener(_listener); } else { _listener = () { setState(() {}); widget.onChanged!(controller.value); }; controller.addListener(_listener); } } @override void dispose() { if (isMultiSelect) { multiSelectController.removeListener(_listener); } else { controller.removeListener(_listener); } super.dispose(); } @override Widget build(BuildContext context) { final dropdownWidget = _buildDropdownWidget(); return SizedBox( width: widget.width, height: widget.height, child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius), border: Border.all( color: widget.borderColor, width: widget.borderWidth, ), color: widget.fillColor, ), child: Padding( padding: _useDropdown2() ? EdgeInsets.zero : widget.margin, child: widget.hidesUnderline ? DropdownButtonHideUnderline(child: dropdownWidget) : dropdownWidget, ), ), ); } bool _useDropdown2() => widget.isMultiSelect || widget.isSearchable || !widget.isOverButton || widget.maxHeight != null; Widget _buildDropdownWidget() => _useDropdown2() ? _buildDropdown() : _buildLegacyDropdown(); Widget _buildLegacyDropdown() { return DropdownButtonFormField( value: currentValue, hint: _createHintText(), items: _createMenuItems(), elevation: widget.elevation.toInt(), onChanged: widget.disabled ? null : (value) => controller.value = value, icon: widget.icon, isExpanded: true, dropdownColor: widget.fillColor, focusColor: Colors.transparent, decoration: InputDecoration( labelText: widget.labelText == null || widget.labelText!.isEmpty ? null : widget.labelText, labelStyle: widget.labelTextStyle, border: widget.hidesUnderline ? InputBorder.none : const UnderlineInputBorder(), ), ); } Text? _createHintText() => widget.hintText != null ? Text( widget.hintText!, style: widget.hintTextStyle, maxLines: 1, overflow: TextOverflow.ellipsis, // 超出部分显示省略号 ) : null; List> _createMenuItems() => widget.options .map( (option) => DropdownMenuItem( value: option, child: Padding( padding: _useDropdown2() ? horizontalMargin : EdgeInsets.zero, child: Text(optionLabels[option] ?? '', style: widget.textStyle), ), ), ) .toList(); List> _createMultiselectMenuItems() => widget.options .map( (item) => DropdownMenuItem( value: item, // Disable default onTap to avoid closing menu when selecting an item enabled: false, child: StatefulBuilder( builder: (context, menuSetState) { final isSelected = multiSelectController.value?.contains(item) ?? false; return InkWell( onTap: () { multiSelectController.value ??= []; isSelected ? multiSelectController.value!.remove(item) : multiSelectController.value!.add(item); multiSelectController.update(); // This rebuilds the StatefulWidget to update the button's text. setState(() {}); // This rebuilds the dropdownMenu Widget to update the check mark. menuSetState(() {}); }, child: Container( height: double.infinity, padding: horizontalMargin, child: Row( children: [ if (isSelected) const Icon(Icons.check_box_outlined) else const Icon(Icons.check_box_outline_blank), const SizedBox(width: 16), Expanded( child: Text( optionLabels[item]!, style: widget.textStyle, ), ), ], ), ), ); }, ), ), ) .toList(); Widget _buildDropdown() { final overlayColor = WidgetStateProperty.resolveWith((states) => states.contains(WidgetState.focused) ? Colors.transparent : null); final iconStyleData = widget.icon != null ? IconStyleData(icon: widget.icon!) : const IconStyleData(); return DropdownButton2( value: currentValue, hint: _createHintText(), items: isMultiSelect ? _createMultiselectMenuItems() : _createMenuItems(), iconStyleData: iconStyleData, buttonStyleData: ButtonStyleData( elevation: widget.elevation.toInt(), overlayColor: WidgetStateProperty.all(Colors.transparent), padding: widget.margin, ), menuItemStyleData: MenuItemStyleData( overlayColor: WidgetStateProperty.all(Colors.transparent), padding: EdgeInsets.zero, ), dropdownStyleData: DropdownStyleData( elevation: widget.elevation.toInt(), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), color: widget.fillColor, ), isOverButton: widget.isOverButton, offset: widget.menuOffset ?? Offset.zero, maxHeight: widget.maxHeight, padding: EdgeInsets.zero, ), onChanged: widget.disabled ? null : (isMultiSelect ? (_) {} : (val) => widget.controller!.value = val), isExpanded: true, selectedItemBuilder: (context) => widget.options .map( (item) => Align( alignment: AlignmentDirectional.centerStart, child: Text( isMultiSelect ? currentValues .where((v) => optionLabels.containsKey(v)) .map((v) => optionLabels[v]) .join(', ') : optionLabels[item]!, style: widget.textStyle, maxLines: 1, ), ), ) .toList(), dropdownSearchData: widget.isSearchable ? DropdownSearchData( searchController: _textEditingController, searchInnerWidgetHeight: 50, searchInnerWidget: Container( height: 50, padding: const EdgeInsets.only( top: 8, bottom: 4, right: 8, left: 8, ), child: TextFormField( expands: true, maxLines: null, controller: _textEditingController, cursorColor: widget.searchCursorColor, style: widget.searchTextStyle, decoration: InputDecoration( isDense: true, contentPadding: const EdgeInsets.symmetric( horizontal: 10, vertical: 8, ), hintText: widget.searchHintText, hintStyle: widget.searchHintTextStyle, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), searchMatchFn: (item, searchValue) { return (optionLabels[item.value] ?? '') .toLowerCase() .contains(searchValue.toLowerCase()); }, ) : null, // This is to clear the search value when you close the menu onMenuStateChange: (isOpen) { if (widget.isSearchable && !isOpen) { _textEditingController.clear(); } if (widget.onMenuStateChange != null) { widget.onMenuStateChange!(isOpen); } }, ); } }