fujidaiti / smooth_sheets

Sheet widgets with smooth motion and great flexibility.
https://pub.dev/packages/smooth_sheets
MIT License
209 stars 21 forks source link

Support transparent space around sheet #76

Open naamapps opened 6 months ago

naamapps commented 6 months ago

Hi, While pushing a new navigation modal sheet, it's not taking SafeArea and bottom margin into consideration. Code:


class SheetShape extends StatelessWidget {
  final Widget child;
  const SheetShape({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Card(
        color: Theme.of(context).bottomSheetTheme.backgroundColor,
        elevation: 0,
        clipBehavior: Clip.antiAlias,
        margin: const EdgeInsets.only(left: 15, right: 15, bottom: 3),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(36),
        ),
        child: child,
      ),
    );
  }
}

class StaticSheetContent extends StatelessWidget {
  final bool includeShape;
  final Widget? header;
  final Widget body;
  final Widget? footer;
  const StaticSheetContent({
    super.key,
    required this.body,
    this.header,
    this.footer,
    this.includeShape = true,
  });

  @override
  Widget build(BuildContext context) {
    final content = Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (header != null) header!,
        Padding(
          padding: const EdgeInsets.symmetric(vertical: 10),
          child: body,
        ),
        if (footer != null) footer!,
      ],
    );

    if (includeShape) {
      return SheetShape(
        child: content,
      );
    }

    return content;
  }
}

final _transitionObserver = NavigationSheetTransitionObserver();

Future<T?> showOptionsSheet<T>({
  required List<SheetOptionProps> options,
}) async {
  final modalRoute = ModalSheetRoute<T>(
    transitionDuration: const Duration(milliseconds: 250),
    builder: (context) => SheetDismissible(
      child: NavigationSheet(
        transitionObserver: _transitionObserver,
        child: SheetShape(
          child: Navigator(
            observers: [_transitionObserver],
            onGenerateInitialRoutes: (navigator, initialRoute) {
              return [
                DraggableNavigationSheetRoute(
                  builder: (context) {
                    return StaticSheetContent(
                      includeShape: false,
                      header: SheetHeader(
                        title: 'options'.tr(),
                      ),
                      body: SheetOptionsList(
                        options: options,
                      ),
                    );
                  },
                ),
              ];
            },
          ),
        ),
      ),
    ),
  );
  return await Navigator.push<T>(rootContext, modalRoute);
}

Expected - There should be a gap from the bottom of the screen: flutter_09

Actual - The sheet is attached to the bottom of the screen: flutter_08

fujidaiti commented 6 months ago

Unfortunately, with the current version, adding invisible space around the sheet is a bit tricky. I hope the following steps will solve your problem:

  1. Remove the SheetShape.
  2. Move the SafeArea to out of the NavigationSheet from the SheetShape.
    SafeArea(child: NavigationSheet(...));
  3. To create the rounded corners, use Card, ClipRRect or something, not outside of the Navigator, but inside each page.
    ClipRRect(child: StaticSheetContent(...));

Here's a fork from the tutorial code of the NavigationSheet, adding extra space around the sheet.

