I have noticed if maxChildSize and minChildSize has the same value then swiping up on the contents of DraggableScrollableSheet closes the bottom sheet.
The expected behaviour is to scroll down the ListView provided in the builder argument of the DraggableScrollableSheet.
I believe the expected behaviour should be followed even when both maxChildSize and minChildSize are the same.
Am I missing something?
Here is the minimum reproducible code:
import 'package:flutter/material.dart';
class DraggableBottomSheetTest extends StatelessWidget {
const DraggableBottomSheetTest({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return DraggableScrollableSheet(
maxChildSize: 0.5,
minChildSize: 0.5,
initialChildSize: 0.5,
expand: false,
builder: (context, controller) => ListView.builder(
controller: controller,
itemCount: 50,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
),
);
},
);
},
child: Text('Show Modal BottomSheet'),
),
),
);
}
}
import 'package:flutter/material.dart';
class DraggableBottomSheetTest extends StatelessWidget {
const DraggableBottomSheetTest({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: ElevatedButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true, // set this to true
builder: (context) {
return DraggableScrollableSheet(
maxChildSize: 0.5,
minChildSize: 0.5,
initialChildSize: 0.5,
expand: false,
builder: (context, controller) => ListView.builder(
controller: controller,
itemCount: 50,
itemBuilder: (context, index) => ListTile(
title: Text('Item $index'),
),
),
);
},
);
},
child: Text('Show Modal BottomSheet'),
),
),
);
}
}
I want to see the bottom textfield in chat screen when keyboard is active
my structure:
Column(
children: [
ListView.builder(
itemCount: controller.messages.length,
itemBuilder: (BuildContext context, int index) {
final item = controller.messages[index];
return Message(
msg: item.message.toString(),
direction: index % 2 == 0 ? 'left' : 'right',
);
},
),
Row(
children: [
Icon(Icons.add),
TextInput(
controller: controller.textController,
hint: 'Write Here',
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(35))),
),
IconButton(
onPressed: () => controller.sendMessage(),
icon: Icon(Icons.send, color: kPrimaryColor),
)
],
),
],
),
ScreenUtilInit(
designSize: Size(375, 867),
minTextAdapt: true,
rebuildFactor: RebuildFactors.all,
builder: (BuildContext context, Widget? child) => MaterialApp(
debugShowCheckedModeBanner: false,
title: 'title',
),
);
if you are using latest version of ScreenUtilInit then make sure ScreenUtilInit is parent of your MaterialApp
ScreenUtilInit(
designSize: Size(yourWidth, yourHeight),
builder: (BuildContext context, Widget? child){
return MaterialApp()
},
);
Like with awesome bottom sheet of Telegram i would like to have that in our application, i tired to implementing that, but unfortunately i don't have more experience about that and i can implement below code like with that which it doesn't have more feature.
here i attached some Gif images such as what features i want to have them on our application:
opening animation:
switch between tabs animation:
expand and collapsing animation:
preventing closing bottom sheet during dragging down:
Full code:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isLong = false;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sample'),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
onPressed: _onPressed,
child: Text('open'),
)
],
),
),
);
}
_onPressed() {
setImages();
Navigator.of(context).push(TransparentRoute(builder: (context) => NewWidget(images)));
}
List<String> images =[];
void setImages() {
images = List.generate(
isLong ? 5 : 25,
(_) => 'http://placeimg.com/100/100/any',
);
}
}
class NewWidget extends StatefulWidget {
const NewWidget(this.images, {Key? key}) : super(key: key);
final List<String> images;
#override
_NewWidgetState createState() => _NewWidgetState();
}
class _NewWidgetState extends State<NewWidget> {
bool isBig = false;
bool isBouncing = true;
final double topOffset = 200;
final double miniHandleHeight = 20;
double safeAreaPadding = 0;
double? startBigAnimationOffset;
double? startStickyOffset;
double backgroundHeight = 0;
double get savedAppBarHeight => safeAreaPadding + kToolbarHeight;
final ScrollController controller = ScrollController();
#override
void initState() {
WidgetsBinding.instance!.addPostFrameCallback(_afterLayout);
super.initState();
}
void _afterLayout(_) {
var media = MediaQuery.of(context);
safeAreaPadding = media.padding.top;
startBigAnimationOffset = topOffset - savedAppBarHeight;
startStickyOffset = (startBigAnimationOffset! + 10);
backgroundHeight = media.size.height - miniHandleHeight - topOffset;
controller.addListener(scrollListener);
}
void scrollListener() {
var offset = controller.offset;
if (offset < 0) {
goOut();
} else if (offset < startBigAnimationOffset! && isBig || offset < startStickyOffset!) {
setState(() {
isBig = false;
});
} else if (offset > startBigAnimationOffset! && !isBig || offset > startStickyOffset!) {
setState(() {
isBig = true;
});
}
if (offset < topOffset && !isBouncing) {
setState(() => isBouncing = true);
} else if (offset > topOffset && isBouncing) {
setState(() => isBouncing = false);
}
}
void goOut() {
controller.dispose();
Navigator.of(context).pop();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
backgroundColor: /*isStack ? Colors.white : */Colors.transparent,
body: Stack(
children: [
ListView(
padding: EdgeInsets.zero,
physics: isBouncing ? BouncingScrollPhysics() : ClampingScrollPhysics(),
controller: controller,
children: <Widget>[
Container(
alignment: Alignment.bottomCenter,
height: topOffset,
child: TweenAnimationBuilder(
tween: Tween(begin: 0.0, end: isBig ? 1.0 : 0.0),
duration: Duration(milliseconds: 200),
child: Align(
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(top: 10),
child: Container(
height: 5,
width: 60,
),
),
),
builder: (_, number, child) {
return Container(
height: savedAppBarHeight * (number as double) + miniHandleHeight,
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular((1 - number) * 50),
),
color: Colors.white,
),
child: Opacity(opacity: 1 - (number), child: child),
);
}),
),
Container(
padding: EdgeInsets.all(5),
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height - savedAppBarHeight,
),
decoration: BoxDecoration(
color: Colors.white,
),
child: getGrid(),
)
],
)
],
),
),
);
}
Widget getGrid() {
return GridView.count(
crossAxisSpacing: 10,
mainAxisSpacing: 10,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
crossAxisCount: 3,
children: widget.images.map((url) {
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueAccent,
),
),
child: Image(
image: NetworkImage(url),
),
);
}).toList(),
);
}
}
class TransparentRoute extends PageRoute<void> {
TransparentRoute({
required this.builder,
RouteSettings? settings,
}) : super(settings: settings, fullscreenDialog: false);
final WidgetBuilder builder;
#override
bool get opaque => false;
#override
Color? get barrierColor => null;
#override
String? get barrierLabel => null;
#override
bool get maintainState => true;
#override
Duration get transitionDuration => Duration(milliseconds: 350);
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final result = builder(context);
return Container(
color: Colors.black.withOpacity(0.5),
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: animation,
curve: Curves.easeIn,
)),
child: result,
),
);
}
}
Here is the outline, with which you can try to implement it. If you feel difficulty, feel free to ask for more suggestions. IMHO, this is not very challenging to implement (but of course need some time to adjust the fine details into what you want).
Firstly, you mentioned animations many times. Have a look at the official guide here: https://flutter.dev/docs/development/ui/animation. For example, if we want to implement the "opening animation" and the "switch between tabs animation" (and other similar things), we want a Container (or other widgets) to have a size changing with time as the animation goes. There are many ways to implement this, as is suggested in the link provided above. For example, we may use AnimatedContainer directly - it even has a video explaining this here. You can also use explicit animations or other things.
Secondly, as for the animating icons, you may use Lottie https://lottiefiles.com/ to create whatever complex animation as you like. Its flutter plugin is here: https://pub.dev/packages/lottie.
Thirdly, as for "preventing closing bottom sheet during dragging down", I will suggest the following: ListView(children: [Container(height:300,color:Colors.black), ...your_real_children...]). Then when dragging down it will not be closed, but only show a header of at most 300 height.
Lastly, you are not restricted to use showModalBottomSheet or things like that. Indeed, when using flutter, you are completely free. So you can just do Navigator.push and draw whatever widget you like without the constraints that modal bottom sheet gives you. Personally, I suggest that you first start with showModalBottomSheet, then when it does not satisfy your needs, you just directly copy its source code and do whatever modifications as you needed. In short, use something in Flutter, and when you need more control, copy the source code and modify it.
You can be helped from this link https://pub.dev/packages/modal_bottom_sheet
import 'package:example/modals/circular_modal.dart';
import 'package:example/web_frame.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'modals/floating_modal.dart';
import 'modals/modal_complex_all.dart';
import 'modals/modal_fit.dart';
import 'modals/modal_inside_modal.dart';
import 'modals/modal_will_scope.dart';
import 'modals/modal_with_navigator.dart';
import 'modals/modal_with_nested_scroll.dart';
import 'modals/modal_with_scroll.dart';
import 'modals/modal_with_page_view.dart';
import 'examples/cupertino_share.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
title: 'BottomSheet Modals',
builder: (context, Widget? child) => WebFrame(
child: child!,
),
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialWithModalsPageRoute(
builder: (_) => MyHomePage(title: 'Flutter Demo Home Page'),
settings: settings);
}
return MaterialPageRoute(
builder: (context) => Scaffold(
body: CupertinoScaffold(
body: Builder(
builder: (context) => CupertinoPageScaffold(
backgroundColor: Colors.white,
navigationBar: CupertinoNavigationBar(
transitionBetweenRoutes: false,
middle: Text('Normal Navigation Presentation'),
trailing: GestureDetector(
child: Icon(Icons.arrow_upward),
onTap: () => CupertinoScaffold
.showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Stack(
children: <Widget>[
ModalWithScroll(),
Positioned(
height: 40,
left: 40,
right: 40,
bottom: 20,
child: MaterialButton(
onPressed: () => Navigator.of(
context)
.popUntil((route) =>
route.settings.name == '/'),
child: Text('Pop back home'),
),
)
],
),
)),
),
child: Center(child: Container()),
),
),
),
),
settings: settings);
},
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: CupertinoPageScaffold(
backgroundColor: Colors.white,
navigationBar: CupertinoNavigationBar(
transitionBetweenRoutes: false,
middle: Text('iOS13 Modal Presentation'),
trailing: GestureDetector(
child: Icon(Icons.arrow_forward),
onTap: () => Navigator.of(context).pushNamed('ss'),
),
),
child: SizedBox.expand(
child: SingleChildScrollView(
primary: true,
child: SafeArea(
bottom: false,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text('Cupertino Photo Share Example'),
onTap: () => Navigator.of(context).push(
MaterialWithModalsPageRoute(
builder: (context) => CupertinoSharePage()))),
section('STYLES'),
ListTile(
title: Text('Material fit'),
onTap: () => showMaterialModalBottomSheet(
expand: false,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalFit(),
)),
ListTile(
title: Text('Bar Modal'),
onTap: () => showBarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalInsideModal(),
)),
ListTile(
title: Text('Avatar Modal'),
onTap: () => showAvatarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalInsideModal(),
)),
ListTile(
title: Text('Float Modal'),
onTap: () => showFloatingModalBottomSheet(
context: context,
builder: (context) => ModalFit(),
)),
ListTile(
title: Text('Cupertino Modal fit'),
onTap: () => showCupertinoModalBottomSheet(
expand: false,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalFit(),
)),
section('COMPLEX CASES'),
ListTile(
title: Text('Cupertino Small Modal forced to expand'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalFit(),
)),
ListTile(
title: Text('Reverse list'),
onTap: () => showBarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) =>
ModalInsideModal(reverse: true),
)),
ListTile(
title: Text('Cupertino Modal inside modal'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalInsideModal(),
)),
ListTile(
title: Text('Cupertino Modal with inside navigation'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalWithNavigator(),
)),
ListTile(
title:
Text('Cupertino Navigator + Scroll + WillPopScope'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ComplexModal(),
)),
ListTile(
title: Text('Modal with WillPopScope'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ModalWillScope(),
)),
ListTile(
title: Text('Modal with Nested Scroll'),
onTap: () => showCupertinoModalBottomSheet(
expand: true,
context: context,
builder: (context) => NestedScrollModal(),
)),
ListTile(
title: Text('Modal with PageView'),
onTap: () => showBarModalBottomSheet(
expand: true,
context: context,
builder: (context) => ModalWithPageView(),
)),
SizedBox(
height: 60,
)
],
),
),
),
),
),
),
);
}
Widget section(String title) {
return Padding(
padding: EdgeInsets.fromLTRB(16, 20, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.caption,
));
}
}
another helpful link [https://pub.dev/packages/draggable_bottom_sheet][2]
I would like to have the iOS-Modal-Transition where the new screen animates from the bottom and the old screen is being pushed behind. I found this very promising package:
modal_bottom_sheet
This is the function I am using to show the modal:
showCupertinoModalBottomSheet(
expand: true,
context: context,
builder: (context) => Container(
color: AppColors.blue,
),
);
However this is not working a 100% correctly as the view behind is not being pushed in the back.
What am I missing here? Let me know if anything is unclear!
Here is some more of my code:
This is my whole page, from where I would like to have the transition:
class _MonthPageState extends State<MonthPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.secondary,
body: SafeArea(
child: Stack(
children: [
...
Positioned(
bottom: 10,
right: 20,
child: Hero(
tag: widget.month.name + 'icon',
child: AddButton(
onTapped: () {
showCupertinoModalBottomSheet(
expand: true,
context: context,
builder: (context) => Container(
color: AppColors.blue,
),
);
},
),
),
),
],
),
),
);
}
And this is my Router:
class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialWithModalsPageRoute(
builder: (context) => HomePage(),
);
case '/month':
final Month month = settings.arguments as Month;
return _buildTransitionToMonthPage(month);
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('No route defined for ${settings.name}'),
),
),
);
}
}
static PageRouteBuilder _buildTransitionToMonthPage(Month month) {
return PageRouteBuilder(
transitionDuration: Duration(milliseconds: 450),
reverseTransitionDuration: Duration(milliseconds: 450),
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return MonthPage(
month: month,
);
},
transitionsBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: animation, child: child);
},
);
}
}
In order to get that pushing behind animation, you need to use CupertinoScaffold alongside with CupertinoPageScaffold, e.g.
#override
Widget build(BuildContext context) {
return CupertinoScaffold(
transitionBackgroundColor: Colors.white,
body: Builder(
builder: (context) => CupertinoPageScaffold(
backgroundColor: Colors.white,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: ElevatedButton(
child: Text('show modal'),
onPressed: () =>
CupertinoScaffold.showCupertinoModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.white,
builder: (context) => Container(
color: Colors.white,
child: Center(
child: ElevatedButton(
onPressed: () => Navigator.of(context)
.popUntil((route) =>
route.settings.name == '/'),
child: Text('return home'),
),
)),
)),
),
],
),
),
),
);
}