diff --git a/lib/bottom_sheet/multi_select_bottom_sheet_field.dart b/lib/bottom_sheet/multi_select_bottom_sheet_field.dart index ff11320..3f47305 100644 --- a/lib/bottom_sheet/multi_select_bottom_sheet_field.dart +++ b/lib/bottom_sheet/multi_select_bottom_sheet_field.dart @@ -1,36 +1,25 @@ -import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; -import '../util/multi_select_list_type.dart'; -import '../chip_display/multi_select_chip_display.dart'; import '../util/multi_select_item.dart'; -import 'multi_select_bottom_sheet.dart'; - -/// A customizable InkWell widget that opens the MultiSelectBottomSheet -// ignore: must_be_immutable -class MultiSelectBottomSheetField extends FormField> { - /// Style the Container that makes up the field. - final BoxDecoration? decoration; - - /// Set text that is displayed on the button. - final Text? buttonText; - - /// Specify the button icon. - final Icon? buttonIcon; +import '../util/multi_select_actions.dart'; +import '../util/multi_select_list_type.dart'; +/// A bottom sheet widget containing either a classic checkbox style list, or a chip style list. +class MultiSelectBottomSheet extends StatefulWidget + with MultiSelectActions { /// List of items to select from. - final List> items; + final List> items; /// The list of selected values before interaction. - final List initialValue; + final List initialValue; - /// The text at the top of the dialog. + /// The text at the top of the BottomSheet. final Widget? title; /// Fires when the an item is selected / unselected. - final void Function(List)? onSelectionChanged; + final void Function(List)? onSelectionChanged; /// Fires when confirm is tapped. - final void Function(List) onConfirm; + final void Function(List)? onConfirm; /// Toggles search functionality. final bool searchable; @@ -44,12 +33,9 @@ class MultiSelectBottomSheetField extends FormField> { /// An enum that determines which type of list to render. final MultiSelectListType? listType; - /// Sets the color of the checkbox or chip body when selected. + /// Sets the color of the checkbox or chip when it's selected. final Color? selectedColor; - /// Set the hint text of the search field. - final String? searchHint; - /// Set the initial height of the BottomSheet. final double? initialChildSize; @@ -59,450 +45,342 @@ class MultiSelectBottomSheetField extends FormField> { /// Set the maximum height of the BottomSheet. final double? maxChildSize; - /// Apply a ShapeBorder to alter the edges of the BottomSheet. - final ShapeBorder? shape; - - /// Set the color of the space outside the BottomSheet. - final Color? barrierColor; - - /// Overrides the default MultiSelectChipDisplay attached to this field. - /// If you want to remove it, use MultiSelectChipDisplay.none(). - final MultiSelectChipDisplay? chipDisplay; + /// Set the placeholder text of the search field. + final String? searchHint; /// A function that sets the color of selected items based on their value. /// It will either set the chip color, or the checkbox color depending on the list type. - final Color Function(V)? colorator; - - /// Set the background color of the bottom sheet. - final Color? backgroundColor; + final Color? Function(T)? colorator; /// Color of the chip body or checkbox border while not selected. final Color? unselectedColor; - /// Replaces the deafult search icon when searchable is true. + /// Icon button that shows the search field. final Icon? searchIcon; - /// Replaces the default close search icon when searchable is true. + /// Icon button that hides the search field final Icon? closeSearchIcon; - /// The TextStyle of the items within the BottomSheet. + /// Style the text on the chips or list tiles. final TextStyle? itemsTextStyle; /// Style the text on the selected chips or list tiles. final TextStyle? selectedItemsTextStyle; - /// Moves the selected items to the top of the list. - final bool separateSelectedItems; - - /// Style the text that is typed into the search field. + /// Style the search text. final TextStyle? searchTextStyle; /// Style the search hint. final TextStyle? searchHintStyle; + /// Moves the selected items to the top of the list. + final bool separateSelectedItems; + /// Set the color of the check in the checkbox final Color? checkColor; - /// Whether the user can dismiss the widget by tapping outside - final bool isDismissible; - - final AutovalidateMode autovalidateMode; - final FormFieldValidator>? validator; - final FormFieldSetter>? onSaved; - final GlobalKey? key; - FormFieldState>? state; - - MultiSelectBottomSheetField({ - required this.items, - required this.onConfirm, - this.title, - this.buttonText, - this.buttonIcon, - this.listType, - this.decoration, - this.onSelectionChanged, - this.chipDisplay, - this.initialValue = const [], - this.searchable = false, - this.confirmText, - this.cancelText, - this.selectedColor, - this.initialChildSize, - this.minChildSize, - this.maxChildSize, - this.shape, - this.barrierColor, - this.searchHint, - this.colorator, - this.backgroundColor, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchTextStyle, - this.searchHintStyle, - this.selectedItemsTextStyle, - this.separateSelectedItems = false, - this.checkColor, - this.isDismissible = true, - this.key, - this.onSaved, - this.validator, - this.autovalidateMode = AutovalidateMode.disabled, - }) : super( - key: key, - onSaved: onSaved, - validator: validator, - autovalidateMode: autovalidateMode, - initialValue: initialValue, - builder: (FormFieldState> state) { - _MultiSelectBottomSheetFieldView view = - _MultiSelectBottomSheetFieldView( - items: items, - decoration: decoration, - unselectedColor: unselectedColor, - colorator: colorator, - itemsTextStyle: itemsTextStyle, - selectedItemsTextStyle: selectedItemsTextStyle, - backgroundColor: backgroundColor, - title: title, - initialValue: initialValue, - barrierColor: barrierColor, - buttonIcon: buttonIcon, - buttonText: buttonText, - cancelText: cancelText, - chipDisplay: chipDisplay, - closeSearchIcon: closeSearchIcon, - confirmText: confirmText, - initialChildSize: initialChildSize, - listType: listType, - maxChildSize: maxChildSize, - minChildSize: minChildSize, - onConfirm: onConfirm, - onSelectionChanged: onSelectionChanged, - searchHintStyle: searchHintStyle, - searchIcon: searchIcon, - searchHint: searchHint, - searchTextStyle: searchTextStyle, - searchable: searchable, - selectedColor: selectedColor, - separateSelectedItems: separateSelectedItems, - shape: shape, - checkColor: checkColor, - isDismissible: isDismissible, - ); - return _MultiSelectBottomSheetFieldView._withState( - view as _MultiSelectBottomSheetFieldView, state); - }); -} - -// ignore: must_be_immutable -class _MultiSelectBottomSheetFieldView extends StatefulWidget { - final BoxDecoration? decoration; - final Text? buttonText; - final Icon? buttonIcon; - final List> items; - final List initialValue; - final Widget? title; - final void Function(List)? onSelectionChanged; - final void Function(List)? onConfirm; - final bool searchable; - final Text? confirmText; - final Text? cancelText; - final MultiSelectListType? listType; - final Color? selectedColor; - final String? searchHint; - final double? initialChildSize; - final double? minChildSize; - final double? maxChildSize; - final ShapeBorder? shape; - final Color? barrierColor; - final MultiSelectChipDisplay? chipDisplay; - final Color Function(V)? colorator; - final Color? backgroundColor; - final Color? unselectedColor; - final Icon? searchIcon; - final Icon? closeSearchIcon; - final TextStyle? itemsTextStyle; - final TextStyle? selectedItemsTextStyle; - final TextStyle? searchTextStyle; - final TextStyle? searchHintStyle; - final bool separateSelectedItems; - final Color? checkColor; - final bool isDismissible; - FormFieldState>? state; - - _MultiSelectBottomSheetFieldView({ - required this.items, - this.title, - this.buttonText, - this.buttonIcon, - this.listType, - this.decoration, - this.onSelectionChanged, - this.onConfirm, - this.chipDisplay, - required this.initialValue, - required this.searchable, - this.confirmText, - this.cancelText, - this.selectedColor, - this.initialChildSize, - this.minChildSize, - this.maxChildSize, - this.shape, - this.barrierColor, - this.searchHint, - this.colorator, - this.backgroundColor, - this.unselectedColor, - this.searchIcon, - this.closeSearchIcon, - this.itemsTextStyle, - this.searchTextStyle, - this.searchHintStyle, - this.selectedItemsTextStyle, - required this.separateSelectedItems, - this.checkColor, - required this.isDismissible, - }); - - /// This constructor allows a FormFieldState to be passed in. Called by MultiSelectBottomSheetField. - _MultiSelectBottomSheetFieldView._withState( - _MultiSelectBottomSheetFieldView field, FormFieldState> state) - : items = field.items, - title = field.title, - buttonText = field.buttonText, - buttonIcon = field.buttonIcon, - listType = field.listType, - decoration = field.decoration, - onSelectionChanged = field.onSelectionChanged, - onConfirm = field.onConfirm, - chipDisplay = field.chipDisplay, - initialValue = field.initialValue, - searchable = field.searchable, - confirmText = field.confirmText, - cancelText = field.cancelText, - selectedColor = field.selectedColor, - initialChildSize = field.initialChildSize, - minChildSize = field.minChildSize, - maxChildSize = field.maxChildSize, - shape = field.shape, - barrierColor = field.barrierColor, - searchHint = field.searchHint, - colorator = field.colorator, - backgroundColor = field.backgroundColor, - unselectedColor = field.unselectedColor, - searchIcon = field.searchIcon, - closeSearchIcon = field.closeSearchIcon, - itemsTextStyle = field.itemsTextStyle, - searchHintStyle = field.searchHintStyle, - searchTextStyle = field.searchTextStyle, - selectedItemsTextStyle = field.selectedItemsTextStyle, - separateSelectedItems = field.separateSelectedItems, - checkColor = field.checkColor, - isDismissible = field.isDismissible, - state = state; + /// it will disable multiple select and switch in to single select mode , default value false , + final bool? singleSelectMode; + + MultiSelectBottomSheet( + {required this.items, + required this.initialValue, + this.title, + this.onSelectionChanged, + this.onConfirm, + this.listType, + this.cancelText, + this.confirmText, + this.searchable = false, + this.selectedColor, + this.initialChildSize, + this.minChildSize, + this.maxChildSize, + this.colorator, + this.unselectedColor, + this.searchIcon, + this.closeSearchIcon, + this.itemsTextStyle, + this.searchTextStyle, + this.searchHint, + this.searchHintStyle, + this.selectedItemsTextStyle, + this.separateSelectedItems = false, + this.checkColor, + this.singleSelectMode}); @override - __MultiSelectBottomSheetFieldViewState createState() => - __MultiSelectBottomSheetFieldViewState(); + _MultiSelectBottomSheetState createState() => + _MultiSelectBottomSheetState(items); } -class __MultiSelectBottomSheetFieldViewState - extends State<_MultiSelectBottomSheetFieldView> { - List _selectedItems = []; +class _MultiSelectBottomSheetState extends State> { + List _selectedValues = []; + bool _showSearch = false; + List> _items; + + _MultiSelectBottomSheetState(this._items); @override void initState() { super.initState(); - _selectedItems.addAll(widget.initialValue); - } - - @override - void didUpdateWidget(_MultiSelectBottomSheetFieldView oldWidget) { - super.didUpdateWidget(oldWidget); + _selectedValues.addAll(widget.initialValue); - if (oldWidget.initialValue != widget.initialValue) { - _selectedItems = []; - _selectedItems.addAll(widget.initialValue); + for (int i = 0; i < _items.length; i++) { + _items[i].selected = false; + if (_selectedValues.contains(_items[i].value)) { + _items[i].selected = true; + } + } - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.state!.didChange(_selectedItems); - }); + if (widget.separateSelectedItems) { + _items = widget.separateSelected(_items); } } - Widget _buildInheritedChipDisplay() { - List?> chipDisplayItems = []; - chipDisplayItems = _selectedItems - .map((e) => - widget.items.firstWhereOrNull((element) => e == element.value)) - .toList(); - chipDisplayItems.removeWhere((element) => element == null); - if (widget.chipDisplay != null) { - // if user has specified a chipDisplay, use its params - if (widget.chipDisplay!.disabled!) { - return Container(); - } else { - return MultiSelectChipDisplay( - items: chipDisplayItems, - colorator: widget.chipDisplay!.colorator ?? widget.colorator, - onTap: (item) { - List? newValues; - if (widget.chipDisplay!.onTap != null) { - dynamic result = widget.chipDisplay!.onTap!(item); - if (result is List) newValues = result; + /// Returns a CheckboxListTile + Widget _buildListItem(MultiSelectItem item) { + return Theme( + data: ThemeData( + unselectedWidgetColor: widget.unselectedColor ?? Colors.black54, + ), + child: CheckboxListTile( + checkColor: widget.checkColor, + value: item.selected, + activeColor: widget.colorator != null + ? widget.colorator!(item.value) ?? widget.selectedColor + : widget.selectedColor, + title: Text( + item.label, + style: item.selected + ? widget.selectedItemsTextStyle + : widget.itemsTextStyle, + ), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (checked) { + setState(() { + _selectedValues = widget.onItemCheckedChange( + _selectedValues, item.value, checked!); + + if (checked) { + item.selected = true; + } else { + item.selected = false; } - if (newValues != null) { - _selectedItems = newValues; - if (widget.state != null) { - widget.state!.didChange(_selectedItems); - } + if (widget.separateSelectedItems) { + _items = widget.separateSelected(_items); } - }, - decoration: widget.chipDisplay!.decoration, - chipColor: widget.chipDisplay!.chipColor ?? - ((widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(0.35) - : null), - alignment: widget.chipDisplay!.alignment, - textStyle: widget.chipDisplay!.textStyle, - icon: widget.chipDisplay!.icon, - shape: widget.chipDisplay!.shape, - scroll: widget.chipDisplay!.scroll, - scrollBar: widget.chipDisplay!.scrollBar, - height: widget.chipDisplay!.height, - chipWidth: widget.chipDisplay!.chipWidth, - ); - } - } else { - // user didn't specify a chipDisplay, build the default - return MultiSelectChipDisplay( - items: chipDisplayItems, - colorator: widget.colorator, - chipColor: (widget.selectedColor != null && - widget.selectedColor != Colors.transparent) - ? widget.selectedColor!.withOpacity(0.35) - : null, - ); - } + }); + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(_selectedValues); + } + }, + ), + ); } - _showBottomSheet(BuildContext ctx) async { - List? myVar = await showModalBottomSheet>( - isDismissible: widget.isDismissible, - backgroundColor: widget.backgroundColor, - barrierColor: widget.barrierColor, - shape: widget.shape ?? - RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(15.0)), - ), - isScrollControlled: true, - context: context, - builder: (context) { - return MultiSelectBottomSheet( - checkColor: widget.checkColor, - selectedItemsTextStyle: widget.selectedItemsTextStyle, - searchTextStyle: widget.searchTextStyle, - searchHintStyle: widget.searchHintStyle, - itemsTextStyle: widget.itemsTextStyle, - searchIcon: widget.searchIcon, - closeSearchIcon: widget.closeSearchIcon, - unselectedColor: widget.unselectedColor, - colorator: widget.colorator, - searchHint: widget.searchHint, - selectedColor: widget.selectedColor, - listType: widget.listType, - items: widget.items, - cancelText: widget.cancelText, - confirmText: widget.confirmText, - separateSelectedItems: widget.separateSelectedItems, - initialValue: _selectedItems, - onConfirm: (selected) { - if (widget.state != null) { - widget.state!.didChange(selected); - } - _selectedItems = selected; - if (widget.onConfirm != null) widget.onConfirm!(selected); - }, - onSelectionChanged: widget.onSelectionChanged, - searchable: widget.searchable, - title: widget.title, - initialChildSize: widget.initialChildSize, - minChildSize: widget.minChildSize, - maxChildSize: widget.maxChildSize, - ); - }); - print(myVar.toString()); - _selectedItems = myVar!; + /// Returns a ChoiceChip + Widget _buildChipItem(MultiSelectItem item) { + return Container( + padding: const EdgeInsets.all(2.0), + child: ChoiceChip( + backgroundColor: widget.unselectedColor, + selectedColor: + widget.colorator != null && widget.colorator!(item.value) != null + ? widget.colorator!(item.value) + : widget.selectedColor != null + ? widget.selectedColor + : Theme.of(context).primaryColor.withOpacity(0.35), + label: Text( + item.label, + style: _selectedValues.contains(item.value) + ? TextStyle( + color: widget.selectedItemsTextStyle?.color ?? + widget.colorator?.call(item.value) ?? + widget.selectedColor?.withOpacity(1) ?? + Theme.of(context).primaryColor, + fontSize: widget.selectedItemsTextStyle != null + ? widget.selectedItemsTextStyle!.fontSize + : null, + ) + : widget.itemsTextStyle, + ), + selected: item.selected, + onSelected: (checked) { + //single select mode + if (widget.singleSelectMode ?? false) { + _items.forEach((i) { + i.selected = false; + }); + _selectedValues.clear(); + } + if (checked) { + item.selected = true; + } else { + item.selected = false; + } + setState(() { + _selectedValues = widget.onItemCheckedChange( + _selectedValues, item.value, checked); + }); + if (widget.onSelectionChanged != null) { + widget.onSelectionChanged!(_selectedValues); + } + }, + ), + ); } @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - InkWell( - onTap: () { - _showBottomSheet(context); - }, - child: Container( - decoration: widget.state != null - ? widget.decoration ?? - BoxDecoration( - border: Border( - bottom: BorderSide( - color: widget.state != null && widget.state!.hasError - ? Colors.red.shade800.withOpacity(0.6) - : _selectedItems.isNotEmpty - ? (widget.selectedColor != null && - widget.selectedColor != - Colors.transparent) - ? widget.selectedColor! - : Theme.of(context).primaryColor - : Colors.black45, - width: _selectedItems.isNotEmpty - ? (widget.state != null && widget.state!.hasError) - ? 1.4 - : 1.8 - : 1.2, + return Container( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: DraggableScrollableSheet( + initialChildSize: widget.initialChildSize ?? 0.3, + minChildSize: widget.minChildSize ?? 0.3, + maxChildSize: widget.maxChildSize ?? 0.6, + expand: false, + builder: (BuildContext context, ScrollController scrollController) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _showSearch + ? Expanded( + child: Container( + padding: const EdgeInsets.only(left: 10), + child: TextField( + autofocus: true, + style: widget.searchTextStyle, + decoration: InputDecoration( + hintStyle: widget.searchHintStyle, + hintText: widget.searchHint ?? "Search", + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: widget.selectedColor ?? + Theme.of(context).primaryColor), + ), + ), + onChanged: (val) { + List> filteredList = []; + filteredList = widget.updateSearchQuery( + val, widget.items); + setState(() { + if (widget.separateSelectedItems) { + _items = + widget.separateSelected(filteredList); + } else { + _items = filteredList; + } + }); + }, + ), + ), + ) + : widget.title ?? + Text( + "Select", + style: TextStyle(fontSize: 18), + ), + widget.searchable + ? IconButton( + icon: _showSearch + ? widget.closeSearchIcon ?? Icon(Icons.close) + : widget.searchIcon ?? Icon(Icons.search), + onPressed: () { + setState(() { + _showSearch = !_showSearch; + if (!_showSearch) { + if (widget.separateSelectedItems) { + _items = + widget.separateSelected(widget.items); + } else { + _items = widget.items; + } + } + }); + }, + ) + : Padding( + padding: EdgeInsets.all(15), + ), + ], + ), + ), + Expanded( + child: widget.listType == null || + widget.listType == MultiSelectListType.LIST + ? ListView.builder( + controller: scrollController, + itemCount: _items.length, + itemBuilder: (context, index) { + return _buildListItem(_items[index]); + }, + ) + : SingleChildScrollView( + controller: scrollController, + child: Container( + padding: EdgeInsets.all(10), + child: Wrap( + children: _items.map(_buildChipItem).toList(), + ), ), ), - ) - : widget.decoration, - padding: EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - widget.buttonText ?? Text("Select"), - widget.buttonIcon ?? Icon(Icons.arrow_downward), - ], - ), - ), - ), - _buildInheritedChipDisplay(), - widget.state != null && widget.state!.hasError - ? SizedBox(height: 5) - : Container(), - widget.state != null && widget.state!.hasError - ? Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 4), - child: Text( - widget.state!.errorText!, - style: TextStyle( - color: Colors.red[800], - fontSize: 12.5, + ), + Container( + padding: EdgeInsets.all(2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: TextButton( + onPressed: () { + widget.onCancelTap(context, widget.initialValue); + }, + child: widget.cancelText ?? + Text( + "CANCEL", + style: TextStyle( + color: (widget.selectedColor != null && + widget.selectedColor != + Colors.transparent) + ? widget.selectedColor!.withOpacity(1) + : Theme.of(context).primaryColor, + ), + ), ), ), - ), - ], - ) - : Container(), - ], + SizedBox(width: 10), + Expanded( + child: TextButton( + onPressed: () { + widget.onConfirmTap( + context, _selectedValues, widget.onConfirm); + }, + child: widget.confirmText ?? + Text( + "OK", + style: TextStyle( + color: (widget.selectedColor != null && + widget.selectedColor != + Colors.transparent) + ? widget.selectedColor!.withOpacity(1) + : Theme.of(context).primaryColor, + ), + ), + ), + ), + ], + ), + ), + ], + ); + }, + ), ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 1e676fd..638bfda 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: multi_select_flutter description: A flexible multi select package for Flutter. Make multi select widgets the way you want. -version: 4.1.3 +version: 4.1.1 repository: https://github.com/CHB61/flutter-multi-select issue_tracker: https://github.com/CHB61/flutter-multi-select documentation: https://github.com/CHB61/flutter-multi-select