diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146..889312e 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-6.4.1-all.zip \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index aaafcd3..365efe4 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:example/util/util.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -64,7 +63,7 @@ class _ExampleState extends State { Widget buildSheet() { return SlidingSheet( - duration: const Duration(milliseconds: 900), + openDuration: const Duration(milliseconds: 900), controller: controller, color: Colors.white, shadowColor: Colors.black26, @@ -122,8 +121,9 @@ class _ExampleState extends State { width: 16, height: 4, borderRadius: 2, - color: - Colors.grey.withOpacity(.5 * (1 - interval(0.7, 1.0, state.progress))), + color: Colors.grey.withOpacity( + .5 * (1 - interval(0.7, 1.0, state.progress)), + ), ), ), const SizedBox(height: 8), @@ -217,10 +217,13 @@ class _ExampleState extends State { () async { // Inherit from context... await SheetController.of(context).hide(); - Future.delayed(const Duration(milliseconds: 1500), () { - // or use the controller - controller.show(); - }); + Future.delayed( + const Duration(milliseconds: 1500), + () { + // or use the controller + controller.show(); + }, + ); }, color: mapsBlue, ), @@ -237,9 +240,7 @@ class _ExampleState extends State { ), Text( !isExpanded ? 'Steps & more' : 'Show map', - style: textStyle.copyWith( - fontSize: 15, - ), + style: textStyle.copyWith(fontSize: 15), ), !isExpanded ? () => controller.scrollTo(state.maxScrollExtent) @@ -348,11 +349,22 @@ class _ExampleState extends State { 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'), + '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( @@ -370,9 +382,7 @@ class _ExampleState extends State { children: [ Text( step.instruction, - style: textStyle.copyWith( - fontSize: 16, - ), + style: textStyle.copyWith(fontSize: 16), ), const SizedBox(height: 16), Row( @@ -489,7 +499,11 @@ class _ExampleState extends State { if (backButton || backDrop) { const duration = Duration(milliseconds: 300); - await controller.snapToExtent(0.2, duration: duration, clamp: false); + await controller.snapToExtent( + 0.2, + duration: duration, + clamp: false, + ); await controller.snapToExtent(0.4, duration: duration); // or Navigator.pop(context); } @@ -519,7 +533,9 @@ class _ExampleState extends State { children: [ Expanded( child: Text( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sagittis tellus lacus, et pulvinar orci eleifend in.', + 'Lorem ipsum dolor sit amet, consectetur adipiscing ' + 'elit. Praesent sagittis tellus lacus, et pulvinar ' + 'orci eleifend in.', style: textTheme.subtitle1.copyWith( color: Colors.white, fontWeight: FontWeight.bold, @@ -589,13 +605,15 @@ class _ExampleState extends State { Align( alignment: Alignment.topRight, child: Padding( - padding: - EdgeInsets.fromLTRB(0, MediaQuery.of(context).padding.top + 16, 16, 0), + padding: EdgeInsets.fromLTRB( + 0, + MediaQuery.of(context).padding.top + 16, + 16, + 0, + ), child: FloatingActionButton( backgroundColor: Colors.white, - onPressed: () async { - await showBottomSheetDialog(context); - }, + onPressed: () async => showBottomSheetDialog(context), child: const Icon( Icons.layers, color: mapsBlue, @@ -631,6 +649,7 @@ class _ExampleState extends State { class Step { final String instruction; final String time; + Step( this.instruction, this.time, @@ -640,6 +659,7 @@ class Step { class Traffic { final double intesity; final String time; + Traffic( this.intesity, this.time, diff --git a/example/lib/util/custom_container.dart b/example/lib/util/custom_container.dart index e786f44..00d6a77 100644 --- a/example/lib/util/custom_container.dart +++ b/example/lib/util/custom_container.dart @@ -71,11 +71,21 @@ class CustomContainer extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final w = width == null || width == EXPAND ? double.infinity : width == WRAP ? null : width; + final w = width == null || width == EXPAND + ? double.infinity + : width == WRAP + ? null + : width; final h = height == EXPAND ? double.infinity : height; final br = customBorders ?? BorderRadius.circular( - boxShape == BoxShape.rectangle ? borderRadius : w != null ? w / 2.0 : h != null ? h / 2.0 : 0, + boxShape == BoxShape.rectangle + ? borderRadius + : w != null + ? w / 2.0 + : h != null + ? h / 2.0 + : 0, ); Widget content = Padding( diff --git a/example/lib/util/util.dart b/example/lib/util/util.dart index 1f46cbe..e30b3be 100644 --- a/example/lib/util/util.dart +++ b/example/lib/util/util.dart @@ -4,7 +4,6 @@ export 'custom_container.dart'; // ignore_for_file: public_member_api_docs - // Shrinks animation values inside a specified range. E.g. from .2 - .4 => .3 = 50%. double interval(double lower, double upper, double progress) { assert(lower < upper); @@ -15,4 +14,5 @@ double interval(double lower, double upper, double progress) { return ((progress - lower) / (upper - lower)).clamp(0.0, 1.0); } -void postFrame(void Function() callback) => WidgetsBinding.instance.addPostFrameCallback((_) => callback()); \ No newline at end of file +void postFrame(void Function() callback) => + WidgetsBinding.instance.addPostFrameCallback((_) => callback()); diff --git a/example/pubspec.lock b/example/pubspec.lock index 0e67b22..a0b2aba 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" charts_common: dependency: transitive description: @@ -56,14 +56,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -80,21 +80,28 @@ packages: name: intl url: "https://pub.dartlang.org" source: hosted - version: "0.16.0" + version: "0.16.1" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.3+2" + version: "1.0.1" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" material_design_icons_flutter: dependency: "direct main" description: @@ -108,14 +115,14 @@ packages: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -134,7 +141,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -169,21 +176,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=0.1.2" diff --git a/lib/src/scrolling.dart b/lib/src/scrolling.dart index e0fc342..3b3a80c 100644 --- a/lib/src/scrolling.dart +++ b/lib/src/scrolling.dart @@ -9,6 +9,7 @@ class _SheetExtent { double headerHeight = 0; double footerHeight = 0; double availableHeight = 0; + _SheetExtent( this.controller, { required this.isDialog, @@ -24,18 +25,22 @@ class _SheetExtent { } late ValueNotifier _currentExtent; + double get currentExtent => _currentExtent.value!; - set currentExtent(double value) => - _currentExtent.value = math.min(value, maxExtent); + + set currentExtent(double value) => _currentExtent.value = math.min(value, maxExtent); double get sheetHeight => childHeight + headerHeight + footerHeight; late double maxExtent; late double minExtent; + double get additionalMinExtent => isAtMin ? 0.0 : 0.1; + double get additionalMaxExtent => isAtMax ? 0.0 : 0.1; bool get isAtMax => currentExtent >= maxExtent; + bool get isAtMin => currentExtent <= minExtent && minExtent != maxExtent; void addPixelDelta(double pixelDelta) { @@ -63,26 +68,35 @@ class _SheetExtent { } bool get isAtTop => scrollOffset <= 0; + bool get isAtBottom => scrollOffset >= maxScrollExtent; } class _SlidingSheetScrollController extends ScrollController { final _SlidingSheetState sheet; + _SlidingSheetScrollController(this.sheet); SlidingSheet get widget => sheet.widget; _SheetExtent get extent => sheet.extent!; + void Function(double, bool, bool) get onPop => sheet._pop; - Duration get duration => sheet.widget.duration; + + Duration get duration => sheet.widget.openDuration; + SnapSpec get snapSpec => sheet.snapSpec; double get currentExtent => extent.currentExtent; + double get maxExtent => extent.maxExtent; + double get minExtent => extent.minExtent; bool inDrag = false; + bool get animating => controller?.isAnimating == true; + bool get inInteraction => inDrag || animating; _SlidingSheetScrollPosition? _currentPosition; @@ -103,15 +117,16 @@ class _SlidingSheetScrollController extends ScrollController { // Adjust the animation duration for a snap to give it a more // realistic feel. final num distanceFactor = - ((currentExtent - snap).abs() / (maxExtent - minExtent)) - .clamp(0.33, 1.0); + ((currentExtent - snap).abs() / (maxExtent - minExtent)).clamp(0.33, 1.0); final speedFactor = 1.0 - ((velocity.abs() / 2500) * 0.33).clamp(0.0, 0.66); duration ??= this.duration * (distanceFactor * speedFactor); controller = AnimationController(duration: duration, vsync: vsync); final animation = CurvedAnimation( parent: controller!, - curve: velocity.abs() > 300 ? Curves.easeOutCubic : Curves.ease, + curve: velocity.abs() > 300 + ? Curves.easeOutCubic + : (snap == 0 || !widget.openBouncing ? Curves.ease : const SimpleBounceOut()), ); final start = extent.currentExtent; @@ -193,6 +208,7 @@ class _SlidingSheetScrollController extends ScrollController { class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { final _SlidingSheetScrollController scrollController; + _SlidingSheetScrollPosition( this.scrollController, { required ScrollPhysics physics, @@ -211,32 +227,45 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { bool isMovingDown = false; bool get inDrag => scrollController.inDrag; + set inDrag(bool value) => scrollController.inDrag = value; _SheetExtent get extent => scrollController.extent; + _SlidingSheetState get sheet => scrollController.sheet; + void Function(double, bool, bool) get onPop => scrollController.onPop; + SnapSpec get snapBehavior => sheet.snapSpec; + ScrollSpec get scrollSpec => sheet.scrollSpec; + List get snappings => extent.snappings; + bool get fromBottomSheet => extent.isDialog; + bool get snap => snapBehavior.snap; + bool get isDismissable => sheet.widget.isDismissable && fromBottomSheet; double get availableHeight => extent.targetHeight; + double get currentExtent => extent.currentExtent; + double get maxExtent => extent.maxExtent; + double get minExtent => extent.minExtent; + double get offset => scrollController.offset; bool get shouldScroll => pixels > 0.0 && extent.isAtMax; + bool get isCoveringFullExtent => scrollController.sheet.isScrollable; + bool get shouldMakeSheetNonDismissable => - sheet.didCompleteInitialRoute && - !isDismissable && - currentExtent < minExtent; - bool get isBottomSheetBelowMinExtent => - fromBottomSheet && currentExtent < minExtent; + sheet.didCompleteInitialRoute && !isDismissable && currentExtent < minExtent; + + bool get isBottomSheetBelowMinExtent => fromBottomSheet && currentExtent < minExtent; @override bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { @@ -258,12 +287,10 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { inDrag = true; final isNotAtMinOrMaxExtent = !(extent.isAtMin || extent.isAtMax); - final scrollsUpWhenAtMinExtent = - extent.isAtMin && (delta < 0 || fromBottomSheet); + final scrollsUpWhenAtMinExtent = extent.isAtMin && (delta < 0 || fromBottomSheet); final scrollsDownWhenAtMaxExtent = extent.isAtMax && delta > 0; - final shouldAddPixelDeltaToExtent = isNotAtMinOrMaxExtent || - scrollsUpWhenAtMinExtent || - scrollsDownWhenAtMaxExtent; + final shouldAddPixelDeltaToExtent = + isNotAtMinOrMaxExtent || scrollsUpWhenAtMinExtent || scrollsDownWhenAtMaxExtent; if (!shouldScroll && shouldAddPixelDeltaToExtent) { final adjustedDelta = adjustDelta(-delta); extent.addPixelDelta(adjustedDelta); @@ -290,8 +317,7 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { void didEndScroll() { super.didEndScroll(); - final canSnapToNextExtent = - snap && !extent.isAtMax && !extent.isAtMin && !shouldScroll; + final canSnapToNextExtent = snap && !extent.isAtMax && !extent.isAtMin && !shouldScroll; if (inDrag && !shouldMakeSheetNonDismissable && (canSnapToNextExtent || isBottomSheetBelowMinExtent)) { @@ -321,9 +347,7 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { return; } - if (velocity == 0.0 || - (isMovingDown && shouldScroll) || - (isMovingUp && extent.isAtMax)) { + if (velocity == 0.0 || (isMovingDown && shouldScroll) || (isMovingUp && extent.isAtMax)) { super.goBallistic(velocity); return; } @@ -337,8 +361,11 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { velocity = velocity.abs(); const flingThreshold = 1700; - void snapTo(double snap) => - scrollController.snapToExtent(snap, context.vsync, velocity: velocity); + void snapTo(double snap) => scrollController.snapToExtent( + snap, + context.vsync, + velocity: velocity, + ); if (velocity > flingThreshold) { if (!isMovingUp) { @@ -360,8 +387,7 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { final slow = velocity < snapToNextThreshold; final target = !slow - ? ((isMovingUp ? 1 : -1) * - (((velocity * .45) * (1 - currentExtent)) / flingThreshold)) + + ? ((isMovingUp ? 1 : -1) * (((velocity * .45) * (1 - currentExtent)) / flingThreshold)) + currentExtent : currentExtent; @@ -370,8 +396,7 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { final stop = snappings[i]; final valid = slow || !greaterThanCurrent || - ((isMovingUp && stop >= target) || - (!isMovingUp && stop <= target)); + ((isMovingUp && stop >= target) || (!isMovingUp && stop <= target)); if (valid) { final dis = (stop - target).abs(); @@ -411,8 +436,10 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { } } - Future runScrollSimulation(double velocity, - {double friction = 0.015}) async { + Future runScrollSimulation( + double velocity, { + double friction = 0.015, + }) async { // The iOS bouncing simulation just isn't right here - once we delegate // the ballistic back to the ScrollView, it will use the right simulation. final simulation = ClampingScrollSimulation( @@ -433,8 +460,8 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { lastDelta = ballisticController.value; extent.addPixelDelta(delta); - final shouldStopScrollOnBottomSheets = fromBottomSheet && - (currentExtent <= 0.0 || shouldMakeSheetNonDismissable); + final shouldStopScrollOnBottomSheets = + fromBottomSheet && (currentExtent <= 0.0 || shouldMakeSheetNonDismissable); final shouldStopOnUpFling = velocity > 0 && extent.isAtMax; final shouldStopOnDownFling = velocity < 0 && (shouldStopScrollOnBottomSheets || extent.isAtMin); @@ -449,9 +476,7 @@ class _SlidingSheetScrollPosition extends ScrollPositionWithSingleContext { ballisticController.stop(); // Pop the route when reaching 0.0 extent. - if (fromBottomSheet && - currentExtent <= 0.0 && - !shouldMakeSheetNonDismissable) { + if (fromBottomSheet && currentExtent <= 0.0 && !shouldMakeSheetNonDismissable) { onPop(0.0, false, false); } } diff --git a/lib/src/sheet.dart b/lib/src/sheet.dart index 2bb4231..51d73d2 100644 --- a/lib/src/sheet.dart +++ b/lib/src/sheet.dart @@ -2,25 +2,32 @@ import 'dart:async'; import 'dart:math' as math; import 'dart:ui'; -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'sheet_container.dart'; +import 'simple_bounce_curve.dart'; import 'specs.dart'; import 'util.dart'; part 'scrolling.dart'; + +part 'sheet_controller.dart'; + part 'sheet_dialog.dart'; +part 'sheet_state.dart'; + +/// Widget for building sheet typedef SheetBuilder = Widget Function(BuildContext context, SheetState state); +/// Callback for changing state of the sheet typedef SheetListener = void Function(SheetState state); -typedef OnDismissPreventedCallback = void Function( - bool backButton, bool backDrop); +/// Callback prevented dismiss the dialog +typedef OnDismissPreventedCallback = void Function(bool backButton, bool backDrop); +/// Callback for opening sheet typedef OnOpenCallback = void Function(); /// A widget that can be dragged and scrolled in a single gesture and snapped @@ -54,7 +61,7 @@ class SlidingSheet extends StatefulWidget { /// {@template sliding_sheet.duration} /// The base animation duration for the sheet. Swipes and flings may have a different duration. /// {@endtemplate} - final Duration duration; + final Duration openDuration; /// {@template sliding_sheet.color} /// The background color of the sheet. @@ -208,7 +215,6 @@ class SlidingSheet extends StatefulWidget { // * SlidingSheetDialog fields /// private, do not use! - // ignore: public_member_api_docs final _SlidingSheetRoute? route; /// {@template sliding_sheet.isDismissable} @@ -228,8 +234,19 @@ class SlidingSheet extends StatefulWidget { /// {@endtemplate} final OnDismissPreventedCallback? onDismissPrevented; + /// {@template sliding_sheet.onOpen} + /// A callback that gets invoked when a user opening sheet + /// {@endtemplate} final OnOpenCallback? onOpen; + /// {@template sliding_sheet.openBouncing} + /// A flag to control sheet open animation. + /// If `true` SimpleBounceOut curve is used, otherwise Curves.ease is used. + /// + /// Defaults to `false` (Curves.ease). + /// {@endtemplate} + final bool openBouncing; + /// Creates a sheet than can be dragged and scrolled in a single gesture to be /// placed inside you widget tree. /// @@ -264,7 +281,7 @@ class SlidingSheet extends StatefulWidget { SheetBuilder? headerBuilder, SheetBuilder? footerBuilder, SnapSpec snapSpec = const SnapSpec(), - Duration duration = const Duration(milliseconds: 1000), + Duration openDuration = const Duration(milliseconds: 1000), Color? color, Color? backdropColor, Color shadowColor = Colors.black54, @@ -291,13 +308,14 @@ class SlidingSheet extends StatefulWidget { double liftOnScrollFooterElevation = 0.0, OnDismissPreventedCallback? onDismissPrevented, OnOpenCallback? onOpen, + bool openBouncing = false, }) : this._( key: key, builder: builder, headerBuilder: headerBuilder, footerBuilder: footerBuilder, snapSpec: snapSpec, - duration: duration, + openDuration: openDuration, color: color, backdropColor: backdropColor, shadowColor: shadowColor, @@ -324,6 +342,7 @@ class SlidingSheet extends StatefulWidget { liftOnScrollFooterElevation: liftOnScrollFooterElevation, onDismissPrevented: onDismissPrevented, onOpen: onOpen, + openBouncing: openBouncing, ); SlidingSheet._({ @@ -332,7 +351,7 @@ class SlidingSheet extends StatefulWidget { required this.headerBuilder, required this.footerBuilder, required this.snapSpec, - required this.duration, + required this.openDuration, required this.color, required this.backdropColor, required this.shadowColor, @@ -355,16 +374,21 @@ class SlidingSheet extends StatefulWidget { required this.extendBody, required this.liftOnScrollHeaderElevation, required this.liftOnScrollFooterElevation, + required this.openBouncing, this.body, this.parallaxSpec, this.route, this.isDismissable = true, this.onDismissPrevented, this.onOpen, - }) : assert(snapSpec.snappings.length >= 2, - 'There must be at least two snapping extents to snap in between.'), - assert(snapSpec.minSnap != snapSpec.maxSnap || route != null, - 'The min and max snaps cannot be equal.'), + }) : assert( + snapSpec.snappings.length >= 2, + 'There must be at least two snapping extents to snap in between.', + ), + assert( + snapSpec.minSnap != snapSpec.maxSnap || route != null, + 'The min and max snaps cannot be equal.', + ), assert(axisAlignment >= -1.0 && axisAlignment <= 1.0), assert(liftOnScrollHeaderElevation >= 0.0), assert(liftOnScrollFooterElevation >= 0.0), @@ -374,13 +398,13 @@ class SlidingSheet extends StatefulWidget { _SlidingSheetState createState() => _SlidingSheetState(); } -class _SlidingSheetState extends State - with TickerProviderStateMixin { +class _SlidingSheetState extends State with TickerProviderStateMixin { final GlobalKey childKey = GlobalKey(); final GlobalKey headerKey = GlobalKey(); final GlobalKey footerKey = GlobalKey(); bool get hasHeader => widget.headerBuilder != null; + bool get hasFooter => widget.footerBuilder != null; late List snappings; @@ -392,49 +416,59 @@ class _SlidingSheetState extends State // Whether the dialog completed its initial fly in bool didCompleteInitialRoute = false; + // Whether a dismiss was already triggered by the sheet itself // and thus further route pops can be safely ignored bool dismissUnderway = false; + // Whether the drag on a delegating widget (such as the backdrop) // did start, when the sheet was not fully collapsed bool didStartDragWhenNotCollapsed = false; _SheetExtent? extent; SheetController? sheetController; - _SlidingSheetScrollController? controller; + late _SlidingSheetScrollController controller; // Whether the sheet has drawn its first frame. bool get isLaidOut => availableHeight > 0 && childHeight > 0; + // The total height of all sheet components. double get sheetHeight => - childHeight + - headerHeight + - footerHeight + - padding.vertical + - borderHeight; + childHeight + headerHeight + footerHeight + padding.vertical + borderHeight; + // The maxiumum height that this sheet will cover. double get maxHeight => math.min(sheetHeight, availableHeight); + bool get isScrollable => sheetHeight >= availableHeight; - double get currentExtent => - (extent?.currentExtent ?? minExtent).clamp(0.0, 1.0); + double get currentExtent => (extent?.currentExtent ?? minExtent).clamp(0.0, 1.0); + set currentExtent(double value) => extent?.currentExtent = value; + double get headerExtent => isLaidOut ? (headerHeight + (borderHeight / 2)) / availableHeight : 0.0; + double get footerExtent => isLaidOut ? (footerHeight + (borderHeight / 2)) / availableHeight : 0.0; + double get headerFooterExtent => headerExtent + footerExtent; + double get minExtent => snappings[isDialog ? 1 : 0].clamp(0.0, 1.0); + double get maxExtent => snappings.last.clamp(0.0, 1.0); - double get initialExtent => snapSpec.initialSnap != null - ? _normalizeSnap(snapSpec.initialSnap!) - : minExtent; + + double get initialExtent => + snapSpec.initialSnap != null ? _normalizeSnap(snapSpec.initialSnap!) : minExtent; bool get isDialog => widget.route != null; + ScrollSpec get scrollSpec => widget.scrollSpec; + SnapSpec get snapSpec => widget.snapSpec; + SnapPositioning get snapPositioning => snapSpec.positioning; double get borderHeight => (widget.border?.top.width ?? 0) * 2; + EdgeInsets get padding { final begin = widget.padding ?? const EdgeInsets.all(0); @@ -450,7 +484,10 @@ class _SlidingSheetState extends State double? get cornerRadius { if (widget.cornerRadiusOnFullscreen == null) return widget.cornerRadius; return lerpDouble( - widget.cornerRadius, widget.cornerRadiusOnFullscreen, lerpFactor); + widget.cornerRadius, + widget.cornerRadiusOnFullscreen, + lerpFactor, + ); } double get lerpFactor { @@ -473,8 +510,7 @@ class _SlidingSheetState extends State ); // A notifier that a child SheetListenableBuilder can inherit to - final ValueNotifier stateNotifier = - ValueNotifier(SheetState.inital()); + final ValueNotifier stateNotifier = ValueNotifier(SheetState.inital()); @override void initState() { @@ -502,11 +538,11 @@ class _SlidingSheetState extends State didCompleteInitialRoute = true; // Set the inital extent after the first frame. postFrame( - () async { - await snapToExtent(initialExtent); - setState(() => currentExtent = initialExtent); - widget.onOpen?.call(); - } + () async { + await snapToExtent(initialExtent); + setState(() => currentExtent = initialExtent); + widget.onOpen?.call(); + }, ); } } @@ -523,10 +559,10 @@ class _SlidingSheetState extends State (_) { if (!dismissUnderway) { dismissUnderway = true; - controller!.jumpTo(controller!.offset); + controller.jumpTo(controller.offset); // When the route gets popped we animate fully out - not just // to the minExtent. - controller!.snapToExtent(0.0, this, clamp: false); + controller.snapToExtent(0.0, this, clamp: false); } }, ); @@ -558,12 +594,9 @@ class _SlidingSheetState extends State // Measure the height of all sheet components. void _measure() { postFrame(() { - final RenderBox? child = - childKey.currentContext?.findRenderObject() as RenderBox?; - final RenderBox? header = - headerKey.currentContext?.findRenderObject() as RenderBox?; - final RenderBox? footer = - footerKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? child = childKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? header = headerKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? footer = footerKey.currentContext?.findRenderObject() as RenderBox?; final isChildLaidOut = child?.hasSize == true; final prevChildHeight = childHeight; @@ -671,6 +704,8 @@ class _SlidingSheetState extends State ..availableHeight = availableHeight ..maxExtent = maxExtent ..minExtent = minExtent; + + if (currentExtent > maxExtent) currentExtent = maxExtent; } } @@ -683,9 +718,16 @@ class _SlidingSheetState extends State // Assign the controller functions to the state functions. sheetController!._scrollTo = scrollTo; - sheetController!._snapToExtent = (snap, {duration, clamp}) { - return snapToExtent(_normalizeSnap(snap), - duration: duration, clamp: clamp); + sheetController!._snapToExtent = ( + snap, { + duration, + clamp, + }) { + return snapToExtent( + _normalizeSnap(snap), + duration: duration, + clamp: clamp, + ); }; sheetController!._expand = () => snapToExtent(maxExtent); sheetController!._collapse = () => snapToExtent(minExtent); @@ -713,18 +755,18 @@ class _SlidingSheetState extends State bool? clamp, }) async { if (!isLaidOut) return; - duration ??= widget.duration; + duration ??= widget.openDuration; if (!state.isAtTop) { duration *= 0.5; - await controller!.animateTo( + await controller.animateTo( 0.0, duration: duration, curve: Curves.easeInCubic, ); } - await controller!.snapToExtent( + await controller.snapToExtent( snap, this, duration: duration, @@ -733,10 +775,13 @@ class _SlidingSheetState extends State ); } - Future scrollTo(double offset, - {Duration? duration, Curve? curve}) async { + Future scrollTo( + double offset, { + Duration? duration, + Curve? curve, + }) async { if (!isLaidOut) return; - duration ??= widget.duration; + duration ??= widget.openDuration; if (!extent!.isAtMax) { duration *= 0.5; @@ -746,7 +791,7 @@ class _SlidingSheetState extends State ); } - await controller!.animateTo( + await controller.animateTo( offset, duration: duration, curve: curve ?? (!extent!.isAtMax ? Curves.easeOutCirc : Curves.ease), @@ -754,8 +799,8 @@ class _SlidingSheetState extends State } void _nudgeToNextSnap() { - if (!controller!.inInteraction && state.isShown) { - controller!.delegateFling(); + if (!controller.inInteraction && state.isShown) { + controller.delegateFling(); } } @@ -766,10 +811,12 @@ class _SlidingSheetState extends State snapToExtent(0.0, velocity: velocity); } else if (!isDialog) { final num fractionCovered = - ((currentExtent - minExtent) / (maxExtent - minExtent)) - .clamp(0.0, 1.0); + ((currentExtent - minExtent) / (maxExtent - minExtent)).clamp(0.0, 1.0); final timeFraction = 1.0 - (fractionCovered * 0.5); - snapToExtent(minExtent, duration: widget.duration * timeFraction); + snapToExtent( + minExtent, + duration: widget.openDuration * timeFraction, + ); } _onDismissPrevented(backButton: isBackButton, backDrop: isBackDrop); } @@ -777,14 +824,11 @@ class _SlidingSheetState extends State // Ensure that the sheet sizes itself correctly when the // constraints change. void _adjustSnapForIncomingConstraints(double previousHeight) { - if (previousHeight > 0.0 && - previousHeight != availableHeight && - state.isShown) { + if (previousHeight > 0.0 && previousHeight != availableHeight && state.isShown) { _updateSnappingsAndExtent(); final num changeAdjustedExtent = - ((currentExtent * previousHeight) / availableHeight) - .clamp(minExtent, maxExtent); + ((currentExtent * previousHeight) / availableHeight).clamp(minExtent, maxExtent); final isAroundFixedSnap = snappings.any( (snap) => (snap - changeAdjustedExtent).abs() < 0.01, @@ -866,7 +910,11 @@ class _SlidingSheetState extends State } } else { if (!state.isCollapsed) { - _pop(0.0, false, true); + if (widget.onDismissPrevented != null) { + _onDismissPrevented(backButton: true); + } else { + _pop(0.0, false, true); + } return false; } else { return true; @@ -933,9 +981,7 @@ class _SlidingSheetState extends State builder: (context, dynamic extent, sheet) { final translation = () { if (headerFooterExtent > 0.0) { - return 1.0 - - (currentExtent.clamp(0.0, headerFooterExtent) / - headerFooterExtent); + return 1.0 - (currentExtent.clamp(0.0, headerFooterExtent) / headerFooterExtent); } else { return 0.0; } @@ -944,9 +990,7 @@ class _SlidingSheetState extends State return Invisible( invisible: !isLaidOut || currentExtent == 0.0, child: FractionallySizedBox( - heightFactor: isLaidOut - ? currentExtent.clamp(headerFooterExtent, 1.0) - : 1.0, + heightFactor: isLaidOut ? currentExtent.clamp(headerFooterExtent, 1.0) : 1.0, alignment: Alignment.bottomCenter, child: FractionalTranslation( translation: Offset(0, translation), @@ -1007,7 +1051,7 @@ class _SlidingSheetState extends State if (scrollSpec.overscroll) { scrollView = GlowingOverscrollIndicator( axisDirection: AxisDirection.down, - color: scrollSpec.overscrollColor ?? Theme.of(context).accentColor, + color: scrollSpec.overscrollColor ?? Theme.of(context).colorScheme.secondary, child: scrollView, ); } @@ -1028,18 +1072,17 @@ class _SlidingSheetState extends State child: widget.body, builder: (context, dynamic _, body) { final amount = spec.amount; - final defaultMaxExtent = snappings.length > 2 - ? snappings[snappings.length - 2] - : this.maxExtent; - final maxExtent = spec.endExtent != null - ? _normalizeSnap(spec.endExtent!) - : defaultMaxExtent; - assert(maxExtent > minExtent, - 'The endExtent must be greater than the min snap extent you set on the SnapSpec'); + final defaultMaxExtent = + snappings.length > 2 ? snappings[snappings.length - 2] : this.maxExtent; + final maxExtent = + spec.endExtent != null ? _normalizeSnap(spec.endExtent!) : defaultMaxExtent; + assert( + maxExtent > minExtent, + 'The endExtent must be greater than the min snap extent you set on the SnapSpec', + ); final maxOffset = (maxExtent - minExtent) * availableHeight; final num fraction = - ((currentExtent - minExtent) / (maxExtent - minExtent)) - .clamp(0.0, 1.0); + ((currentExtent - minExtent) / (maxExtent - minExtent)).clamp(0.0, 1.0); return Padding( padding: EdgeInsets.only(bottom: (amount * maxOffset) * fraction), @@ -1054,18 +1097,14 @@ class _SlidingSheetState extends State valueListenable: extent!._currentExtent, builder: (context, dynamic value, child) { final opacity = () { - if (!widget.isDismissable && - !dismissUnderway && - didCompleteInitialRoute) { + if (!widget.isDismissable && !dismissUnderway && didCompleteInitialRoute) { return 1.0; } else if (currentExtent != 0.0) { if (isDialog) { return (currentExtent / minExtent).clamp(0.0, 1.0); } else { - final secondarySnap = - snappings.length > 2 ? snappings[1] : maxExtent; - return ((currentExtent - minExtent) / (secondarySnap - minExtent)) - .clamp(0.0, 1.0); + final secondarySnap = snappings.length > 2 ? snappings[1] : maxExtent; + return ((currentExtent - minExtent) / (secondarySnap - minExtent)).clamp(0.0, 1.0); } } else { return 0.0; @@ -1084,15 +1123,16 @@ class _SlidingSheetState extends State ), ); - void onTap() => widget.isDismissable - ? _pop(0.0, true, false) - : _onDismissPrevented(backDrop: true); + void onTap() => + widget.isDismissable ? _pop(0.0, true, false) : _onDismissPrevented(backDrop: true); // see: https://github.com/BendixMa/sliding-sheet/issues/30 if (opacity >= 0.05 || didStartDragWhenNotCollapsed) { if (widget.isBackdropInteractable) { - return _delegateInteractions(backDrop, - onTap: widget.closeOnBackdropTap ? onTap : null); + return _delegateInteractions( + backDrop, + onTap: widget.closeOnBackdropTap ? onTap : null, + ); } else if (widget.closeOnBackdropTap) { return GestureDetector( onTap: onTap, @@ -1108,18 +1148,19 @@ class _SlidingSheetState extends State } Widget _delegateInteractions(Widget child, {VoidCallback? onTap}) { - if (child == null) return const SizedBox(); - var start = 0.0, end = 0.0; void onDragEnd([double velocity = 0.0]) { - controller!.delegateFling(velocity); + controller.delegateFling(velocity); // If a header was dragged, but the scroll view is not at the top // animate to the top when the drag has ended. if (!state.isAtTop && (start - end).abs() > 15.0) { - controller!.animateTo(0.0, - duration: widget.duration * 0.5, curve: Curves.ease); + controller.animateTo( + 0.0, + duration: widget.openDuration * 0.5, + curve: Curves.ease, + ); } _handleNonDismissableSnapBack(); @@ -1143,7 +1184,7 @@ class _SlidingSheetState extends State // sheet, only to drag it between min and max extent. final shouldDelegate = !delta.isNegative || currentExtent < maxExtent; if (shouldDelegate) { - controller!.delegateDrag(delta); + controller.delegateDrag(delta); } }, onVerticalDragEnd: (details) { @@ -1164,178 +1205,19 @@ class _SlidingSheetState extends State @override void dispose() { - controller!.dispose(); + controller.dispose(); super.dispose(); } } -/// A data class containing state information about the [SlidingSheet] -/// such as the extent and scroll offset. -class SheetState { - /// The current extent the sheet covers. - final double extent; - - /// The minimum extent that the sheet will cover. - final double minExtent; - - /// The maximum extent that the sheet will cover - /// until it begins scrolling. - final double maxExtent; - - /// Whether the sheet has finished measuring its children and computed - /// the correct extents. This takes until the first frame was drawn. - final bool isLaidOut; - - /// The progress between [minExtent] and [maxExtent] of the current [extent]. - /// A progress of 1 means the sheet is fully expanded, while - /// a progress of 0 means the sheet is fully collapsed. - final double progress; - - /// Whether the [SlidingSheet] has reached its maximum extent. - final bool isExpanded; - - /// Whether the [SlidingSheet] has reached its minimum extent. - final bool isCollapsed; - - /// Whether the [SlidingSheet] has a [scrollOffset] of zero. - final bool isAtTop; - - /// Whether the [SlidingSheet] has reached its maximum scroll extent. - final bool isAtBottom; - - /// Whether the sheet is hidden to the user. - final bool isHidden; - - /// Whether the sheet is visible to the user. - final bool isShown; - - /// The scroll offset of the Scrollable inside the sheet - /// at the time this [SheetState] was emitted. - final double scrollOffset; - - final _SheetExtent? _extent; - - /// A data class containing state information about the [SlidingSheet] - /// at the time this state was emitted. - SheetState( - this._extent, { - required this.extent, - required this.isLaidOut, - required this.maxExtent, - required double minExtent, - // On Bottomsheets it is possible for min and maxExtents to be the same (when you only set one snap). - // Thus we have to account for this and set the minExtent to be zero. - }) : minExtent = minExtent != maxExtent ? minExtent : 0.0, - progress = isLaidOut - ? ((extent - minExtent) / (maxExtent - minExtent)).clamp(0.0, 1.0) - : 0.0, - isExpanded = toPrecision(extent) >= toPrecision(maxExtent), - isCollapsed = toPrecision(extent) <= toPrecision(minExtent), - isAtTop = _extent?.isAtTop ?? true, - isAtBottom = _extent?.isAtBottom ?? false, - isHidden = extent <= 0.0, - isShown = extent > 0.0, - scrollOffset = _extent?.scrollOffset ?? 0.0; - - /// A default constructor which can be used to initial `ValueNotifers` for instance. - SheetState.inital() - : this(null, - extent: 0.0, minExtent: 0.0, maxExtent: 1.0, isLaidOut: false); - - /// The current scroll offset of the [Scrollable] inside the sheet. - double get currentScrollOffset => _extent?.scrollOffset ?? 0.0; - - /// The maximum amount the Scrollable inside the sheet can scroll. - double get maxScrollExtent => _extent?.maxScrollExtent ?? 0.0; - - /// private - static ValueNotifier notifier(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType<_InheritedSheetState>()! - .state; - } - - @override - String toString() { - return 'SheetState(extent: $extent, minExtent: $minExtent, maxExtent: $maxExtent, isLaidOut: $isLaidOut, progress: $progress, scrollOffset: $scrollOffset, maxScrollExtent: $maxScrollExtent, isExpanded: $isExpanded, isCollapsed: $isCollapsed, isAtTop: $isAtTop, isAtBottom: $isAtBottom, isHidden: $isHidden, isShown: $isShown)'; - } -} - class _InheritedSheetState extends InheritedWidget { final ValueNotifier state; + const _InheritedSheetState( this.state, Widget child, ) : super(child: child); @override - bool updateShouldNotify(_InheritedSheetState oldWidget) => - state != oldWidget.state; -} - -/// A controller for a [SlidingSheet]. -class SheetController { - /// Inherit the [SheetController] from the closest [SlidingSheet]. - /// - /// Every [SlidingSheet] has a [SheetController], even if you didn't assign - /// one explicitly. This allows you to call functions on the controller from child - /// widgets without having to pass a [SheetController] around. - static SheetController? of(BuildContext context) { - return context - .findAncestorStateOfType<_SlidingSheetState>() - ?.sheetController; - } - - /// Animates the sheet to the [extent]. - /// - /// The [extent] will be clamped to the minimum and maximum extent. - /// If the scrolling child is not at the top, it will scroll to the top - /// first and then animate to the specified extent. - Future? snapToExtent(double extent, - {Duration? duration, bool clamp = true}) => - _snapToExtent?.call(extent, duration: duration, clamp: clamp); - Future Function(double extent, {Duration? duration, bool? clamp})? - _snapToExtent; - - /// Animates the scrolling child to a specified offset. - /// - /// If the sheet is not fully expanded it will expand first and then - /// animate to the given [offset]. - Future? scrollTo(double offset, {Duration? duration, Curve? curve}) => - _scrollTo?.call(offset, duration: duration, curve: curve); - Future Function(double offset, {Duration? duration, Curve? curve})? - _scrollTo; - - /// Calls every builder function of the sheet to rebuild the widgets with - /// the current [SheetState]. - /// - /// This function can be used to reflect changes on the [SlidingSheet] - /// without calling `setState(() {})` on the parent widget if that would be - /// too expensive. - void rebuild() => _rebuild?.call(); - VoidCallback? _rebuild; - - /// Fully collapses the sheet. - /// - /// Short-hand for calling `snapToExtent(minExtent)`. - Future? collapse() => _collapse?.call(); - Future Function()? _collapse; - - /// Fully expands the sheet. - /// - /// Short-hand for calling `snapToExtent(maxExtent)`. - Future? expand() => _expand?.call(); - Future Function()? _expand; - - /// Reveals the [SlidingSheet] if it is currently hidden. - Future? show() => _show?.call(); - Future Function()? _show; - - /// Slides the sheet off to the bottom and hides it. - Future? hide() => _hide?.call(); - Future Function()? _hide; - - /// The current [SheetState] of this [SlidingSheet]. - SheetState? get state => _state; - SheetState? _state; + bool updateShouldNotify(_InheritedSheetState oldWidget) => state != oldWidget.state; } diff --git a/lib/src/sheet_controller.dart b/lib/src/sheet_controller.dart new file mode 100644 index 0000000..9780a57 --- /dev/null +++ b/lib/src/sheet_controller.dart @@ -0,0 +1,87 @@ +part of 'sheet.dart'; + +/// A controller for a [SlidingSheet]. +class SheetController { + /// Inherit the [SheetController] from the closest [SlidingSheet]. + /// + /// Every [SlidingSheet] has a [SheetController], even if you didn't assign + /// one explicitly. This allows you to call functions on the controller from child + /// widgets without having to pass a [SheetController] around. + static SheetController? of(BuildContext context) { + return context.findAncestorStateOfType<_SlidingSheetState>()?.sheetController; + } + + /// Animates the sheet to the [extent]. + /// + /// The [extent] will be clamped to the minimum and maximum extent. + /// If the scrolling child is not at the top, it will scroll to the top + /// first and then animate to the specified extent. + Future? snapToExtent( + double extent, { + Duration? duration, + bool clamp = true, + }) => + _snapToExtent?.call( + extent, + duration: duration, + clamp: clamp, + ); + Future Function( + double extent, { + Duration? duration, + bool? clamp, + })? _snapToExtent; + + /// Animates the scrolling child to a specified offset. + /// + /// If the sheet is not fully expanded it will expand first and then + /// animate to the given [offset]. + Future? scrollTo( + double offset, { + Duration? duration, + Curve? curve, + }) => + _scrollTo?.call( + offset, + duration: duration, + curve: curve, + ); + Future Function( + double offset, { + Duration? duration, + Curve? curve, + })? _scrollTo; + + /// Calls every builder function of the sheet to rebuild the widgets with + /// the current [SheetState]. + /// + /// This function can be used to reflect changes on the [SlidingSheet] + /// without calling `setState(() {})` on the parent widget if that would be + /// too expensive. + void rebuild() => _rebuild?.call(); + VoidCallback? _rebuild; + + /// Fully collapses the sheet. + /// + /// Short-hand for calling `snapToExtent(minExtent)`. + Future? collapse() => _collapse?.call(); + Future Function()? _collapse; + + /// Fully expands the sheet. + /// + /// Short-hand for calling `snapToExtent(maxExtent)`. + Future? expand() => _expand?.call(); + Future Function()? _expand; + + /// Reveals the [SlidingSheet] if it is currently hidden. + Future? show() => _show?.call(); + Future Function()? _show; + + /// Slides the sheet off to the bottom and hides it. + Future? hide() => _hide?.call(); + Future Function()? _hide; + + /// The current [SheetState] of this [SlidingSheet]. + SheetState? get state => _state; + SheetState? _state; +} diff --git a/lib/src/sheet_dialog.dart b/lib/src/sheet_dialog.dart index 924acea..f928a96 100644 --- a/lib/src/sheet_dialog.dart +++ b/lib/src/sheet_dialog.dart @@ -56,7 +56,7 @@ Future showSlidingBottomSheet( route: route, controller: controller, snapSpec: snapSpec, - duration: dialog.duration, + openDuration: dialog.duration, color: dialog.color ?? theme.bottomSheetTheme.backgroundColor ?? theme.dialogTheme.backgroundColor ?? @@ -87,6 +87,7 @@ Future showSlidingBottomSheet( liftOnScrollHeaderElevation: dialog.liftOnScrollHeaderElevation, liftOnScrollFooterElevation: dialog.liftOnScrollFooterElevation, body: null, + openBouncing: false, ); if (parentBuilder != null) { @@ -232,9 +233,13 @@ class SlidingSheetDialog { /// A transparent route for a bottom sheet dialog. class _SlidingSheetRoute extends PageRoute { - final Widget Function(BuildContext, Animation, _SlidingSheetRoute) - builder; + final Widget Function( + BuildContext, + Animation, + _SlidingSheetRoute, + ) builder; final Duration duration; + _SlidingSheetRoute({ required this.builder, required this.duration, diff --git a/lib/src/sheet_state.dart b/lib/src/sheet_state.dart new file mode 100644 index 0000000..95d2ca9 --- /dev/null +++ b/lib/src/sheet_state.dart @@ -0,0 +1,99 @@ +part of 'sheet.dart'; + +/// A data class containing state information about the [SlidingSheet] +/// such as the extent and scroll offset. +class SheetState { + /// The current extent the sheet covers. + final double extent; + + /// The minimum extent that the sheet will cover. + final double minExtent; + + /// The maximum extent that the sheet will cover + /// until it begins scrolling. + final double maxExtent; + + /// Whether the sheet has finished measuring its children and computed + /// the correct extents. This takes until the first frame was drawn. + final bool isLaidOut; + + /// The progress between [minExtent] and [maxExtent] of the current [extent]. + /// A progress of 1 means the sheet is fully expanded, while + /// a progress of 0 means the sheet is fully collapsed. + final double progress; + + /// Whether the [SlidingSheet] has reached its maximum extent. + final bool isExpanded; + + /// Whether the [SlidingSheet] has reached its minimum extent. + final bool isCollapsed; + + /// Whether the [SlidingSheet] has a [scrollOffset] of zero. + final bool isAtTop; + + /// Whether the [SlidingSheet] has reached its maximum scroll extent. + final bool isAtBottom; + + /// Whether the sheet is hidden to the user. + final bool isHidden; + + /// Whether the sheet is visible to the user. + final bool isShown; + + /// The scroll offset of the Scrollable inside the sheet + /// at the time this [SheetState] was emitted. + final double scrollOffset; + + final _SheetExtent? _extent; + + /// A data class containing state information about the [SlidingSheet] + /// at the time this state was emitted. + SheetState( + this._extent, { + required this.extent, + required this.isLaidOut, + required this.maxExtent, + required double minExtent, + // On Bottomsheets it is possible for min and maxExtents to be the same (when you only set one snap). + // Thus we have to account for this and set the minExtent to be zero. + }) : minExtent = minExtent != maxExtent ? minExtent : 0.0, + progress = + isLaidOut ? ((extent - minExtent) / (maxExtent - minExtent)).clamp(0.0, 1.0) : 0.0, + isExpanded = toPrecision(extent) >= toPrecision(maxExtent), + isCollapsed = toPrecision(extent) <= toPrecision(minExtent), + isAtTop = _extent?.isAtTop ?? true, + isAtBottom = _extent?.isAtBottom ?? false, + isHidden = extent <= 0.0, + isShown = extent > 0.0, + scrollOffset = _extent?.scrollOffset ?? 0.0; + + /// A default constructor which can be used to initial `ValueNotifers` for instance. + SheetState.inital() + : this( + null, + extent: 0.0, + minExtent: 0.0, + maxExtent: 1.0, + isLaidOut: false, + ); + + /// The current scroll offset of the [Scrollable] inside the sheet. + double get currentScrollOffset => _extent?.scrollOffset ?? 0.0; + + /// The maximum amount the Scrollable inside the sheet can scroll. + double get maxScrollExtent => _extent?.maxScrollExtent ?? 0.0; + + /// private + static ValueNotifier notifier(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType<_InheritedSheetState>()!.state; + } + + @override + String toString() { + return 'SheetState(extent: $extent, minExtent: $minExtent, ' + 'maxExtent: $maxExtent, isLaidOut: $isLaidOut, progress: $progress, ' + 'scrollOffset: $scrollOffset, maxScrollExtent: $maxScrollExtent, ' + 'isExpanded: $isExpanded, isCollapsed: $isCollapsed, isAtTop: $isAtTop, ' + 'isAtBottom: $isAtBottom, isHidden: $isHidden, isShown: $isShown)'; + } +} diff --git a/lib/src/simple_bounce_curve.dart b/lib/src/simple_bounce_curve.dart new file mode 100644 index 0000000..acf41bd --- /dev/null +++ b/lib/src/simple_bounce_curve.dart @@ -0,0 +1,16 @@ +import 'dart:math'; + +import 'package:flutter/animation.dart'; + +// ignore_for_file: public_member_api_docs +class SimpleBounceOut extends Curve { + const SimpleBounceOut({this.a = 0.09, this.w = 8}); + + final double a; + final double w; + + @override + double transformInternal(double t) { + return -(pow(e, -t / a) * cos(1.5 * t * w)) + 1; + } +} diff --git a/lib/src/specs.dart b/lib/src/specs.dart index 75c977c..5401770 100644 --- a/lib/src/specs.dart +++ b/lib/src/specs.dart @@ -106,7 +106,9 @@ class SnapSpec { @override String toString() { - return 'SnapSpec(snap: $snap, snappings: $snappings, initialExtent: $initialSnap, positioning: $positioning, onSnap: $onSnap)'; + return 'SnapSpec(snap: $snap, snappings: $snappings, ' + 'initialExtent: $initialSnap, positioning: $positioning, ' + 'onSnap: $onSnap)'; } @override @@ -157,12 +159,12 @@ class ScrollSpec { const ScrollSpec.overscroll({Color? color}) : this(overscrollColor: color); /// Creates an iOS bouncing scroll effect. - const ScrollSpec.bouncingScroll() - : this(physics: const BouncingScrollPhysics()); + const ScrollSpec.bouncingScroll() : this(physics: const BouncingScrollPhysics()); @override String toString() { - return 'ScrollSpec(overscroll: $overscroll, overscrollColor: $overscrollColor, physics: $physics, showScrollbar: $showScrollbar)'; + return 'ScrollSpec(overscroll: $overscroll, overscrollColor: $overscrollColor, ' + 'physics: $physics, showScrollbar: $showScrollbar)'; } @override @@ -216,8 +218,8 @@ class ParallaxSpec { }) : assert(amount >= 0.0 && amount <= 1.0); @override - String toString() => - 'ParallaxSpec(enabled: $enabled, amount: $amount, extent: $endExtent)'; + String toString() => 'ParallaxSpec(enabled: $enabled, amount: $amount, ' + 'extent: $endExtent)'; @override bool operator ==(Object o) { diff --git a/lib/src/util.dart b/lib/src/util.dart index 6ef9f1f..ed4a37b 100644 --- a/lib/src/util.dart +++ b/lib/src/util.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; // ignore_for_file: public_member_api_docs void postFrame(VoidCallback callback) { - WidgetsBinding.instance?.addPostFrameCallback((_) => callback()); + WidgetsBinding.instance.addPostFrameCallback((_) => callback()); } T swapSign(T value) { diff --git a/pubspec.lock b/pubspec.lock index 85d5b84..8a19e73 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.5.0" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,14 +21,14 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" clock: dependency: transitive description: @@ -42,14 +42,14 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" flutter: dependency: "direct main" description: flutter @@ -66,21 +66,28 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.7.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" sky_engine: dependency: transitive description: flutter @@ -92,7 +99,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" stack_trace: dependency: transitive description: @@ -127,20 +134,13 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.19" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "0.4.9" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.2" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.17.0 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 21c666f..67da38a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.5.0 homepage: https://github.com/bnxm/sliding_sheet environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' dependencies: flutter: