Skip to content

Drag sheet #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class HomePage extends StatelessWidget {
),
),
),
config: DragConfig(),
);
},
child: const Text('Show country picker'),
Expand Down
4 changes: 4 additions & 0 deletions lib/country_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ 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';
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.
///
Expand Down Expand Up @@ -68,6 +70,7 @@ void showCountryPicker({
bool useRootNavigator = false,
bool moveAlongWithKeyboard = false,
Widget header = const SizedBox.shrink(),
DragConfig? config,
}) {
assert(
exclude == null || countryFilter == null,
Expand All @@ -90,5 +93,6 @@ void showCountryPicker({
useRootNavigator: useRootNavigator,
moveAlongWithKeyboard: moveAlongWithKeyboard,
header: header,
config: config,
);
}
95 changes: 60 additions & 35 deletions lib/src/country_list_bottom_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -42,6 +44,7 @@ void showCountryListBottomSheet({
moveAlongWithKeyboard,
customFlagBuilder,
header,
config: config,
),
).whenComplete(() {
if (onClosed != null) onClosed();
Expand All @@ -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 ??
Expand All @@ -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);
},
);
}
116 changes: 116 additions & 0 deletions lib/src/drag_config.dart
Original file line number Diff line number Diff line change
@@ -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<double>? 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',
);
}