Skip to content

Instantly share code, notes, and snippets.

@rohan20
Last active July 4, 2023 15:38
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save rohan20/869492358cbb15311538f069a0c749af to your computer and use it in GitHub Desktop.
Save rohan20/869492358cbb15311538f069a0c749af to your computer and use it in GitHub Desktop.
Flutter Google Maps Bottom Sheet
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),
),
);
}
}
@rohan20
Copy link
Author

rohan20 commented May 29, 2020

@RomanJos
Copy link

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.

@RomanJos
Copy link

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 ?

@RomanJos
Copy link

ezgif-4-cd00e1856f52
Succes !
Below is my code

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

@CMingTseng
Copy link

Dear Sir

if i use Stack+Positioned (or Stack+? )

i want resize Google map area like attach image

the top & bottom widget is fix Positioned

other empty area i want put re-size Google map

how to do ?

THX
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment