sliding_sheet 0.2.5

  • Readme
  • Changelog
  • Example
  • Installing
  • 90

Sliding Sheet #

A widget that can be dragged and scrolled in a single gesture and snapped to a list of extents.

Example of a SlidingSheet

Click here to view the full example.

Installing #

Add it to your pubspec.yaml file:

dependencies:
  sliding_sheet: ^0.2.5

Install packages from the command line

flutter packages get

Usage #

There are two ways in which you can use a SlidingSheet: either as a permanent (or persistent) Widget in your widget tree or as a BottomSheetDialog.

As a Widget #

This method can be used to show the SlidingSheet permanently (usually above your other widget) as shown in the example.

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.grey.shade200,
    appBar: AppBar(
      title: Text('Simple Example'),
    ),
    body: Stack(
      children: <Widget>[
        Center(
          child: Text('This widget is below the SlidingSheet'),
        ),

        SlidingSheet(
          elevation: 8,
          cornerRadius: 16,
          snapSpec: const SnapSpec(
            // Enable snapping. This is true by default.
            snap: true,
            // Set custom snapping points.
            snappings: [0.4, 0.7, 1.0],
            // Define to what the snappings relate to. In this case, 
            // the total available space that the sheet can expand to.
            positioning: SnapPositioning.relativeToAvailableSpace,
          ),
          builder: (context, state) {
            // This is the content of the sheet that will get
            // scrolled, if the content is bigger than the available
            // height of the sheet.
            return Container(
              height: MediaQuery.of(context).size.height,
              child: Center(
                child: Text('This is the content of the sheet'),
              ),
            );
          },
        )
      ],
    ),
  );
}

Result: #

Example

As a BottomSheetDialog #

This method can be used to show a SlidingSheet as a BottomSheetDialog by calling the showSlidingBottomSheet function and returning and instance of SlidingSheetDialog.

void showAsBottomSheet() async {
  final result = await showSlidingBottomSheet(
    context,
    builder: (context) {
      return SlidingSheetDialog(
        elevation: 8,
        cornerRadius: 16,
        snapSpec: const SnapSpec(
          snap: true,
          snappings: [0.4, 0.7, 1.0],
          positioning: SnapPositioning.relativeToAvailableSpace,
        ),
        builder: (context, state) {
          return Container(
            height: MediaQuery.of(context).size.height,
            child: Center(
              child: Material(
                child: InkWell(
                  onTap: () => Navigator.pop(context, 'This is the result.'),
                  child: Padding(
                    padding: const EdgeInsets.all(16),
                    child: Text(
                      'This is the content of the sheet',
                      style: Theme.of(context).textTheme.body1,
                    ),
                  ),
                ),
              ),
            ),
          );
        },
      );
    }
  );

  print(result); // This is the result.
}

Result: #

Example

Snapping #

A SlidingSheet can snap to multiple extents or to no at all. You can customize the snapping behavior by passing an instance of SnapSpec to the SlidingSheet.

ParameterDescription
snapIf true, the SlidingSheet will snap to the provided snappings. If false, the SlidingSheet will slide from minExtent to maxExtent and then begin to scroll, if the content is bigger than the available height.
snappingsThe extents that the SlidingSheet will snap to, when the user ends a drag interaction. The minimum and maximum values will represent the bounds in which the SlidingSheet will slide until it reaches the maximum from which on it will scroll.
positioningCan be set to one of these three values: SnapPositioning.relativeToAvailableSpace - Positions the snaps relative to total available height that the SlidingSheet can expand to. All values must be between 0 and 1. E.g. a snap of 0.5 in a Scaffold without an AppBar would mean that the snap would be positioned at 40% of the screen height, irrespective of the height of the SlidingSheet. SnapPositioning.relativeToSheetHeight - Positions the snaps relative to the total height of the sheet. All values must be between 0 and 1. E.g. a snap of 0.5 and a total sheet size of 300 pixels would mean the snap would be positioned at a 150 pixel offset from the bottom. SnapPositioning.pixelOffset - Positions the snaps at a fixed pixel offset. double.infinity can be used to refer to the total available space without having to compute it yourself.
onSnapA callback function that gets invoked when the SlidingSheet snaps to an extent.

SnapPositioning.relativeToAvailableSpace with a snap of 0.5 SnapPositioning.relativeToSheetHeight with a snap of 0.5 SnapPositioning.pixelOffset with a snap of 100

There are also some prebuild snaps you can facilitate to snap for example to headers or footers as shown in the example.

SnapDescription
SnapSpec.headerFooterSnapThe snap extent that makes header and footer fully visible without account for vertical padding on the SlidingSheet.
SnapSpec.headerSnapThe snap extent that makes the header fully visible without account for top padding on the SlidingSheet.
SnapSpec.footerSnapThe snap extent that makes the footer fully visible without account for bottom padding on the SlidingSheet.
SnapSpec.expandedThe snap extent that expands the whole SlidingSheet.

SheetController #

The SheetController can be used to change the state of a SlidingSheet manually, simply passing an instance of SheetController to a SlidingSheet. Note that the methods can only be used after the SlidingSheet has been rendered, however calling them before wont throw an exception.

MethodDescription
expand()Expands the SlidingSheet to the maximum extent.
collapse()Collapses the SlidingSheet to the minimum extent.
snapToExtent()Snaps the SlidingSheet to an arbitrary extent. The extent will be clamped to the minimum and maximum extent. If the scroll offset is > 0, the SlidingSheet will first scroll to the top and then slide to the extent.
scrollTo()Scrolls the SlidingSheet to the given offset. If the SlidingSheet is not yet at its maximum extent, it will first snap to the maximum extent and then scroll to the given offset.
rebuild()Calls all builders of the SlidingSheet to rebuild their children. This method can be used to reflect changes in the SlidingSheets children without calling setState(() {}); on the parent widget to improve performance.

Headers and Footers #

Headers and footers are UI elements of a SlidingSheet that will be displayed at the top or bottom of a SlidingSheet respectively and will not get scrolled. The scrollable content will then live in between the header and the footer if specified. Delegating the touch events to the SlidingSheet is done for you. Example:

@override
Widget build(BuildContext context) {
  return Scaffold(
    backgroundColor: Colors.grey.shade200,
    appBar: AppBar(
      title: Text('Simple Example'),
    ),
    body: Stack(
      children: <Widget>[
        SlidingSheet(
          elevation: 8,
          cornerRadius: 16,
          snapSpec: const SnapSpec(
            snap: true,
            snappings: [112, 400, double.infinity],
            positioning: SnapPositioning.pixelOffset,
          ),
          builder: (context, state) {
            return Container(
              height: 500,
              child: Center(
                child: Text(
                  'This is the content of the sheet',
                  style: Theme.of(context).textTheme.body1,
                ),
              ),
            );
          },
          headerBuilder: (context, state) {
            return Container(
              height: 56,
              width: double.infinity,
              color: Colors.green,
              alignment: Alignment.center,
              child: Text(
                'This is the header',
                style: Theme.of(context).textTheme.body1.copyWith(color: Colors.white),
              ),
            );
          },
          footerBuilder: (context, state) {
            return Container(
              height: 56,
              width: double.infinity,
              color: Colors.yellow,
              alignment: Alignment.center,
              child: Text(
                'This is the footer',
                style: Theme.of(context).textTheme.body1.copyWith(color: Colors.black),
              ),
            );
          },
        ),
      ],
    ),
  );
}

Result: #

Simple header/footer example

ListViews and Columns #

The children of a SlidingSheet are not allowed to have an inifinite (unbounded) height. Therefore when using a ListView, make sure to set shrinkWrap to true and physics to NeverScrollableScrollPhysics. Similarly when using a Column as a child of a SlidingSheet, make sure to set the mainAxisSize to MainAxisSize.min.

Reflecting changes #

To improve performace, the children of a SlidingSheet are not rebuild when it slides or gets scrolled. You can however pass a callback function to the listener parameter of a SlidingSheet, that gets called with the current SheetState whenever the SlidingSheet slides or gets scrolled. You can then rebuild your UI by calling setState(() {}), (instance of SheetController).rebuild() or by a different state management solution to rebuild the sheet. The example for instance decreases the corner radius of the SlidingSheet as it gets dragged to the top and increases the headers top padding by the status bar height. When using the SlidingSheet as a bottomSheetDialog you can also use (instance of SheetController).rebuild() to rebuild the sheet.

Example on how to reflect changes in the SlidingSheet

0.1.0 Initial release #

0.2.0 Changed the implemenation for bottom sheets. #

example/lib/main.dart

import 'dart:async';

import 'package:charts_flutter/flutter.dart' as charts;
import 'package:example/util/util.dart';
import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';

