Last active
July 21, 2024 10:17
-
-
Save rohan20/869492358cbb15311538f069a0c749af to your computer and use it in GitHub Desktop.
Flutter Google Maps Bottom Sheet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
class GoogleMapsClonePage extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Stack( | |
children: <Widget>[ | |
CustomGoogleMap(), | |
CustomHeader(), | |
DraggableScrollableSheet( | |
initialChildSize: 0.30, | |
minChildSize: 0.15, | |
builder: (BuildContext context, ScrollController scrollController) { | |
return SingleChildScrollView( | |
controller: scrollController, | |
child: CustomScrollViewContent(), | |
); | |
}, | |
), | |
], | |
), | |
); | |
} | |
} | |
/// Google Map in the background | |
class CustomGoogleMap extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
color: Colors.blue[50], | |
child: Center(child: Text("Google Map here")), | |
); | |
} | |
} | |
/// Search text field plus the horizontally scrolling categories below the text field | |
class CustomHeader extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: <Widget>[ | |
CustomSearchContainer(), | |
CustomSearchCategories(), | |
], | |
); | |
} | |
} | |
class CustomSearchContainer extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.fromLTRB(16, 40, 16, 8), //adjust "40" according to the status bar size | |
child: Container( | |
height: 50, | |
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(6)), | |
child: Row( | |
children: <Widget>[ | |
CustomTextField(), | |
Icon(Icons.mic), | |
SizedBox(width: 16), | |
CustomUserAvatar(), | |
SizedBox(width: 16), | |
], | |
), | |
), | |
); | |
} | |
} | |
class CustomTextField extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Expanded( | |
child: TextFormField( | |
maxLines: 1, | |
decoration: InputDecoration( | |
contentPadding: const EdgeInsets.all(16), | |
hintText: "Search here", | |
border: InputBorder.none, | |
), | |
), | |
); | |
} | |
} | |
class CustomUserAvatar extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
height: 32, | |
width: 32, | |
decoration: BoxDecoration(color: Colors.grey[500], borderRadius: BorderRadius.circular(16)), | |
); | |
} | |
} | |
class CustomSearchCategories extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return SingleChildScrollView( | |
scrollDirection: Axis.horizontal, | |
child: Row( | |
children: <Widget>[ | |
SizedBox(width: 16), | |
CustomCategoryChip(Icons.fastfood, "Takeout"), | |
SizedBox(width: 12), | |
CustomCategoryChip(Icons.directions_bike, "Delivery"), | |
SizedBox(width: 12), | |
CustomCategoryChip(Icons.local_gas_station, "Gas"), | |
SizedBox(width: 12), | |
CustomCategoryChip(Icons.shopping_cart, "Groceries"), | |
SizedBox(width: 12), | |
CustomCategoryChip(Icons.local_pharmacy, "Pharmacies"), | |
SizedBox(width: 12), | |
], | |
), | |
); | |
} | |
} | |
class CustomCategoryChip extends StatelessWidget { | |
final IconData iconData; | |
final String title; | |
CustomCategoryChip(this.iconData, this.title); | |
@override | |
Widget build(BuildContext context) { | |
return Chip( | |
label: Row( | |
children: <Widget>[Icon(iconData, size: 16), SizedBox(width: 8), Text(title)], | |
), | |
backgroundColor: Colors.grey[50], | |
); | |
} | |
} | |
/// Content of the DraggableBottomSheet's child SingleChildScrollView | |
class CustomScrollViewContent extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Card( | |
elevation: 12.0, | |
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), | |
margin: const EdgeInsets.all(0), | |
child: Container( | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(24), | |
), | |
child: CustomInnerContent(), | |
), | |
); | |
} | |
} | |
class CustomInnerContent extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Column( | |
children: <Widget>[ | |
SizedBox(height: 12), | |
CustomDraggingHandle(), | |
SizedBox(height: 16), | |
CustomExploreBerlin(), | |
SizedBox(height: 16), | |
CustomHorizontallyScrollingRestaurants(), | |
SizedBox(height: 24), | |
CustomFeaturedListsText(), | |
SizedBox(height: 16), | |
CustomFeaturedItemsGrid(), | |
SizedBox(height: 24), | |
CustomRecentPhotosText(), | |
SizedBox(height: 16), | |
CustomRecentPhotoLarge(), | |
SizedBox(height: 12), | |
CustomRecentPhotosSmall(), | |
SizedBox(height: 16), | |
], | |
); | |
} | |
} | |
class CustomDraggingHandle extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
height: 5, | |
width: 30, | |
decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(16)), | |
); | |
} | |
} | |
class CustomExploreBerlin extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
Text("Explore Berlin", style: TextStyle(fontSize: 22, color: Colors.black45)), | |
SizedBox(width: 8), | |
Container( | |
height: 24, | |
width: 24, | |
child: Icon(Icons.arrow_forward_ios, size: 12, color: Colors.black54), | |
decoration: BoxDecoration(color: Colors.grey[200], borderRadius: BorderRadius.circular(16)), | |
), | |
], | |
); | |
} | |
} | |
class CustomHorizontallyScrollingRestaurants extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.only(left: 16), | |
child: SingleChildScrollView( | |
scrollDirection: Axis.horizontal, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
CustomRestaurantCategory(), | |
SizedBox(width: 12), | |
CustomRestaurantCategory(), | |
SizedBox(width: 12), | |
CustomRestaurantCategory(), | |
SizedBox(width: 12), | |
CustomRestaurantCategory(), | |
SizedBox(width: 12), | |
], | |
), | |
), | |
); | |
} | |
} | |
class CustomFeaturedListsText extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.only(left: 16), | |
//only to left align the text | |
child: Row( | |
children: <Widget>[Text("Featured Lists", style: TextStyle(fontSize: 14))], | |
), | |
); | |
} | |
} | |
class CustomFeaturedItemsGrid extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 16), | |
child: GridView.count( | |
//to avoid scrolling conflict with the dragging sheet | |
physics: NeverScrollableScrollPhysics(), | |
padding: const EdgeInsets.all(0), | |
crossAxisCount: 2, | |
mainAxisSpacing: 12, | |
crossAxisSpacing: 12, | |
shrinkWrap: true, | |
children: <Widget>[ | |
CustomFeaturedItem(), | |
CustomFeaturedItem(), | |
CustomFeaturedItem(), | |
CustomFeaturedItem(), | |
], | |
), | |
); | |
} | |
} | |
class CustomRecentPhotosText extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.only(left: 16), | |
child: Row( | |
children: <Widget>[ | |
Text("Recent Photos", style: TextStyle(fontSize: 14)), | |
], | |
), | |
); | |
} | |
} | |
class CustomRecentPhotoLarge extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 16), | |
child: CustomFeaturedItem(), | |
); | |
} | |
} | |
class CustomRecentPhotosSmall extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return CustomFeaturedItemsGrid(); | |
} | |
} | |
class CustomRestaurantCategory extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
height: 100, | |
width: 100, | |
decoration: BoxDecoration( | |
color: Colors.grey[500], | |
borderRadius: BorderRadius.circular(8), | |
), | |
); | |
} | |
} | |
class CustomFeaturedItem extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
height: 200, | |
decoration: BoxDecoration( | |
color: Colors.grey[500], | |
borderRadius: BorderRadius.circular(8), | |
), | |
); | |
} | |
} |
I found a solution but now my maps widget can't get reached:
LayoutBuilder(builder: (context, constraints) {
return SingleChildScrollView(
reverse: true,//to anchor it to the bottom so when my panel grow in size it expand from the drawer
child: Column(children: [
Container(
height: constraints.maxHeight - 67,//Create an empty box that stop the scroll at 67px of the drawer
),
Container(//My drawer
decoration: BoxDecoration(
color: Coolors.background,
borderRadius:
BorderRadius.vertical(top: Radius.circular(30))),
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
child: Panel(),
),
]),
);
}),
The only problem I have with this is the inability to Ignore the pointer on the void container because its handled by SingleChildScrollView
and thus my map below don't get any event. I tried using others widget but each one fail. Do you have an idea ?
class PanelDraggable extends StatefulWidget {
@override
_PanelDraggableState createState() => _PanelDraggableState();
}
class _PanelDraggableState extends State<PanelDraggable> {
ScrollController scrollController = ScrollController();
double containerSize =
67; //my px value of the panel that need to be shown when closed
double margin;
double unlockScroll = 0;
double previousOffset;
bool lock = false;
@override
void initState() {
super.initState();
margin = containerSize;
WidgetsBinding.instance.addPostFrameCallback((_) => scrollController.jumpTo(
scrollController
.position.maxScrollExtent)); //put the panel in closed position
}
Future<void> waitAndUpdate() async {
//Wait for the ScrollPhysics to stop before updating the scrollarea
//(I maybe need to change the scrollphysics's speed if some users play around quickly)
if (lock) return;
lock = true;
Future.doWhile(() async {
if (previousOffset == scrollController.offset) {
updateScrollArea(null);
lock = false;
return false;
}
previousOffset = scrollController.offset;
await Future.delayed(Duration(milliseconds: 40));
return true;
});
}
void updateScrollArea(double dimension) {
if (dimension != null) {
//print("child's dimension augmented");
setState(() {
containerSize = dimension - 1;
unlockScroll = 1;
});
} else {
//Lower the view port
setState(() => containerSize = margin);
//callback after the frame built
WidgetsBinding.instance.addPostFrameCallback((_) => setState(() {
containerSize = scrollController.position.maxScrollExtent -
scrollController.offset +
margin;
//If it reached max (can't be scrolled since its useless), add 1px and update
if (containerSize ==
scrollController.position.maxScrollExtent + margin) {
unlockScroll = 1;
containerSize =
scrollController.position.maxScrollExtent + margin - 1;
}
print("container size = $containerSize");
}));
}
}
void notified() {
//Called by the "pause" button
//print("viewport $scrollController.position.viewportDimension");
//print("capacuty to scroll $scrollController.position.maxScrollExtent");
updateScrollArea((scrollController.position.maxScrollExtent > 0)
? scrollController.position.maxScrollExtent +
scrollController.position.viewportDimension
: null);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return Listener(
onPointerUp: (_) => waitAndUpdate(),
onPointerDown: (_) => setState(() => containerSize = margin),
child: Container(
height: containerSize,
child: SingleChildScrollView(
physics: NoVelocityScrollPhysics(),
controller: scrollController,
clipBehavior: Clip.none,
reverse: true,
child: Column(
children: [
Container(height: unlockScroll),
Container(
//My drawer
decoration: BoxDecoration(
color: Coolors.background,
borderRadius:
BorderRadius.vertical(top: Radius.circular(30))),
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 10),
child: Panel(notifySize: notified),
),
],
),
),
),
);
});
}
}
This class sit in a stack with the alignment settings set to bottomCenter
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi thank you for your example, however I'm having trouble using it with a small widget as
CustomScrollViewContent
, the sheet can be scrolled all the way to the top while I'm trying to keep it at the bottom, do you have any solution ? Thank you again.