From 684d0c2c7a3b4a3c463dc0e556010de7c54311bb Mon Sep 17 00:00:00 2001 From: Fulberto TCHIAKPE Date: Wed, 10 May 2023 10:35:29 +0100 Subject: [PATCH] (feat): country code picker style (dialog or modal bottom sheet) suggested --- example/lib/main.dart | 1 + lib/country_code_picker.dart | 60 +++++++++- lib/src/bottom_sheet.dart | 207 +++++++++++++++++++++++++++++++++++ lib/src/constants.dart | 1 + 4 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 lib/src/bottom_sheet.dart create mode 100644 lib/src/constants.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index c8d9d1e..6c40323 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -122,6 +122,7 @@ class MyAppState extends State { flagDecoration: BoxDecoration( borderRadius: BorderRadius.circular(7), ), + pickerStyle: PickerStyle.bottomSheet, ), const SizedBox( width: 400, diff --git a/lib/country_code_picker.dart b/lib/country_code_picker.dart index a2d82a7..01a3263 100644 --- a/lib/country_code_picker.dart +++ b/lib/country_code_picker.dart @@ -3,6 +3,8 @@ library country_code_picker; import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; +import 'src/bottom_sheet.dart'; +import 'src/constants.dart'; import 'src/country_code.dart'; import 'src/country_codes.dart'; import 'src/selection_dialog.dart'; @@ -11,6 +13,8 @@ export 'src/country_code.dart'; export 'src/country_codes.dart'; export 'src/country_localizations.dart'; export 'src/selection_dialog.dart'; +export 'src/bottom_sheet.dart'; +export 'src/constants.dart'; class CountryCodePicker extends StatefulWidget { final ValueChanged? onChanged; @@ -29,6 +33,9 @@ class CountryCodePicker extends StatefulWidget { final TextOverflow textOverflow; final Icon closeIcon; + ///Picker style [BottomSheet] or [Dialog] + final PickerStyle pickerStyle; + /// Barrier color of ModalBottomSheet final Color? barrierColor; @@ -119,6 +126,7 @@ class CountryCodePicker extends StatefulWidget { this.dialogBackgroundColor, this.closeIcon = const Icon(Icons.close), this.countryList = codes, + this.pickerStyle = PickerStyle.dialog, Key? key, }) : super(key: key); @@ -145,7 +153,7 @@ class CountryCodePicker extends StatefulWidget { .toList(); } - return CountryCodePickerState(elements); + return CountryCodePickerState(elements, pickerStyle); } } @@ -153,20 +161,27 @@ class CountryCodePickerState extends State { CountryCode? selectedItem; List elements = []; List favoriteElements = []; + PickerStyle pickerStyle; - CountryCodePickerState(this.elements); + CountryCodePickerState(this.elements, this.pickerStyle); @override Widget build(BuildContext context) { Widget internalWidget; if (widget.builder != null) { internalWidget = InkWell( - onTap: showCountryCodePickerDialog, + onTap: pickerStyle == PickerStyle.dialog + ? showCountryCodePickerDialog + : showCountryCodePickerBottomSheet, child: widget.builder!(selectedItem), ); } else { internalWidget = TextButton( - onPressed: widget.enabled ? showCountryCodePickerDialog : null, + onPressed: widget.enabled + ? pickerStyle == PickerStyle.dialog + ? showCountryCodePickerDialog + : showCountryCodePickerBottomSheet + : null, child: Padding( padding: widget.padding, child: Flex( @@ -321,6 +336,43 @@ class CountryCodePickerState extends State { } } + void showCountryCodePickerBottomSheet() async { + final item = await showModalBottomSheet( + isScrollControlled: true, + context: context, + backgroundColor: Colors.transparent, + elevation: 0, + builder: (ctx) { + return SelectionBottomSheet( + elements, + favoriteElements, + showCountryOnly: widget.showCountryOnly, + emptySearchBuilder: widget.emptySearchBuilder, + searchDecoration: widget.searchDecoration, + searchStyle: widget.searchStyle, + textStyle: widget.dialogTextStyle, + boxDecoration: widget.boxDecoration, + showFlag: widget.showFlagDialog ?? widget.showFlag, + flagWidth: widget.flagWidth, + size: widget.dialogSize, + backgroundColor: widget.dialogBackgroundColor, + barrierColor: widget.barrierColor, + hideSearch: widget.hideSearch, + closeIcon: widget.closeIcon, + flagDecoration: widget.flagDecoration, + ); + }, + ); + + if (item == null) return; + + setState(() { + selectedItem = item; + }); + + _publishSelection(item); + } + void _publishSelection(CountryCode countryCode) { if (widget.onChanged != null) { widget.onChanged!(countryCode); diff --git a/lib/src/bottom_sheet.dart b/lib/src/bottom_sheet.dart new file mode 100644 index 0000000..0f32f27 --- /dev/null +++ b/lib/src/bottom_sheet.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; + +import 'country_code.dart'; +import 'country_localizations.dart'; + +/// selection bottom sheet used for selection of the country code +class SelectionBottomSheet extends StatefulWidget { + final List elements; + final bool? showCountryOnly; + final InputDecoration searchDecoration; + final TextStyle? searchStyle; + final TextStyle? textStyle; + final BoxDecoration? boxDecoration; + final WidgetBuilder? emptySearchBuilder; + final bool? showFlag; + final double flagWidth; + final Decoration? flagDecoration; + final Size? size; + final bool hideSearch; + final Icon? closeIcon; + + /// Background color of SelectionBottomSheet + final Color? backgroundColor; + + /// Boxshaow color of SelectionBottomSheet that matches CountryCodePicker barrier color + final Color? barrierColor; + + /// elements passed as favorite + final List favoriteElements; + + SelectionBottomSheet( + this.elements, + this.favoriteElements, { + Key? key, + this.showCountryOnly, + this.emptySearchBuilder, + InputDecoration searchDecoration = const InputDecoration(), + this.searchStyle, + this.textStyle, + this.boxDecoration, + this.showFlag, + this.flagDecoration, + this.flagWidth = 32, + this.size, + this.backgroundColor, + this.barrierColor, + this.hideSearch = false, + this.closeIcon, + }) : searchDecoration = searchDecoration.prefixIcon == null + ? searchDecoration.copyWith(prefixIcon: const Icon(Icons.search)) + : searchDecoration, + super(key: key); + + @override + State createState() => _SelectionBottomSheetState(); +} + +class _SelectionBottomSheetState extends State { + /// this is useful for filtering purpose + late List filteredElements; + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.all(0.0), + child: Container( + clipBehavior: Clip.hardEdge, + width: widget.size?.width ?? MediaQuery.of(context).size.width, + height: + widget.size?.height ?? MediaQuery.of(context).size.height * 0.85, + decoration: widget.boxDecoration ?? + BoxDecoration( + color: widget.backgroundColor ?? Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + boxShadow: [ + BoxShadow( + color: widget.barrierColor ?? Colors.grey.withOpacity(1), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), // changes position of shadow + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + IconButton( + padding: const EdgeInsets.all(0), + iconSize: 20, + icon: widget.closeIcon!, + onPressed: () => Navigator.pop(context), + ), + if (!widget.hideSearch) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: TextField( + style: widget.searchStyle, + decoration: widget.searchDecoration, + onChanged: _filterElements, + ), + ), + Expanded( + child: ListView( + children: [ + widget.favoriteElements.isEmpty + ? const DecoratedBox(decoration: BoxDecoration()) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...widget.favoriteElements.map( + (f) => SimpleDialogOption( + child: _buildOption(f), + onPressed: () { + _selectItem(f); + }, + ), + ), + const Divider(), + ], + ), + if (filteredElements.isEmpty) + _buildEmptySearchWidget(context) + else + ...filteredElements.map( + (e) => SimpleDialogOption( + child: _buildOption(e), + onPressed: () { + _selectItem(e); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + + Widget _buildOption(CountryCode e) { + return SizedBox( + width: 400, + child: Flex( + direction: Axis.horizontal, + children: [ + if (widget.showFlag!) + Flexible( + child: Container( + margin: const EdgeInsets.only(right: 16.0), + decoration: widget.flagDecoration, + clipBehavior: + widget.flagDecoration == null ? Clip.none : Clip.hardEdge, + child: Image.asset( + e.flagUri!, + package: 'country_code_picker', + width: widget.flagWidth, + ), + ), + ), + Expanded( + flex: 4, + child: Text( + widget.showCountryOnly! + ? e.toCountryStringOnly() + : e.toLongString(), + overflow: TextOverflow.fade, + style: widget.textStyle, + ), + ), + ], + ), + ); + } + + Widget _buildEmptySearchWidget(BuildContext context) { + if (widget.emptySearchBuilder != null) { + return widget.emptySearchBuilder!(context); + } + + return Center( + child: Text(CountryLocalizations.of(context)?.translate('no_country') ?? + 'No country found'), + ); + } + + @override + void initState() { + filteredElements = widget.elements; + super.initState(); + } + + void _filterElements(String s) { + s = s.toUpperCase(); + setState(() { + filteredElements = widget.elements + .where((e) => + e.code!.contains(s) || + e.dialCode!.contains(s) || + e.name!.toUpperCase().contains(s)) + .toList(); + }); + } + + void _selectItem(CountryCode e) { + Navigator.pop(context, e); + } +} diff --git a/lib/src/constants.dart b/lib/src/constants.dart new file mode 100644 index 0000000..9dac0ca --- /dev/null +++ b/lib/src/constants.dart @@ -0,0 +1 @@ +enum PickerStyle { dialog, bottomSheet, fullScreen }