Code ```dart import 'package:flutter/material.dart'; import 'package:smooth_sheets/smooth_sheets.dart'; void main() { runApp(const _ImperativeNavigationSheetExample()); } class _ImperativeNavigationSheetExample extends StatelessWidget { const _ImperativeNavigationSheetExample(); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: Stack( children: [ Placeholder(), _ExampleSheet(), ], ), ), ); } } // NavigationSheet requires a special NavigatorObserver in order to // smoothly change its extent during a route transition. final _transitionObserver = NavigationSheetTransitionObserver(); class _ExampleSheet extends StatelessWidget { const _ExampleSheet(); @override Widget build(BuildContext context) { // Create a navigator somehow that will be used for nested navigation in the sheet. final nestedNavigator = Navigator( // Do not forget to attach the observer! observers: [_transitionObserver], onGenerateInitialRoutes: (navigator, initialRoute) { return [ // Use DraggableNavigationSheetRoute for a draggable page. DraggableNavigationSheetRoute( builder: (context) { return const _DraggablePage(); }, transitionsBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( position: Tween(begin: const Offset(1, 0), end: Offset.zero) .animate(animation), child: child, ); }, ), ]; }, ); // Wrap the nested navigator in a NavigationSheet. return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: NavigationSheet( transitionObserver: _transitionObserver, child: nestedNavigator, ), ), ); } } class _DraggablePage extends StatelessWidget { const _DraggablePage(); void navigateToScrollablePage(BuildContext context) { // Use ScrollableNavigationSheetRoute for a scrollable page. final route = ScrollableNavigationSheetRoute( builder: (context) { return const _ScrollablePage(); }, // You may want to use a custom page transition. transitionsBuilder: (context, animation, secondaryAnimation, child) { return SlideTransition( position: Tween(begin: const Offset(1, 0), end: Offset.zero) .animate(animation), child: child, ); }, ); Navigator.push(context, route); } @override Widget build(BuildContext context) { final title = Text( 'Draggable Page', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.onSecondaryContainer, ), ); return LayoutBuilder( builder: (context, constraints) { return Container( width: constraints.maxWidth, height: constraints.maxHeight * 0.5, // Add rounded corners decoration: ShapeDecoration( color: Theme.of(context).colorScheme.secondaryContainer, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(16), ), ), ), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ title, const SizedBox(height: 32), TextButton( onPressed: () => navigateToScrollablePage(context), child: const Text('Next'), ), ], ), ); }, ); } } class _ScrollablePage extends StatelessWidget { const _ScrollablePage(); @override Widget build(BuildContext context) { final backgroundColor = Theme.of(context).colorScheme.secondaryContainer; return ClipRRect( // Add rounded corners borderRadius: BorderRadius.circular(16), child: Scaffold( backgroundColor: backgroundColor, appBar: AppBar( title: const Text('Scrollable Page'), backgroundColor: backgroundColor, ), body: ListView.builder( itemCount: 30, itemBuilder: (context, index) { return ListTile( title: Text('Item #$index'), ); }, ), ), ); } } ```

https://github.com/fujidaiti/smooth_sheets/assets/68946713/7b7bd8ee-b793-4d98-b5c1-25d525c95d95

naamapps commented 6 months ago

I got a normal modal sheet working with invisible space around, it's just that the navigation sheet isn't working like the normal one.

Are you sure there isn't a bug regarding the navigation sheet? I also didn't succeed reproducing the example you wrote here.. Can you try pushing a new ModalSheetRoute with NavigationSheet instead of putting the sheet inside a Stack?

Also, your example transition from one sheet to the other is not really smooth because the transition not only for the content but rather for the whole sheet! - which is not the expected result I'm trying to achieve.

This is a working code with gaps all around the sheet (using the widgets I wrote above & without NavigationSheet):

    final modalRoute = ModalSheetRoute<bool>(
      transitionDuration: const Duration(milliseconds: 250),
      builder: (context) => const SheetDismissible(
        child: DraggableSheet(
          child: StaticSheetContent(
            header: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('hello'),
              ],
            ),
            body: Padding(
              padding: EdgeInsets.all(8.0),
              child: Text('wow'),
            ),
            footer: Text('hi'),
          ),
        ),
      ),
    );
    final result = await Navigator.push<bool>(context, modalRoute);
fujidaiti commented 6 months ago

Are you sure there isn't a bug regarding the navigation sheet?

No. It's a limitation of the current version.

I got a normal modal sheet working with invisible space around

Although it seems to work in some configurations, adding invisible space around a sheet is not yet officially supported. For example, wrapping a SheetContentScaffold with a SafeArea won't work as we expected:

DraggableSheet(
  child: SafeArea(
    child: SheetContentScaffold(...),
  ),
);

Can you try pushing a new ModalSheetRoute with NavigationSheet instead of putting the sheet inside a Stack?

Try this:

class _ImperativeNavigationSheetExample extends StatelessWidget {
  const _ImperativeNavigationSheetExample();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Builder(
          builder: (context) {
            return Center(
              child: ElevatedButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    ModalSheetRoute(
                      builder: (_) => const _ExampleSheet(),
                    ),
                  );
                },
                child: const Text('Show Sheet'),
              ),
            );
          },
        ),
      ),
    );
  }
}
cabaucom376 commented 2 months ago

Just wanted to express my interest in this as well. Being able to have the padding around the sheet as well as the ability to navigate/animate the sheets themselves would align with my intended design language.