diff --git a/example/lib/main.dart b/example/lib/main.dart index acc4b92..638499c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -118,6 +118,7 @@ class HomePage extends StatelessWidget { ), ), ), + config: DragConfig(), ); }, child: const Text('Show country picker'), diff --git a/lib/country_picker.dart b/lib/country_picker.dart index fd9dce4..8fbbe0d 100644 --- a/lib/country_picker.dart +++ b/lib/country_picker.dart @@ -7,6 +7,7 @@ import 'src/country.dart'; import 'src/country_list_bottom_sheet.dart'; import 'src/country_list_theme_data.dart'; import 'src/country_list_view.dart'; +import 'src/drag_config.dart'; export 'src/country.dart'; export 'src/country_list_theme_data.dart'; @@ -14,6 +15,7 @@ export 'src/country_list_view.dart' show CustomFlagBuilder; export 'src/country_localizations.dart'; export 'src/country_parser.dart'; export 'src/country_service.dart'; +export 'src/drag_config.dart'; /// Shows a bottom sheet containing a list of countries to select one. /// @@ -68,6 +70,7 @@ void showCountryPicker({ bool useRootNavigator = false, bool moveAlongWithKeyboard = false, Widget header = const SizedBox.shrink(), + DragConfig? config, }) { assert( exclude == null || countryFilter == null, @@ -90,5 +93,6 @@ void showCountryPicker({ useRootNavigator: useRootNavigator, moveAlongWithKeyboard: moveAlongWithKeyboard, header: header, + config: config, ); } diff --git a/lib/src/country_list_bottom_sheet.dart b/lib/src/country_list_bottom_sheet.dart index 3bfd06d..3b83f05 100644 --- a/lib/src/country_list_bottom_sheet.dart +++ b/lib/src/country_list_bottom_sheet.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'country.dart'; import 'country_list_theme_data.dart'; import 'country_list_view.dart'; +import 'drag_config.dart'; void showCountryListBottomSheet({ required BuildContext context, @@ -21,12 +22,13 @@ void showCountryListBottomSheet({ bool useRootNavigator = false, bool moveAlongWithKeyboard = false, Widget header = const SizedBox.shrink(), + DragConfig? config, }) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, - useSafeArea: useSafeArea, + useSafeArea: config == null ? useSafeArea : true, useRootNavigator: useRootNavigator, builder: (context) => _builder( context, @@ -42,6 +44,7 @@ void showCountryListBottomSheet({ moveAlongWithKeyboard, customFlagBuilder, header, + config: config, ), ).whenComplete(() { if (onClosed != null) onClosed(); @@ -61,8 +64,9 @@ Widget _builder( bool showSearch, bool moveAlongWithKeyboard, CustomFlagBuilder? customFlagBuilder, - Widget header, -) { + Widget header, { + DragConfig? config, +}) { final device = MediaQuery.of(context).size.height; final statusBarHeight = MediaQuery.of(context).padding.top; final height = countryListTheme?.bottomSheetHeight ?? @@ -86,41 +90,62 @@ Widget _builder( topRight: Radius.circular(40.0), ); - return Padding( - padding: moveAlongWithKeyboard - ? MediaQuery.of(context).viewInsets - : EdgeInsets.zero, - child: Container( - height: height, - width: width, - padding: countryListTheme?.padding, - margin: countryListTheme?.margin, - decoration: BoxDecoration( - color: _backgroundColor, - borderRadius: _borderRadius, - ), + Widget child({ScrollController? scrollController}) { + return SingleChildScrollView( + controller: scrollController, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - child: Column( - children: [ - header, - Flexible( - child: CountryListView( - onSelect: onSelect, - exclude: exclude, - favorite: favorite, - countryFilter: countryFilter, - showPhoneCode: showPhoneCode, - countryListTheme: countryListTheme, - searchAutofocus: searchAutofocus, - showWorldWide: showWorldWide, - showSearch: showSearch, - customFlagBuilder: customFlagBuilder, - ), + padding: moveAlongWithKeyboard + ? MediaQuery.of(context).viewInsets + : EdgeInsets.zero, + child: Container( + height: height, + width: width, + padding: countryListTheme?.padding, + margin: countryListTheme?.margin, + decoration: BoxDecoration( + color: _backgroundColor, + borderRadius: _borderRadius, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + child: Column( + children: [ + header, + Flexible( + child: CountryListView( + onSelect: onSelect, + exclude: exclude, + favorite: favorite, + countryFilter: countryFilter, + showPhoneCode: showPhoneCode, + countryListTheme: countryListTheme, + searchAutofocus: searchAutofocus, + showWorldWide: showWorldWide, + showSearch: showSearch, + customFlagBuilder: customFlagBuilder, + ), + ), + ], ), - ], + ), ), ), - ), + ); + } + + if(config == null) return child(); + + return DraggableScrollableSheet( + initialChildSize: config.initialChildSize, + minChildSize: config.minChildSize, + maxChildSize: config.maxChildSize, + expand: config.expand, + snap: config.snap, + snapSizes: config.snapSizes, + snapAnimationDuration: config.snapAnimationDuration, + controller: config.controller, + builder: (context, scrollController) { + return child(scrollController: scrollController); + }, ); } diff --git a/lib/src/drag_config.dart b/lib/src/drag_config.dart new file mode 100644 index 0000000..24a6cd3 --- /dev/null +++ b/lib/src/drag_config.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; + +/// A configuration object for customizing the behavior of a +/// [DraggableScrollableSheet]-based widget. +/// +/// This includes initial, minimum, and maximum sizes, snapping behavior, +/// animation control, and closing behavior. +/// SafeArea is used when [DragConfig] is used +class DragConfig { + /// The initial fractional value of the parent container's height to use when + /// displaying the widget. + /// + /// This value must be between [minChildSize] and [maxChildSize]. + /// + /// Rebuilding the sheet with a new [initialChildSize] will only move + /// the sheet to the new value if it has not yet been dragged since it + /// was first built or since the last call to [DraggableScrollableActuator.reset]. + /// + /// The default value is `0.5`. + final double initialChildSize; + + /// The minimum fractional value of the parent container's height to use when + /// displaying the widget. + /// + /// Must be less than or equal to [initialChildSize] and [maxChildSize]. + /// + /// The default value is `0.25`. + final double minChildSize; + + /// The maximum fractional value of the parent container's height to use when + /// displaying the widget. + /// + /// Must be greater than or equal to [initialChildSize] and [minChildSize]. + /// + /// The default value is `1.0`. + final double maxChildSize; + + /// Whether the widget should expand to fill the available space in its parent. + /// + /// Typically, this should be `true`. Set to `false` when the parent widget + /// (like [Center]) sizes this sheet based on its intrinsic size. + /// + /// The default value is `true`. + final bool expand; + + /// Whether the widget should snap between [snapSizes] when the user lifts + /// their finger after a drag. + /// + /// If the drag ends with a velocity, the sheet will snap in the direction + /// of that drag. Otherwise, it will snap to the nearest snap size. + /// + /// Programmatic movements (e.g. via [DraggableScrollableController.animateTo]) + /// do not use snapping. + /// + /// Enabling snapping during a rebuild triggers a snap unless the sheet + /// has not been dragged from its initial position. + final bool snap; + + /// A list of fractional sizes to snap to when [snap] is true. + /// + /// These values must be in increasing order and within the range of + /// [minChildSize] to [maxChildSize]. The min and max sizes are implicitly + /// included even if not specified. + /// + /// For example, `snapSizes: [.5]` causes the sheet to snap to + /// `[minChildSize, .5, maxChildSize]`. + /// + /// Changes to this list only take effect on rebuild. + final List? snapSizes; + + /// The duration to use for snap animations. + /// + /// If not set, the snap animation will use a duration based on distance + /// to the target and current velocity. + final Duration? snapAnimationDuration; + + /// A controller for programmatic control of the draggable sheet. + /// + /// Use this to animate or jump to specific positions. + final DraggableScrollableController? controller; + + /// Whether the sheet should trigger a close action when it reaches + /// [minChildSize]. + /// + /// This is typically interpreted by parent widgets that listen to + /// [DraggableScrollableNotification]s. + final bool shouldCloseOnMinExtent; + + /// Creates a [DragConfig] instance with custom sheet behavior. + /// + /// All size parameters must satisfy: + /// [minChildSize] ≤ [initialChildSize] ≤ [maxChildSize] + /// and all [snapSizes] (if provided) must be within that range. + DragConfig({ + this.initialChildSize = 0.5, + this.minChildSize = 0.25, + this.maxChildSize = 1.0, + this.expand = true, + this.snap = false, + this.snapSizes, + this.snapAnimationDuration, + this.controller, + this.shouldCloseOnMinExtent = true, + }) : assert(minChildSize <= initialChildSize, + 'initialChildSize must be >= minChildSize'), + assert(initialChildSize <= maxChildSize, + 'initialChildSize must be <= maxChildSize'), + assert(minChildSize <= maxChildSize, + 'minChildSize must be <= maxChildSize'), + assert( + snapSizes == null || + snapSizes.every( + (size) => size >= minChildSize && size <= maxChildSize), + 'All snapSizes must be within minChildSize and maxChildSize', + ); +}