import 'package:sliding_sheet/sliding_sheet.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  static const mapsBlue = Color(0xFF4185F3);
  static const textStyle = TextStyle(
    color: Colors.black,
    fontFamily: 'sans-serif-medium',
    fontSize: 15,
  );

  SheetState state;
  BuildContext context;
  SheetController controller;

  bool get isExpanded => state?.isExpanded ?? false;
  bool get isCollapsed => state?.isCollapsed ?? true;
  double get progress => state?.progress ?? 0.0;

  @override
  void initState() {
    super.initState();
    controller = SheetController();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Example App',
      debugShowCheckedModeBanner: false,
      home: Builder(
        builder: (context) {
          this.context = context;

          return WillPopScope(
            onWillPop: () async {
              if (state?.isCollapsed == false) {
                controller?.collapse();
                return false;
              }
              return true;
            },
            child: Scaffold(
              body: Stack(
                children: <Widget>[
                  buildMap(),
                  Align(
                    alignment: Alignment.topRight,
                    child: Padding(
                      padding: EdgeInsets.fromLTRB(0, MediaQuery.of(context).padding.top + 16, 16, 0),
                      child: FloatingActionButton(
                        child: Icon(
                          Icons.layers,
                          color: mapsBlue,
                        ),
                        backgroundColor: Colors.white,
                        onPressed: () async {
                          await showBottomSheet(context);
                        },
                      ),
                    ),
                  ),
                  buildSheet(),
                ],
              ),
            ),
          );
        },
      ),
    );
  }

  Widget buildSheet() {
    return SlidingSheet(
      controller: controller,
      color: Colors.white,
      elevation: 16,
      maxWidth: 500,
      padding: EdgeInsets.only(
        top: MediaQuery.of(context).padding.top * interval(.7, 1.0, progress),
      ),
      cornerRadius: 16 * (1 - interval(0.7, 1.0, progress)),
      border: Border.all(
        color: Colors.grey.shade300,
        width: 3,
      ),
      snapSpec: SnapSpec(
        snap: true,
        positioning: SnapPositioning.relativeToAvailableSpace,
        snappings: [
          SnapSpec.headerFooterSnap,
          0.8,
          SnapSpec.expanded,
        ],
        onSnap: (state, snap) {
          print('Snapped to $snap');
        },
      ),
      scrollSpec: ScrollSpec.bouncingScroll(),
      listener: (state) {
        this.state = state;
        setState(() {});
      },
      headerBuilder: buildHeader,
      footerBuilder: buildFooter,
      builder: buildChild,
    );
  }

  Widget buildHeader(BuildContext context, SheetState state) {
    return CustomContainer(
      animate: true,
      color: Colors.white,
      padding: const EdgeInsets.symmetric(horizontal: 16),
      elevation: !state.isAtTop ? 4 : 0,
      shadowColor: Colors.black12,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          SizedBox(height: 2),
          Align(
            alignment: Alignment.topCenter,
            child: CustomContainer(
              width: 16,
              height: 4,
              borderRadius: 2,
              color: Colors.grey.withOpacity(.5 * (1 - interval(0.7, 1.0, progress))),
            ),
          ),
          SizedBox(height: 8),
          Row(
            children: <Widget>[
              Text(
                '5h 36m',
                style: textStyle.copyWith(
                  color: Color(0xFFF0BA64),
                  fontSize: 22,
                ),
              ),
              SizedBox(width: 8),
              Text(
                '(353 mi)',
                style: textStyle.copyWith(
                  color: Colors.grey.shade600,
                  fontSize: 21,
                ),
              ),
            ],
          ),
          SizedBox(height: 8),
          Text(
            'Fastest route now due to traffic conditions.',
            style: textStyle.copyWith(
              color: Colors.grey,
              fontSize: 16,
            ),
          ),
          SizedBox(height: 8),
        ],
      ),
    );
  }

  Widget buildFooter(BuildContext context, SheetState state) {
    Widget button(Icon icon, Text text, VoidCallback onTap, {BorderSide border, Color color}) {
      final child = Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: <Widget>[
          icon,
          SizedBox(width: 8),
          text,
        ],
      );

      final shape = RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(18)),
      );

      return border == null
          ? RaisedButton(
              color: color,
              onPressed: onTap,
              elevation: 2,
              child: child,
              shape: shape,
            )
          : OutlineButton(
              color: color,
              onPressed: onTap,
              child: child,
              borderSide: border,
              shape: shape,
            );
    }

    return CustomContainer(
      animate: true,
      elevation: !isCollapsed && !state.isAtBottom ? 4 : 0,
      shadowDirection: ShadowDirection.top,
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      color: Colors.white,
      shadowColor: Colors.black12,
      child: Row(
        children: <Widget>[
          button(
            Icon(
              Icons.navigation,
              color: Colors.white,
            ),
            Text(
              'Start',
              style: textStyle.copyWith(
                color: Colors.white,
                fontSize: 15,
              ),
            ),
            () async {
              await controller.hide();
              Future.delayed(const Duration(milliseconds: 1500), () {
                controller.show();
              });
            },
            color: mapsBlue,
          ),
          SizedBox(width: 8),
          button(
            Icon(
              !isExpanded ? Icons.list : Icons.map,
              color: mapsBlue,
            ),
            Text(
              !isExpanded ? 'Steps & more' : 'Show map',
              style: textStyle.copyWith(
                fontSize: 15,
              ),
            ),
            !isExpanded ? () => controller.scrollTo(230) : controller.collapse,
            color: Colors.white,
            border: BorderSide(
              color: Colors.grey.shade400,
              width: 2,
            ),
          ),
        ],
      ),
    );
  }

  Widget buildChild(BuildContext context, SheetState state) {
    final divider = Container(
      height: 1,
      color: Colors.grey.shade300,
    );

    final titleStyle = textStyle.copyWith(
      fontSize: 16,
      fontWeight: FontWeight.w600,
    );

    final padding = const EdgeInsets.symmetric(horizontal: 16);

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        divider,
        SizedBox(height: 32),
        Padding(
          padding: padding,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                'Traffic',
                style: titleStyle,
              ),
              SizedBox(height: 16),
              buildChart(context),
            ],
          ),
        ),
        SizedBox(height: 32),
        divider,
        SizedBox(height: 32),
        Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: padding,
              child: Text(
                'Steps',
                style: titleStyle,
              ),
            ),
            SizedBox(height: 8),
            buildSteps(context),
          ],
        ),
        SizedBox(height: 32),
        divider,
        SizedBox(height: 32),
        Icon(
          MdiIcons.githubCircle,
          color: Colors.grey.shade900,
          size: 48,
        ),
        SizedBox(height: 16),
        Align(
          alignment: Alignment.center,
          child: Text(
            'Pull request are welcome!',
            style: textStyle.copyWith(
              color: Colors.grey.shade700,
            ),
            textAlign: TextAlign.center,
          ),
        ),
        SizedBox(height: 8),
        Align(
          alignment: Alignment.center,
          child: Text(
            '(Stars too)',
            style: textStyle.copyWith(
              fontSize: 12,
              color: Colors.grey,
            ),
          ),
        ),
        SizedBox(height: 32),
      ],
    );
  }

  Widget buildSteps(BuildContext context) {
    final steps = [
      Step('Go to your pubspec.yaml file.', '2 seconds'),
      Step("Add the newest version of 'sliding_sheet' to your dependencies.", '5 seconds'),
      Step("Run 'flutter packages get' in the terminal.", '4 seconds'),
      Step("Happy coding!", 'Forever'),
    ];

    return ListView.builder(
      shrinkWrap: true,
      physics: NeverScrollableScrollPhysics(),
      itemCount: steps.length,
      itemBuilder: (context, i) {
        final step = steps[i];

        return Padding(
          padding: const EdgeInsets.fromLTRB(56, 16, 0, 0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                step.instruction,
                style: textStyle.copyWith(
                  fontSize: 16,
                ),
              ),
              SizedBox(height: 16),
              Row(
                children: <Widget>[
                  Text(
                    '${step.time}',
                    style: textStyle.copyWith(
                      color: Colors.grey,
                      fontSize: 15,
                    ),
                  ),
                  SizedBox(width: 16),
                  Expanded(
                    child: Container(
                      height: 1,
                      color: Colors.grey.shade300,
                    ),
                  )
                ],
              ),
              SizedBox(height: 8),
            ],
          ),
        );
      },
    );
  }

  Widget buildChart(BuildContext context) {
    final series = [
      charts.Series<Traffic, String>(
        id: 'traffic',
        data: [
          Traffic(0.5, '14:00'),
          Traffic(0.6, '14:30'),
          Traffic(0.5, '15:00'),
          Traffic(0.7, '15:30'),
          Traffic(0.8, '16:00'),
          Traffic(0.6, '16:30'),
        ],
        colorFn: (traffic, __) {
          if (traffic.time == '14:30') return charts.Color.fromHex(code: '#F0BA64');
          return charts.MaterialPalette.gray.shade300;
        },
        domainFn: (Traffic traffic, _) => traffic.time,
        measureFn: (Traffic traffic, _) => traffic.intesity,
      ),
    ];

    return Container(
      height: 128,
      child: charts.BarChart(
        series,
        animate: true,
        domainAxis: charts.OrdinalAxisSpec(
          renderSpec: charts.SmallTickRendererSpec(
            labelStyle: charts.TextStyleSpec(
              fontSize: 12, // size in Pts.
              color: charts.MaterialPalette.gray.shade500,
            ),
          ),
        ),
        defaultRenderer: charts.BarRendererConfig(
          cornerStrategy: const charts.ConstCornerStrategy(5),
        ),
      ),
    );
  }

  Future showBottomSheet(BuildContext context) async {
    final dialogController = SheetController();
    double progress = 0;
    double multiple = 1;

    await showSlidingBottomSheet(
      context,
      builder: (context) {
        return SlidingSheetDialog(
          controller: dialogController,
          snapSpec: const SnapSpec(
            snap: false,
            snappings: const [0.4, 0.7, 1.0],
          ),
          scrollSpec: ScrollSpec.bouncingScroll(),
          maxWidth: 500,
          color: Colors.white,
          cornerRadius: 16 * multiple,
          listener: (state) {
            progress = state.progress;
            multiple = 1 - interval(0.7, 1.0, progress);
            if (progress >= 0.6 || (state.isExpanded && state.scrollOffset < 8.0)) {
              dialogController.rebuild();
            }
          },
          headerBuilder: (context, state) {
            final theme = Theme.of(context);
            final textTheme = theme.textTheme;

            return Material(
              elevation: interval(0.0, 8.0, state.scrollOffset) * 4,
              shadowColor: Colors.black,
              color: Colors.white,
              child: Container(
                padding: EdgeInsets.fromLTRB(
                  16,
                  16 + (MediaQuery.of(context).viewPadding.top * (1 - multiple)),
                  16,
                  16,
                ),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: <Widget>[
                    Text(
                      'Header',
                      style: textTheme.headline,
                    ),
                    Stack(
                      children: <Widget>[
                        IgnorePointer(
                          ignoring: progress > 0.7,
                          child: Opacity(
                            opacity: 1 - interval(0.7, 0.85, progress),
                            child: IconButton(
                              icon: Icon(Icons.keyboard_arrow_up),
                              onPressed: dialogController.expand,
                            ),
                          ),
                        ),
                        IgnorePointer(
                          ignoring: progress < 1.0,
                          child: Opacity(
                            opacity: interval(0.85, 1.0, progress),
                            child: IconButton(
                              icon: Icon(Icons.keyboard_arrow_down),
                              onPressed: () => Navigator.pop(context),
                            ),
                          ),
                        ),
                      ],
                    )
                  ],
                ),
              ),
            );
          },
          /* builder: (context, state) {
            return Container(
              height: 1000,
              color: Colors.white,
              child: Center(
                child: Text(
                  'This is a bottom sheet dialog!',
                  style: textStyle,
                ),
              ),
            );
          }, */
          footerBuilder: (context, state) {
            return Container(
              height: 56,
              color: Colors.black,
            );
          },
          builder: (context, state) {
            return Container(
              color: Colors.white,
              child: Material(
                child: ListView(
                  shrinkWrap: true,
                  physics: NeverScrollableScrollPhysics(),
                  children: [0, 1, 2, 3].map((i) {
                    return Container(
                      padding: const EdgeInsets.all(48),
                      child: Text('Text $i'),
                    );
                  }).toList(),
                ),
              ),
            );
          },
        );
      },
    );
  }

  Widget buildMap() {
    return Column(
      children: <Widget>[
        Expanded(
          child: Image.asset(
            'assets/maps_screenshot.png',
            width: double.infinity,
            height: double.infinity,
            alignment: Alignment.center,
            fit: BoxFit.cover,
          ),
        ),
        SizedBox(height: 56),
      ],
    );
  }
}

class Step {
  final String instruction;
  final String time;
  Step(
    this.instruction,
    this.time,
  );
}

class Traffic {
  final double intesity;
  final String time;
  Traffic(
    this.intesity,
    this.time,
  );
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  sliding_sheet: ^0.2.5

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:sliding_sheet/sliding_sheet.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
81
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
90
Learn more about scoring.

We analyzed this package on Jan 19, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.0
  • pana: 0.13.4
  • Flutter: 1.12.13+hotfix.5

Health suggestions

Format lib/sliding_sheet.dart.

Run flutter format to format lib/sliding_sheet.dart.

Format lib/src/sheet.dart.

Run flutter format to format lib/src/sheet.dart.

Format lib/src/src.dart.

Run flutter format to format lib/src/src.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.2.2 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test