Skip to content

Commit

Permalink
feat: Settings now use Modal Sheets 🤩 (openfoodfacts#4307)
Browse files Browse the repository at this point in the history
  • Loading branch information
g123k authored Jul 22, 2023
1 parent 41e9191 commit ef15cab
Show file tree
Hide file tree
Showing 19 changed files with 862 additions and 418 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:smooth_app/generic_lib/bottom_sheets/smooth_draggable_bottom_sheet_route.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';

Future<T?> showSmoothModalSheet<T>({
required BuildContext context,
required WidgetBuilder builder,
double? minHeight,
}) {
return showModalBottomSheet<T>(
constraints:
minHeight != null ? BoxConstraints(minHeight: minHeight) : null,
isScrollControlled: minHeight != null,
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: ROUNDED_RADIUS),
),
builder: builder,
useSafeArea: true,
);
}

Future<T?> showSmoothDraggableModalSheet<T>({
required BuildContext context,
required SmoothModalSheetHeader header,

/// You must return a Sliver Widget
required WidgetBuilder bodyBuilder,
}) {
return showDraggableModalSheet<T>(
context: context,
borderRadius: const BorderRadius.vertical(top: ROUNDED_RADIUS),
headerBuilder: (_) => header,
headerHeight:
SmoothModalSheetHeader.computeHeight(context, header.closeButton),
bodyBuilder: bodyBuilder,
);
}

/// A non scrollable modal sheet
class SmoothModalSheet extends StatelessWidget {
const SmoothModalSheet({
required this.title,
required this.body,
this.closeButton = true,
this.bodyPadding,
this.closeButtonSemanticsOrder,
});

final String title;
final bool closeButton;
final double? closeButtonSemanticsOrder;
final Widget body;
final EdgeInsetsGeometry? bodyPadding;

@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: const BorderRadius.vertical(top: ROUNDED_RADIUS),
child: DecoratedBox(
decoration: const BoxDecoration(
borderRadius: BorderRadius.vertical(top: ROUNDED_RADIUS),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SmoothModalSheetHeader(
title: title,
closeButton: closeButton,
closeButtonSemanticsOrder: closeButtonSemanticsOrder,
),
Padding(
padding: bodyPadding ?? const EdgeInsets.all(MEDIUM_SPACE),
child: body,
),
],
)),
);
}
}

