Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active February 16, 2024 04:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slightfoot/59fa0101d0c51731fa4acf3816c2b72d to your computer and use it in GitHub Desktop.
Save slightfoot/59fa0101d0c51731fa4acf3816c2b72d to your computer and use it in GitHub Desktop.
Custom Animated Bottom Sheet - by Simon Lightfoot - Humpday Q&A :: 14th February 2024 #Flutter #Dart - https://www.youtube.com/watch?v=1qZxFApx1xs
// MIT License
//
// Copyright (c) 2023 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'blah.dart';
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: Home(),
));
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Material(
color: Colors.blue,
child: Stack(
alignment: Alignment.center,
children: [
Placeholder(color: Colors.blue.shade800),
ElevatedButton(
onPressed: () {
ExampleInfoBottomSheet.show(context);
},
child: const Text('Show Sheet'),
),
],
),
);
}
}
class ExampleInfoBottomSheet extends StatelessWidget {
const ExampleInfoBottomSheet._();
static Future<void> show(BuildContext context) async {
await Navigator.of(context).push(
ScrollableBottomSheet.route<void>(
sheetAlignment: Alignment.topCenter,
openFraction: 0.5,
(BuildContext context) {
return const ExampleInfoBottomSheet._();
},
),
);
}
@override
Widget build(BuildContext context) {
return const SizedBox(
// width: 280.0,
height: 800.0,
child: Placeholder(color: Colors.red),
);
}
}
// -----------------------------------------------------------------------------
const defaultBottomSheetDecoration = ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24.0),
topRight: Radius.circular(24.0),
),
),
shadows: [
BoxShadow(
color: Colors.black,
spreadRadius: -4.0,
blurRadius: 14.0,
),
],
);
class ScrollableBottomSheet extends StatefulWidget {
const ScrollableBottomSheet({
super.key,
required this.openFraction,
required this.decoration,
required this.sheetAlignment,
required this.child,
});
final double openFraction;
final Decoration decoration;
final Alignment sheetAlignment;
final Widget child;
static Route<T> route<T>(
WidgetBuilder builder, {
double openFraction = 0.3,
Decoration decoration = defaultBottomSheetDecoration,
Alignment sheetAlignment = Alignment.topCenter,
}) {
return PageRouteBuilder(
opaque: false,
transitionDuration: const Duration(milliseconds: 350),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return ScrollableBottomSheet(
openFraction: openFraction,
decoration: decoration,
sheetAlignment: sheetAlignment,
child: builder(context),
);
},
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
final color = ColorTween(
begin: Colors.transparent,
end: Colors.black54,
);
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return ColoredBox(
color: color.evaluate(animation)!,
child: child,
);
},
child: child,
);
},
);
}
@override
State<ScrollableBottomSheet> createState() => _ScrollableBottomSheetState();
}
class _ScrollableBottomSheetState extends State<ScrollableBottomSheet> {
late final ScrollController _controller;
bool _shouldDismiss = false;
@override
void initState() {
super.initState();
_controller = ScrollController();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final routeAnimation = CurvedAnimation(
parent: ModalRoute.of(context)!.animation!,
curve: Curves.easeInOut,
);
final decoration = widget.decoration;
ShapeBorder? shape;
if (decoration is ShapeDecoration) {
shape = decoration.shape;
} else if (decoration is BoxDecoration) {
shape = const Border();
}
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification is OverscrollNotification) {
_shouldDismiss = (notification.overscroll < 0);
} else if (notification is ScrollUpdateNotification) {
if (_shouldDismiss && (notification.scrollDelta ?? 0) > 0) {
_shouldDismiss = false;
}
} else if (notification is ScrollEndNotification) {
if (_shouldDismiss) {
Navigator.of(context).pop();
}
}
// Return true to cancel the notification bubbling.
return true;
},
child: SingleChildScrollView(
controller: _controller,
physics: const ClampingScrollPhysics(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => Navigator.of(context).pop(),
onVerticalDragStart: (_) {
// Absorb vertical drag events to prevent them
// from reaching the parent [SingleChildScrollView]
},
child: AnimatedBuilder(
animation: routeAnimation,
builder: (BuildContext context, Widget? child) {
final fraction =
(widget.openFraction * routeAnimation.value)
.clamp(0.0, 1.0);
return SizedBox(
height: constraints.maxHeight * (1.0 - fraction),
);
},
),
),
Align(
alignment: widget.sheetAlignment,
child: ConstrainedBox(
constraints: constraints.copyWith(
minWidth: 0.0,
minHeight: 0.0,
),
child: DecoratedBox(
decoration: widget.decoration,
child: ClipPath(
clipBehavior: Clip.antiAlias,
clipper: shape != null
? ShapeBorderClipper(shape: shape)
: null,
child: widget.child,
),
),
),
),
],
),
),
);
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment