diff --git a/example/lib/CustomSelectionDialog/custom_selection_dialog.dart b/example/lib/CustomSelectionDialog/custom_selection_dialog.dart new file mode 100644 index 0000000..a2dde98 --- /dev/null +++ b/example/lib/CustomSelectionDialog/custom_selection_dialog.dart @@ -0,0 +1,168 @@ +import 'package:country_code_picker/country_code_picker.dart'; +import 'package:flutter/material.dart'; + +class CustomSelectionDialog extends StatefulWidget { + /// The full list of country codes from the parent SelectionDialog. + final List elements; + + /// Optional favorite country codes. + final List? favoriteElements; + + /// Header text (if any) to show at the top. + final String? headerText; + + /// Callback when a country code is selected. + final ValueChanged? onSelected; + + final bool hideCountryTitle; + + const CustomSelectionDialog({ + Key? key, + required this.elements, + this.favoriteElements, + this.headerText, + this.onSelected, + this.hideCountryTitle = false, + }) : super(key: key); + + @override + _CustomSelectionDialogState createState() => _CustomSelectionDialogState(); +} + +class _CustomSelectionDialogState extends State { + late List filteredElements; + final TextEditingController searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + filteredElements = widget.elements; + } + + void _filterElements(String query) { + setState(() { + final lowerQuery = query.toLowerCase(); + filteredElements = widget.elements.where((code) { + return (code.name?.toLowerCase().contains(lowerQuery) ?? false) || + (code.dialCode?.contains(query) ?? false) || + (code.code?.toLowerCase().contains(lowerQuery) ?? false); + }).toList(); + }); + } + + Widget _buildOption(CountryCode code) { + return ListTile( + leading: code.flagUri != null + ? ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.asset( + code.flagUri!, + package: 'country_code_picker', + width: 32, + height: 24, + fit: BoxFit.cover, + ), + ) + : null, + title: Text( + widget.hideCountryTitle + ? code.dialCode! + : "${code.name} (${code.dialCode})", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black), + ), + onTap: () { + widget.onSelected?.call(code); + Navigator.pop(context, code); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.height * 0.75, + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + ), + child: Column( + children: [ + // Header with title and close button. + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (widget.headerText != null) + Text( + widget.headerText!, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + IconButton( + icon: Icon(Icons.close, + color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const SizedBox(height: 8), + // Modern search field. + TextField( + style: TextStyle( + color: Colors.black), + controller: searchController, + decoration: InputDecoration( + hintText: 'Search country', + hintStyle: TextStyle( + color: Colors.black), + prefixIcon: Icon(Icons.search, + color: Colors.black), + filled: true, + fillColor: Colors.white, + contentPadding: + const EdgeInsets.symmetric(vertical: 0, horizontal: 16), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.0), + borderSide: BorderSide.none, + ), + ), + onChanged: _filterElements, + ), + const SizedBox(height: 16), + // Optional favorites. + if (widget.favoriteElements != null && + widget.favoriteElements!.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...widget.favoriteElements!.map((code) => _buildOption(code)), + const Divider(), + ], + ), + // List of filtered country codes. + Expanded( + child: filteredElements.isEmpty + ? Center( + child: Text( + 'No country found', + style: TextStyle(color: Colors.grey.shade600), + ), + ) + : ListView.builder( + itemCount: filteredElements.length, + itemBuilder: (context, index) { + return _buildOption(filteredElements[index]); + }, + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 30fe4db..de23aa8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,7 @@ import 'package:country_code_picker/country_code_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:hello_example/CustomSelectionDialog/custom_selection_dialog.dart'; void main() => runApp(const MyApp()); @@ -162,10 +163,35 @@ class MyAppState extends State { favorite: const ['+39', 'FR'], ), ), + // With Custom Dialog + CountryCodePicker( + onChanged: print, + // Initial selection and favorite can be one of code ('IT') OR dial_code('+39') + initialSelection: 'IT', + //You can set the margin between the flag and the country name to your taste. + margin: const EdgeInsets.symmetric(horizontal: 6), + comparator: (a, b) => b.name!.compareTo(a.name!), + //Get the country information relevant to the initial selection + onInit: (code) => debugPrint("on init ${code?.name} ${code?.dialCode} ${code?.name}"), + dialogBuilder: customDialogBuilder, + ), ], ), ), ), ); } + + Widget customDialogBuilder(BuildContext context, + List filteredElements, SelectionDialog dialog) { + return CustomSelectionDialog( + elements: dialog.elements, + headerText: dialog.headerText, + onSelected: (code) { + // This callback is optional because the SelectionDialog + // itself handles returning the selected code. + // You can update any state here if needed. + }, + ); + } } diff --git a/lib/country_code_picker.dart b/lib/country_code_picker.dart index c4b5260..869e956 100644 --- a/lib/country_code_picker.dart +++ b/lib/country_code_picker.dart @@ -115,6 +115,9 @@ class CountryCodePicker extends StatefulWidget { ///Header Text Alignment final MainAxisAlignment headerAlignment; + ///Custom dialog builder + final SelectionDialogBuilder? dialogBuilder; + const CountryCodePicker({ this.onChanged, this.onInit, @@ -159,6 +162,7 @@ class CountryCodePicker extends StatefulWidget { this.headerTextStyle = const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), this.hideHeaderText = false, this.topBarPadding = const EdgeInsets.symmetric(vertical: 5.0, horizontal: 20), + this.dialogBuilder, Key? key, }) : super(key: key); @@ -348,6 +352,7 @@ class CountryCodePickerState extends State { flagDecoration: widget.flagDecoration, dialogItemPadding: widget.dialogItemPadding, searchPadding: widget.searchPadding, + dialogBuilder: widget.dialogBuilder, ), ), ), diff --git a/lib/src/selection_dialog.dart b/lib/src/selection_dialog.dart index 46cc466..bbfd6c0 100644 --- a/lib/src/selection_dialog.dart +++ b/lib/src/selection_dialog.dart @@ -3,6 +3,10 @@ import 'package:flutter/material.dart'; import 'country_code.dart'; import 'country_localizations.dart'; +/// Custom builder signature for the selection dialog. +typedef SelectionDialogBuilder = Widget Function(BuildContext context, + List filteredElements, SelectionDialog dialog); + /// selection dialog used for selection of the country code class SelectionDialog extends StatefulWidget { final List elements; @@ -38,6 +42,9 @@ class SelectionDialog extends StatefulWidget { final EdgeInsetsGeometry searchPadding; + /// Custom dialog builder with extended signature. + final SelectionDialogBuilder? dialogBuilder; + SelectionDialog( this.elements, this.favoriteElements, { @@ -50,7 +57,7 @@ class SelectionDialog extends StatefulWidget { InputDecoration searchDecoration = const InputDecoration(), this.searchStyle, this.textStyle, - required this.topBarPadding, + required this.topBarPadding, this.headerText, this.boxDecoration, this.showFlag, @@ -64,6 +71,7 @@ class SelectionDialog extends StatefulWidget { this.closeIcon, this.dialogItemPadding = const EdgeInsets.symmetric(horizontal: 24, vertical: 8), this.searchPadding = const EdgeInsets.symmetric(horizontal: 24), + this.dialogBuilder, }) : searchDecoration = searchDecoration.prefixIcon == null ? searchDecoration.copyWith(prefixIcon: const Icon(Icons.search)) : searchDecoration, super(key: key); @@ -76,97 +84,101 @@ class _SelectionDialogState extends State { 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.withAlpha(255), - spreadRadius: 5, - blurRadius: 7, - offset: const Offset(0, 3), // changes position of shadow - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Padding( - padding:!widget.hideHeaderText? widget.topBarPadding: EdgeInsets.zero, - child: Row( - mainAxisAlignment: widget.headerAlignment, - children: [ - !widget.hideHeaderText && widget.headerText != null - ? Text( - widget.headerText!, - overflow: TextOverflow.fade, - style: widget.headerTextStyle, - ) - : const SizedBox.shrink(), - if (!widget.hideCloseIcon) - IconButton( - padding: const EdgeInsets.all(0), - iconSize: 20, - icon: widget.closeIcon!, - onPressed: () => Navigator.pop(context), + Widget build(BuildContext context) { + return widget.dialogBuilder != null + ? widget.dialogBuilder!(context, filteredElements, widget) + : 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.withAlpha(255), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), // changes position of shadow ), - ], - ), - ), - if (!widget.hideSearch) - Padding( - padding: widget.searchPadding, - 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) => InkWell( - onTap: () { - _selectItem(f); - }, - child: Padding( - padding: widget.dialogItemPadding, - child: _buildOption(f), - ))), - const Divider(), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding:!widget.hideHeaderText? widget.topBarPadding: EdgeInsets.zero, + child: Row( + mainAxisAlignment: widget.headerAlignment, + children: [ + !widget.hideHeaderText && widget.headerText != null + ? Text( + widget.headerText!, + overflow: TextOverflow.fade, + style: widget.headerTextStyle, + ) + : const SizedBox.shrink(), + if (!widget.hideCloseIcon) + IconButton( + padding: const EdgeInsets.all(0), + iconSize: 20, + icon: widget.closeIcon!, + onPressed: () => Navigator.pop(context), ), - if (filteredElements.isEmpty) - _buildEmptySearchWidget(context) - else - ...filteredElements.map((e) => InkWell( - onTap: () { - _selectItem(e); - }, - child: Padding( - padding: widget.dialogItemPadding, - child: _buildOption(e), - ))), - ], - ), + ], + ), + ), + if (!widget.hideSearch) + Padding( + padding: widget.searchPadding, + 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) => InkWell( + onTap: () { + _selectItem(f); + }, + child: Padding( + padding: widget.dialogItemPadding, + child: _buildOption(f), + ))), + const Divider(), + ], + ), + if (filteredElements.isEmpty) + _buildEmptySearchWidget(context) + else + ...filteredElements.map((e) => InkWell( + onTap: () { + _selectItem(e); + }, + child: Padding( + padding: widget.dialogItemPadding, + child: _buildOption(e), + ))), + ], + ), + ), + ], ), - ], - ), - ), - ); + ), + ); + } Widget _buildOption(CountryCode e) { return SizedBox(