class SmoothModalSheetHeader extends StatelessWidget {
const SmoothModalSheetHeader({
required this.title,
this.closeButton = true,
this.closeButtonSemanticsOrder,
});

final String title;
final bool closeButton;
final double? closeButtonSemanticsOrder;

@override
Widget build(BuildContext context) {
final Color primaryColor = Theme.of(context).primaryColor;

return Container(
color: primaryColor.withOpacity(0.2),
padding: EdgeInsetsDirectional.only(
start: VERY_LARGE_SPACE,
top: VERY_SMALL_SPACE,
bottom: VERY_SMALL_SPACE,
end: VERY_LARGE_SPACE - (closeButton ? LARGE_SPACE : 0),
),
child: Row(
children: <Widget>[
Expanded(
child: Semantics(
sortKey: const OrdinalSortKey(1.0),
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
if (closeButton)
Semantics(
value: MaterialLocalizations.of(context).closeButtonTooltip,
button: true,
excludeSemantics: true,
onScrollDown: () {},
sortKey: OrdinalSortKey(closeButtonSemanticsOrder ?? 2.0),
child: Tooltip(
message: MaterialLocalizations.of(context).closeButtonTooltip,
enableFeedback: true,
child: InkWell(
onTap: () => Navigator.of(context).pop(),
customBorder: const CircleBorder(),
child: const Padding(
padding: EdgeInsets.all(MEDIUM_SPACE),
child: Icon(Icons.clear),
),
),
),
)
],
),
);
}

static double computeHeight(
BuildContext context,
bool hasCloseButton,
) {
double size = VERY_SMALL_SPACE * 2;

if (hasCloseButton == true) {
size += (MEDIUM_SPACE * 2) + (Theme.of(context).iconTheme.size ?? 20.0);
} else {
size += Theme.of(context).textTheme.titleLarge?.fontSize ?? 15.0;
}

return size;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class SmoothDraggableBottomSheet extends StatefulWidget {
const SmoothDraggableBottomSheet({
Key? key,
required this.headerBuilder,
required this.headerHeight,
required this.bodyBuilder,
this.initHeightFraction = 0.5,
this.maxHeightFraction = 1.0,
this.animationController,
this.borderRadius,
this.bottomSheetColor,
this.draggableScrollableController,
}) : assert(maxHeightFraction > 0.0 && maxHeightFraction <= 1.0),
super(key: key);

final double initHeightFraction;
final double maxHeightFraction;
final WidgetBuilder headerBuilder;
final double headerHeight;
final WidgetBuilder bodyBuilder;
final DraggableScrollableController? draggableScrollableController;
final AnimationController? animationController;
final BorderRadius? borderRadius;
final Color? bottomSheetColor;

@override
SmoothDraggableBottomSheetState createState() =>
SmoothDraggableBottomSheetState();
}

class SmoothDraggableBottomSheetState
extends State<SmoothDraggableBottomSheet> {
late final DraggableScrollableController _controller;

bool _isClosing = false;

@override
void initState() {
super.initState();
_controller =
widget.draggableScrollableController ?? DraggableScrollableController();
widget.animationController?.addStatusListener(_animationStatusListener);
}

@override
Widget build(BuildContext context) {
return NotificationListener<DraggableScrollableNotification>(
onNotification: _scrolling,
child: SafeArea(
child: DraggableScrollableSheet(
minChildSize: 0.0,
maxChildSize: widget.maxHeightFraction,
initialChildSize: widget.initHeightFraction,
snap: true,
controller: _controller,
builder: (BuildContext context, ScrollController controller) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: widget.borderRadius,
color: widget.bottomSheetColor ??
Theme.of(context).bottomSheetTheme.backgroundColor ??
Theme.of(context).scaffoldBackgroundColor,
),
child: Material(
type: MaterialType.transparency,
child: ClipRRect(
borderRadius: widget.borderRadius,
child: _SmoothDraggableContent(
bodyBuilder: widget.bodyBuilder,
headerBuilder: widget.headerBuilder,
headerHeight: widget.headerHeight,
currentExtent: _controller.isAttached
? _controller.size
: widget.initHeightFraction,
scrollController: controller,
cacheExtent: _calculateCacheExtent(
MediaQuery.of(context).viewInsets.bottom,
),
),
),
),
);
},
),
),
);
}

@override
void dispose() {
widget.animationController?.removeStatusListener(_animationStatusListener);
super.dispose();
}

// Method will be called when scrolling.
bool _scrolling(DraggableScrollableNotification notification) {
if (_isClosing) {
return false;
}

if (notification.extent <= 0.005) {
_isClosing = true;
Navigator.of(context).maybePop();
}

return false;
}

// Method that listens for changing AnimationStatus, to track the closing of
// the bottom sheet by clicking above it.
void _animationStatusListener(AnimationStatus status) {
if (status == AnimationStatus.reverse ||
status == AnimationStatus.dismissed) {
_isClosing = true;
}
}

double _calculateCacheExtent(double bottomInset) {
const double defaultExtent = RenderAbstractViewport.defaultCacheExtent;
if (bottomInset > defaultExtent) {
return bottomInset;
} else {
return defaultExtent;
}
}
}

class _SmoothDraggableContent extends StatefulWidget {
const _SmoothDraggableContent({
required this.currentExtent,
required this.scrollController,
required this.cacheExtent,
required this.headerHeight,
required this.headerBuilder,
required this.bodyBuilder,
Key? key,
}) : super(key: key);

final WidgetBuilder headerBuilder;
final double headerHeight;
final WidgetBuilder bodyBuilder;
final double currentExtent;
final ScrollController scrollController;
final double cacheExtent;

@override
State<_SmoothDraggableContent> createState() =>
_SmoothDraggableContentState();
}

class _SmoothDraggableContentState extends State<_SmoothDraggableContent> {
final GlobalKey<State<StatefulWidget>> _contentKey = GlobalKey();

@override
Widget build(BuildContext context) {
return Scrollbar(
child: CustomScrollView(
cacheExtent: widget.cacheExtent,
key: _contentKey,
controller: widget.scrollController,
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: _SliverHeader(
child: widget.headerBuilder(context),
height: widget.headerHeight,
),
),
widget.bodyBuilder(context),
],
),
);
}
}

/// A fixed header
class _SliverHeader extends SliverPersistentHeaderDelegate {
_SliverHeader({required this.child, required this.height})
: assert(height > 0.0);

final Widget child;
final double height;

@override
Widget build(BuildContext context, _, __) {
return child;
}

@override
double get maxExtent => height;

@override
double get minExtent => height;

@override
bool shouldRebuild(_SliverHeader oldDelegate) => oldDelegate.height != height;
}
Loading

0 comments on commit ef15cab

Please sign in to comment.