diff --git a/package/lib/src/draggable/draggable_sheet.dart b/package/lib/src/draggable/draggable_sheet.dart index 68874834..8c96d330 100644 --- a/package/lib/src/draggable/draggable_sheet.dart +++ b/package/lib/src/draggable/draggable_sheet.dart @@ -34,9 +34,7 @@ class DraggableSheet extends StatelessWidget { this.initialExtent = const Extent.proportional(1), this.minExtent = const Extent.proportional(1), this.maxExtent = const Extent.proportional(1), - this.physics = const StretchingSheetPhysics( - parent: SnappingSheetPhysics(), - ), + this.physics, required this.child, this.controller, }); @@ -54,7 +52,7 @@ class DraggableSheet extends StatelessWidget { final Extent maxExtent; /// {@macro SheetExtent.physics} - final SheetPhysics physics; + final SheetPhysics? physics; /// An object that can be used to control and observe the sheet height. final SheetController? controller; @@ -117,7 +115,7 @@ class DraggableSheetExtentConfig extends SheetExtentConfig { final Extent maxExtent; /// {@macro SheetExtent.physics} - final SheetPhysics physics; + final SheetPhysics? physics; @override bool shouldRebuild(BuildContext context, SheetExtent oldExtent) { @@ -125,7 +123,7 @@ class DraggableSheetExtentConfig extends SheetExtentConfig { oldExtent.minExtent != minExtent || oldExtent.maxExtent != maxExtent || oldExtent.initialExtent != initialExtent || - oldExtent.physics != physics; + oldExtent.physics != _resolvePhysics(context); } @override @@ -135,9 +133,23 @@ class DraggableSheetExtentConfig extends SheetExtentConfig { initialExtent: initialExtent, minExtent: minExtent, maxExtent: maxExtent, - physics: physics, + physics: _resolvePhysics(context), ); } + + SheetPhysics _resolvePhysics(BuildContext context) { + const fallback = StretchingSheetPhysics(parent: SnappingSheetPhysics()); + final theme = SheetTheme.maybeOf(context); + final base = theme?.basePhysics; + if (physics case final physics?) { + return base != null ? physics.applyTo(base) : physics; + } else if (theme?.physics case final inherited?) { + // Do not apply the base physics to the inherited physics. + return inherited; + } else { + return base != null ? fallback.applyTo(base) : fallback; + } + } } /// [SheetExtent] for a [DraggableSheet]. diff --git a/package/lib/src/foundation/physics.dart b/package/lib/src/foundation/physics.dart index b6c1aaff..f18ddd9d 100644 --- a/package/lib/src/foundation/physics.dart +++ b/package/lib/src/foundation/physics.dart @@ -24,6 +24,24 @@ abstract class SheetPhysics { return _spring ?? const ScrollPhysics().spring; } + /// Create a copy of this object appending the [ancestor] to + /// the physics chain, much like [ScrollPhysics.applyTo]. + /// + /// Can be used to dynamically create an inheritance relationship + /// between [SheetPhysics] objects. For example, [SheetPhysics] `x` + /// and `y` in the following code will have the same behavior. + /// ```dart + /// final x = FooSheetPhysics().applyTo(BarSheetPhysics()); + /// final y = FooSheetPhysics(parent: BarSheetPhysics()); + /// ``` + SheetPhysics applyTo(SheetPhysics ancestor) { + return copyWith(parent: parent?.applyTo(ancestor) ?? ancestor); + } + + /// Create a copy of this object with the given fields replaced + /// by the new values. + SheetPhysics copyWith({SheetPhysics? parent, SpringDescription? spring}); + double computeOverflow(double offset, SheetMetrics metrics) { if (parent case final parent?) { return parent.computeOverflow(offset, metrics); @@ -312,6 +330,19 @@ class SnappingSheetPhysics extends SheetPhysics { final SnappingSheetBehavior snappingBehavior; + @override + SheetPhysics copyWith({ + SheetPhysics? parent, + SpringDescription? spring, + SnappingSheetBehavior? snappingBehavior, + }) { + return SnappingSheetPhysics( + parent: parent ?? this.parent, + spring: spring ?? this.spring, + snappingBehavior: snappingBehavior ?? this.snappingBehavior, + ); + } + @override Simulation? createBallisticSimulation(double velocity, SheetMetrics metrics) { final snapPixels = snappingBehavior.findSnapPixels(velocity, metrics); @@ -338,6 +369,11 @@ class ClampingSheetPhysics extends SheetPhysics { const ClampingSheetPhysics({ super.parent, }); + + @override + SheetPhysics copyWith({SheetPhysics? parent, SpringDescription? spring}) { + return ClampingSheetPhysics(parent: parent ?? this.parent); + } } class StretchingSheetPhysics extends SheetPhysics { @@ -350,6 +386,20 @@ class StretchingSheetPhysics extends SheetPhysics { final Extent stretchingRange; final Curve frictionCurve; + @override + SheetPhysics copyWith({ + SheetPhysics? parent, + SpringDescription? spring, + Extent? stretchingRange, + Curve? frictionCurve, + }) { + return StretchingSheetPhysics( + parent: parent ?? this.parent, + stretchingRange: stretchingRange ?? this.stretchingRange, + frictionCurve: frictionCurve ?? this.frictionCurve, + ); + } + @override double computeOverflow(double offset, SheetMetrics metrics) { final stretchingRange = diff --git a/package/lib/src/foundation/theme.dart b/package/lib/src/foundation/theme.dart index d54d3e9f..cf735eb0 100644 --- a/package/lib/src/foundation/theme.dart +++ b/package/lib/src/foundation/theme.dart @@ -1,5 +1,7 @@ import 'package:flutter/widgets.dart'; + import 'keyboard_dismissible.dart'; +import 'physics.dart'; /// A theme for descendant sheets. /// @@ -48,19 +50,34 @@ class SheetThemeData { /// behavior of a sheet. const SheetThemeData({ this.keyboardDismissBehavior, + this.physics, + this.basePhysics, }); /// Determines when the on-screen keyboard should be dismissed. final SheetKeyboardDismissBehavior? keyboardDismissBehavior; + /// The physics that is used by the sheet. + final SheetPhysics? physics; + + /// The most distant ancestor of the physics that is used by the sheet. + /// + /// Note that this value is ignored if the sheet uses [SheetThemeData.physics] + /// as its physics. + final SheetPhysics? basePhysics; + /// Creates a copy of this object but with the given fields replaced with /// the new values. SheetThemeData copyWith({ SheetKeyboardDismissBehavior? keyboardDismissBehavior, + SheetPhysics? physics, + SheetPhysics? basePhysics, }) => SheetThemeData( keyboardDismissBehavior: keyboardDismissBehavior ?? this.keyboardDismissBehavior, + physics: physics ?? this.physics, + basePhysics: basePhysics ?? this.basePhysics, ); @override @@ -68,11 +85,15 @@ class SheetThemeData { identical(this, other) || other is SheetThemeData && runtimeType == other.runtimeType && - keyboardDismissBehavior == other.keyboardDismissBehavior; + keyboardDismissBehavior == other.keyboardDismissBehavior && + physics == other.physics && + basePhysics == other.basePhysics; @override int get hashCode => Object.hash( runtimeType, keyboardDismissBehavior, + physics, + basePhysics, ); } diff --git a/package/lib/src/navigation/navigation_routes.dart b/package/lib/src/navigation/navigation_routes.dart index b5ec3671..a5d3da4a 100644 --- a/package/lib/src/navigation/navigation_routes.dart +++ b/package/lib/src/navigation/navigation_routes.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../draggable/draggable_sheet.dart'; import '../draggable/sheet_draggable.dart'; import '../foundation/physics.dart'; @@ -17,7 +18,7 @@ class ScrollableNavigationSheetRoute extends NavigationSheetRoute this.initialExtent = const Extent.proportional(1), this.minExtent = const Extent.proportional(1), this.maxExtent = const Extent.proportional(1), - this.physics = const StretchingSheetPhysics(parent: SnappingSheetPhysics()), + this.physics, this.transitionsBuilder, required this.builder, }) : pageExtentConfig = ScrollableSheetExtentConfig( @@ -33,7 +34,7 @@ class ScrollableNavigationSheetRoute extends NavigationSheetRoute final Extent initialExtent; final Extent minExtent; final Extent maxExtent; - final SheetPhysics physics; + final SheetPhysics? physics; @override final bool maintainState; @@ -73,7 +74,7 @@ class DraggableNavigationSheetRoute extends NavigationSheetRoute this.initialExtent = const Extent.proportional(1), this.minExtent = const Extent.proportional(1), this.maxExtent = const Extent.proportional(1), - this.physics = const StretchingSheetPhysics(parent: SnappingSheetPhysics()), + this.physics, this.transitionsBuilder, required this.builder, }) : pageExtentConfig = DraggableSheetExtentConfig( @@ -86,7 +87,7 @@ class DraggableNavigationSheetRoute extends NavigationSheetRoute final Extent initialExtent; final Extent minExtent; final Extent maxExtent; - final SheetPhysics physics; + final SheetPhysics? physics; @override final bool maintainState; @@ -131,7 +132,7 @@ class ScrollableNavigationSheetPage extends Page { this.initialExtent = const Extent.proportional(1), this.minExtent = const Extent.proportional(1), this.maxExtent = const Extent.proportional(1), - this.physics = const StretchingSheetPhysics(parent: SnappingSheetPhysics()), + this.physics, this.transitionsBuilder, required this.child, }); @@ -145,7 +146,7 @@ class ScrollableNavigationSheetPage extends Page { final Extent minExtent; final Extent maxExtent; - final SheetPhysics physics; + final SheetPhysics? physics; final RouteTransitionsBuilder? transitionsBuilder; @@ -174,6 +175,7 @@ class _PageBasedScrollableNavigationSheetRoute Duration get transitionDuration => page.transitionDuration; @override + // TODO: Prefer to directly create a config object than storing it in a field. ScrollableSheetExtentConfig get pageExtentConfig => _pageExtentConfig!; ScrollableSheetExtentConfig? _pageExtentConfig; @@ -223,7 +225,7 @@ class DraggableNavigationSheetPage extends Page { this.initialExtent = const Extent.proportional(1), this.minExtent = const Extent.proportional(1), this.maxExtent = const Extent.proportional(1), - this.physics = const StretchingSheetPhysics(parent: SnappingSheetPhysics()), + this.physics, this.transitionsBuilder, required this.child, }); @@ -237,7 +239,7 @@ class DraggableNavigationSheetPage extends Page { final Extent minExtent; final Extent maxExtent; - final SheetPhysics physics; + final SheetPhysics? physics; final RouteTransitionsBuilder? transitionsBuilder; @@ -266,6 +268,7 @@ class _PageBasedDraggableNavigationSheetRoute extends NavigationSheetRoute Duration get transitionDuration => page.transitionDuration; @override + // TODO: Prefer to directly create a config object than storing it in a field. DraggableSheetExtentConfig get pageExtentConfig => _pageExtentConfig!; DraggableSheetExtentConfig? _pageExtentConfig; diff --git a/package/lib/src/scrollable/scrollable_sheet.dart b/package/lib/src/scrollable/scrollable_sheet.dart index 5ed0262e..eb8d0266 100644 --- a/package/lib/src/scrollable/scrollable_sheet.dart +++ b/package/lib/src/scrollable/scrollable_sheet.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; + import '../foundation/framework.dart'; import '../foundation/keyboard_dismissible.dart'; import '../foundation/physics.dart'; @@ -16,9 +17,7 @@ class ScrollableSheet extends StatelessWidget { this.initialExtent = const Extent.proportional(1), this.minExtent = const Extent.proportional(1), this.maxExtent = const Extent.proportional(1), - this.physics = const StretchingSheetPhysics( - parent: SnappingSheetPhysics(), - ), + this.physics, this.controller, required this.child, }); @@ -36,7 +35,7 @@ class ScrollableSheet extends StatelessWidget { final Extent maxExtent; /// {@macro SheetExtent.physics} - final SheetPhysics physics; + final SheetPhysics? physics; /// An object that can be used to control and observe the sheet height. final SheetController? controller; diff --git a/package/lib/src/scrollable/scrollable_sheet_extent.dart b/package/lib/src/scrollable/scrollable_sheet_extent.dart index 8dd49902..d98683ca 100644 --- a/package/lib/src/scrollable/scrollable_sheet_extent.dart +++ b/package/lib/src/scrollable/scrollable_sheet_extent.dart @@ -7,6 +7,7 @@ import '../foundation/activities.dart'; import '../foundation/physics.dart'; import '../foundation/sheet_extent.dart'; import '../foundation/sheet_status.dart'; +import '../foundation/theme.dart'; import '../internal/double_utils.dart'; import 'delegatable_scroll_position.dart'; import 'scrollable_sheet_physics.dart'; @@ -29,7 +30,7 @@ class ScrollableSheetExtentConfig extends SheetExtentConfig { final Extent maxExtent; /// {@macro SheetExtent.physics} - final SheetPhysics physics; + final SheetPhysics? physics; @override bool shouldRebuild(BuildContext context, SheetExtent oldExtent) { @@ -37,7 +38,7 @@ class ScrollableSheetExtentConfig extends SheetExtentConfig { oldExtent.initialExtent != initialExtent || oldExtent.minExtent != minExtent || oldExtent.maxExtent != maxExtent || - oldExtent.physics != physics; + oldExtent.physics != _resolvePhysics(context); } @override @@ -47,9 +48,23 @@ class ScrollableSheetExtentConfig extends SheetExtentConfig { initialExtent: initialExtent, minExtent: minExtent, maxExtent: maxExtent, - physics: physics, + physics: _resolvePhysics(context), ); } + + SheetPhysics _resolvePhysics(BuildContext context) { + const fallback = StretchingSheetPhysics(parent: SnappingSheetPhysics()); + final theme = SheetTheme.maybeOf(context); + final base = theme?.basePhysics; + if (physics case final physics?) { + return base != null ? physics.applyTo(base) : physics; + } else if (theme?.physics case final inherited?) { + // Do not apply the base physics to the inherited physics. + return inherited; + } else { + return base != null ? fallback.applyTo(base) : fallback; + } + } } class ScrollableSheetExtent extends SheetExtent { diff --git a/package/lib/src/scrollable/scrollable_sheet_physics.dart b/package/lib/src/scrollable/scrollable_sheet_physics.dart index 10f7917f..f1778b5d 100644 --- a/package/lib/src/scrollable/scrollable_sheet_physics.dart +++ b/package/lib/src/scrollable/scrollable_sheet_physics.dart @@ -1,3 +1,5 @@ +import 'package:flutter/physics.dart'; + import '../foundation/physics.dart'; import '../foundation/sheet_extent.dart'; @@ -10,6 +12,20 @@ class ScrollableSheetPhysics extends SheetPhysics { final double maxScrollSpeedToInterrupt; + @override + SheetPhysics copyWith({ + SheetPhysics? parent, + SpringDescription? spring, + double? maxScrollSpeedToInterrupt, + }) { + return ScrollableSheetPhysics( + parent: parent ?? this.parent, + spring: spring ?? this.spring, + maxScrollSpeedToInterrupt: + maxScrollSpeedToInterrupt ?? this.maxScrollSpeedToInterrupt, + ); + } + bool shouldInterruptBallisticScroll(double velocity, SheetMetrics metrics) { return velocity.abs() < maxScrollSpeedToInterrupt; } diff --git a/package/test/foundation/sheet_physics_test.dart b/package/test/foundation/physics_test.dart similarity index 89% rename from package/test/foundation/sheet_physics_test.dart rename to package/test/foundation/physics_test.dart index 0f622016..eaaed14d 100644 --- a/package/test/foundation/sheet_physics_test.dart +++ b/package/test/foundation/physics_test.dart @@ -1,3 +1,4 @@ +// ignore_for_file: lines_longer_than_80_chars import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:smooth_sheets/smooth_sheets.dart'; @@ -5,6 +6,11 @@ import 'package:smooth_sheets/src/foundation/sheet_status.dart'; class _SheetPhysicsWithDefaultConfiguration extends SheetPhysics { const _SheetPhysicsWithDefaultConfiguration(); + + @override + SheetPhysics copyWith({SheetPhysics? parent, SpringDescription? spring}) { + return const _SheetPhysicsWithDefaultConfiguration(); + } } const _referenceSheetMetrics = SheetMetricsSnapshot( @@ -31,6 +37,36 @@ final _positionAtMiddle = _referenceSheetMetrics.copyWith( ); void main() { + group('$SheetPhysics subclasses', () { + test('can create dynamic inheritance relationships', () { + const clamp = ClampingSheetPhysics(); + const stretch = StretchingSheetPhysics(); + const snap = SnappingSheetPhysics(); + + List getChain(SheetPhysics physics) { + return switch (physics.parent) { + null => [physics.runtimeType], + final parent => [physics.runtimeType, ...getChain(parent)], + }; + } + + expect( + getChain(clamp.applyTo(stretch).applyTo(snap)).join(' -> '), + 'ClampingSheetPhysics -> StretchingSheetPhysics -> SnappingSheetPhysics', + ); + + expect( + getChain(snap.applyTo(stretch).applyTo(clamp)).join(' -> '), + 'SnappingSheetPhysics -> StretchingSheetPhysics -> ClampingSheetPhysics', + ); + + expect( + getChain(stretch.applyTo(clamp).applyTo(snap)).join(' -> '), + 'StretchingSheetPhysics -> ClampingSheetPhysics -> SnappingSheetPhysics', + ); + }); + }); + group('Default configuration of $SheetPhysics', () { late SheetPhysics physicsUnderTest;