From 4bb0a158be316fc3a9d7a1d2160e4ca2f196640e Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sat, 6 May 2023 23:26:00 +0900 Subject: [PATCH 01/24] :recycle: Replace nearEqual with almostEqualTo --- package/lib/src/addon/adaptive_padding.dart | 9 +++------ package/lib/src/addon/gutter.dart | 8 ++------ package/lib/src/core/controller.dart | 6 ++---- package/lib/src/internal/scroll.dart | 19 ++++++------------- 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/package/lib/src/addon/adaptive_padding.dart b/package/lib/src/addon/adaptive_padding.dart index 132f690..fee2a79 100644 --- a/package/lib/src/addon/adaptive_padding.dart +++ b/package/lib/src/addon/adaptive_padding.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:exprollable_page_view/src/core/controller.dart'; import 'package:exprollable_page_view/src/core/view.dart'; -import 'package:flutter/physics.dart'; +import 'package:exprollable_page_view/src/internal/utils.dart'; import 'package:flutter/widgets.dart'; /// Inserts appropriate padding into the child widget according to the current viewpor offset. @@ -63,11 +63,8 @@ class _AdaptivePagePaddingState extends State { void invalidateState() { final oldPadding = padding; correctState(); - if (!nearEqual( - oldPadding, - padding, - Tolerance.defaultTolerance.distance, - )) { + assert(padding != null); + if (oldPadding?.almostEqualTo(padding!) != true) { setState(() {}); } } diff --git a/package/lib/src/addon/gutter.dart b/package/lib/src/addon/gutter.dart index 0d2a580..ebf6c59 100644 --- a/package/lib/src/addon/gutter.dart +++ b/package/lib/src/addon/gutter.dart @@ -1,6 +1,6 @@ import 'package:exprollable_page_view/src/core/controller.dart'; import 'package:exprollable_page_view/src/core/view.dart'; -import 'package:flutter/physics.dart'; +import 'package:exprollable_page_view/src/internal/utils.dart'; import 'package:flutter/widgets.dart'; /// Insert spaces at both sides of the wrapped page. @@ -64,11 +64,7 @@ class _PageGutterState extends State { void invalidateState() { final oldDeltaX = deltaX; correctState(); - if (!nearEqual( - oldDeltaX, - deltaX, - Tolerance.defaultTolerance.distance, - )) { + if (!oldDeltaX.almostEqualTo(deltaX)) { setState(() {}); } } diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 680efc4..159984f 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -372,13 +372,11 @@ mixin PageViewportMetrics on ViewportMetrics { /// Indicates if the viewport is fully shrunk. bool get isShrunk => - nearEqual(offset, shrunkOffset, Tolerance.defaultTolerance.distance) || - offset > shrunkOffset; + offset.almostEqualTo(shrunkOffset) || offset > shrunkOffset; // Indicates if the viewport is fully expanded. bool get isExpanded => - nearEqual(offset, expandedOffset, Tolerance.defaultTolerance.distance) || - offset < expandedOffset; + offset.almostEqualTo(expandedOffset) || offset < expandedOffset; } /// A snapshot of the state of the conceptual viewport. diff --git a/package/lib/src/internal/scroll.dart b/package/lib/src/internal/scroll.dart index d3783f8..82a2d8f 100644 --- a/package/lib/src/internal/scroll.dart +++ b/package/lib/src/internal/scroll.dart @@ -1,10 +1,9 @@ import 'dart:math'; import 'package:exprollable_page_view/src/internal/utils.dart'; -import 'package:flutter/physics.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; class ScrollAbsorber extends ChangeNotifier { double _capacity; @@ -39,16 +38,10 @@ class ScrollAbsorber extends ChangeNotifier { final oldOverflow = _overflow; _absorbedPixels = min(pixels, capacity); _overflow = max(0.0, pixels - capacity); - if (!nearEqual( - absorbedPixels, - oldAbsorbedPixels, - Tolerance.defaultTolerance.distance, - ) || - !nearEqual( - overflow, - oldOverflow, - Tolerance.defaultTolerance.distance, - )) { + assert(absorbedPixels != null); + assert(overflow != null); + if (oldAbsorbedPixels?.almostEqualTo(absorbedPixels!) != true || + oldOverflow?.almostEqualTo(overflow!) != true) { notifyListeners(); } } @@ -326,7 +319,7 @@ class AbsorbScrollPosition extends ScrollPositionWithSingleContext { required Duration duration, required Curve curve, }) { - if (nearEqual(to, impliedPixels, physics.tolerance.distance)) { + if (impliedPixels.almostEqualTo(to)) { // Skip the animation, go straight to the position as we are already close. jumpTo(to); return Future.value(); From 62a0a567bca5dac01af3eb41b0546963dfb74160 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Fri, 12 May 2023 19:39:32 +0900 Subject: [PATCH 02/24] fix analysis errors --- package/analysis_options.yaml | 1 - package/lib/src/core/controller.dart | 5 ++--- package/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/package/analysis_options.yaml b/package/analysis_options.yaml index 61b6c4d..fd16f92 100644 --- a/package/analysis_options.yaml +++ b/package/analysis_options.yaml @@ -24,6 +24,5 @@ linter: rules: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 159984f..312bf82 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -6,7 +6,6 @@ import 'package:exprollable_page_view/src/core/view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/physics.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; @@ -838,14 +837,14 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { if (snapTo == null) { return super.createBallisticSimulation(position, velocity); } - return (position.pixels - snapTo).abs() < tolerance.distance + return position.pixels.almostEqualTo(snapTo) ? null : ScrollSpringSimulation( spring, position.pixels, snapTo, velocity, - tolerance: tolerance, + tolerance: Tolerance.defaultTolerance, ); } } diff --git a/package/pubspec.yaml b/package/pubspec.yaml index 0dff7a5..d2b3ba2 100644 --- a/package/pubspec.yaml +++ b/package/pubspec.yaml @@ -15,4 +15,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^2.0.1 From b9898753d1b890110ce049bb63f7548ec28c6a11 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sat, 13 May 2023 16:15:18 +0900 Subject: [PATCH 03/24] :sparkles: Allow customization of viewport fraction calculations --- package/lib/src/core/controller.dart | 64 ++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 312bf82..678a919 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -95,6 +95,9 @@ class ExprollablePageController extends PageController { /// [ViewportOffset.explored] and [ViewportOffset.shrunk] are set to be snaped by default. /// If you specify additional offsets, you may need to also specify `maxViewportOffset` /// to be able to drag the page to the additional snap offsets larger than [ViewportOffset.shrunk]. + /// + /// Specifying `viewportFractionBehavior` allows you to control how the viewport fraction changes + /// along with vertical scrolling. [DefaultViewportFractionBehavior] is used by default. ExprollablePageController({ super.initialPage, super.keepPage, @@ -106,12 +109,15 @@ class ExprollablePageController extends PageController { ViewportOffset.expanded, ViewportOffset.shrunk, ], + ViewportFractionBehavior viewportFractionBehavior = + const DefaultViewportFractionBehavior(), }) : assert(0 <= minViewportFraction && minViewportFraction <= 1.0), super(viewportFraction: minViewportFraction) { final snapOffsets = [...snapViewportOffsets]..sort(); viewport = PageViewport( minFraction: viewportFraction, absorber: _absorberGroup, + fractionBehavior: viewportFractionBehavior, overshootEffect: overshootEffect, initialOffset: initialViewportOffset, maxOffset: maxViewportOffset, @@ -482,6 +488,40 @@ class PageViewportUpdateNotification extends Notification { final PageViewportMetrics metrics; } +/// Describes how the viewport fraction changes when the page is scrolled vertically . +/// +/// Use the convenient [DefaultViewportFractionBehavior], which implements the default behavior, +/// or extend this class and override [preferredFraction] to create a custom behavior. +abstract class ViewportFractionBehavior { + /// Calculate the viewport fraction according to the state of the current viewport and the new offset. + /// + /// This method is called by [PageViewport] whenevery the fraction should be updated. + double preferredFraction(PageViewportMetrics viewport, double newOffset); +} + +/// The default implementation of [ViewportFractionBehavior]. +class DefaultViewportFractionBehavior implements ViewportFractionBehavior { + /// Create the default implementation of [ViewportFractionBehavior]. + /// + /// The viewport fraction changes along the [curve] from 0 to 1, + /// where the viewport offset is equal to [PageViewportMetrics.shrunkOffset] + /// and [PageViewportMetrics.expandedOffset], respectively. + const DefaultViewportFractionBehavior({this.curve = Curves.easeIn}); + + /// The curve of the viewport fraction. + final Curve curve; + + @override + double preferredFraction(PageViewportMetrics viewport, double newOffset) { + assert(viewport.hasDimensions); + final pixels = newOffset - viewport.expandedOffset; + final delta = viewport.shrunkOffset - viewport.expandedOffset; + assert(delta > 0.0); + final t = 1.0 - (pixels / delta).clamp(0.0, 1.0); + return curve.transform(t) * viewport.deltaFraction + viewport.minFraction; + } +} + /// An object that represents the state of the **conceptual** viewport. /// /// "Conceptual" means that the actual measurements for each page is calculated according to the state of this object, @@ -500,6 +540,7 @@ class PageViewport extends ChangeNotifier PageViewport({ required this.minFraction, required this.overshootEffect, + required this.fractionBehavior, required ScrollAbsorber absorber, required ViewportOffset initialOffset, required ViewportOffset maxOffset, @@ -510,6 +551,9 @@ class PageViewport extends ChangeNotifier _absorber.addListener(_invalidateState); } + /// Describes how the [fraction] changes along with vertical scrolling. + final ViewportFractionBehavior fractionBehavior; + final ViewportOffset _maxOffset; final ViewportOffset _initialOffset; final ScrollAbsorber _absorber; @@ -609,23 +653,15 @@ class PageViewport extends ChangeNotifier } double _computeFraction() { - assert(_absorber.pixels != null); assert(hasDimensions); - - final a = _absorber; final dim = dimensions; - - final offset = max(0.0, _computeOffset()); + final offset = _computeOffset(); + final pixels = max(0.0, offset); final lowerBoundFraction = overshootEffect - ? (dim.height - dim.padding.bottom - offset) / dim.height - : (dim.height - offset) / dim.height; - - final delta = shrunkOffset - expandedOffset; - assert(delta > 0.0); - final t = 1.0 - (a.absorbedPixels! / delta).clamp(0.0, 1.0); - const curve = Curves.easeIn; - final fraction = curve.transform(t) * deltaFraction + minFraction; - return max(lowerBoundFraction, fraction); + ? (dim.height - dim.padding.bottom - pixels) / dim.height + : (dim.height - pixels) / dim.height; + final preferredFraction = fractionBehavior.preferredFraction(this, offset); + return max(lowerBoundFraction, preferredFraction); } } From 3514902f5676626fe0f820995d0442e3f2cbf30a Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sat, 13 May 2023 18:23:35 +0900 Subject: [PATCH 04/24] update docs --- package/lib/src/core/controller.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 678a919..5518597 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -490,22 +490,23 @@ class PageViewportUpdateNotification extends Notification { /// Describes how the viewport fraction changes when the page is scrolled vertically . /// -/// Use the convenient [DefaultViewportFractionBehavior], which implements the default behavior, +/// Use the convenient [DefaultViewportFractionBehavior] which implements the default behavior, /// or extend this class and override [preferredFraction] to create a custom behavior. abstract class ViewportFractionBehavior { /// Calculate the viewport fraction according to the state of the current viewport and the new offset. - /// - /// This method is called by [PageViewport] whenevery the fraction should be updated. + /// + /// This method is called by [PageViewport] whenever the fraction should be updated. + /// The calculated fraction must be 0 when [PageViewportMetrics.offset] is greater than or equal to [PageViewportMetrics.shrunkOffset], + /// and must be 1 when [PageViewportMetrics.offset] is less than or equal to [PageViewportMetrics.expandedOffset]. + /// There's no restriction in the other cases, but it will usually took a value between 0 and 1. double preferredFraction(PageViewportMetrics viewport, double newOffset); } /// The default implementation of [ViewportFractionBehavior]. class DefaultViewportFractionBehavior implements ViewportFractionBehavior { /// Create the default implementation of [ViewportFractionBehavior]. - /// - /// The viewport fraction changes along the [curve] from 0 to 1, - /// where the viewport offset is equal to [PageViewportMetrics.shrunkOffset] - /// and [PageViewportMetrics.expandedOffset], respectively. + /// + /// The calculated viewport fractions take values between 0 and 1 along the [curve]. const DefaultViewportFractionBehavior({this.curve = Curves.easeIn}); /// The curve of the viewport fraction. From 37ab7f8150ccfa6bd9b588fdcfad0c6207819ad5 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sat, 13 May 2023 18:37:57 +0900 Subject: [PATCH 05/24] fix docs --- package/lib/src/core/controller.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 5518597..dbe9fc4 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -496,9 +496,11 @@ abstract class ViewportFractionBehavior { /// Calculate the viewport fraction according to the state of the current viewport and the new offset. /// /// This method is called by [PageViewport] whenever the fraction should be updated. - /// The calculated fraction must be 0 when [PageViewportMetrics.offset] is greater than or equal to [PageViewportMetrics.shrunkOffset], - /// and must be 1 when [PageViewportMetrics.offset] is less than or equal to [PageViewportMetrics.expandedOffset]. - /// There's no restriction in the other cases, but it will usually took a value between 0 and 1. + /// The calculated fraction must be [PageViewportMetrics.minFraction] + /// when [PageViewportMetrics.offset] is greater than or equal to [PageViewportMetrics.shrunkOffset], + /// and must be [PageViewportMetrics.maxFraction] when [PageViewportMetrics.offset] is less than or equal to [PageViewportMetrics.expandedOffset]. + /// There's no restriction in the other cases, but it will usually took a value + /// between [PageViewportMetrics.minFraction] and [PageViewportMetrics.maxFraction]. double preferredFraction(PageViewportMetrics viewport, double newOffset); } From 29b4be510bf0d39ed22a06f475b68be555e5b1ef Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sun, 14 May 2023 14:18:44 +0900 Subject: [PATCH 06/24] fix #24 --- package/lib/src/addon/modal.dart | 2 +- package/lib/src/core/controller.dart | 281 +++++++++++++-------------- 2 files changed, 139 insertions(+), 144 deletions(-) diff --git a/package/lib/src/addon/modal.dart b/package/lib/src/addon/modal.dart index 15da314..39b1054 100644 --- a/package/lib/src/addon/modal.dart +++ b/package/lib/src/addon/modal.dart @@ -62,7 +62,7 @@ class ModalExprollable extends StatefulWidget { this.dismissBehavior = _defaultDismissBehavior, this.barrierDismissible = true, this.dismissThresholdOffset = const ViewportOffset.fractional(0.1), - }) : assert(dismissThresholdOffset > ViewportOffset.shrunk); + }); /// Called when the dialog should be dismissed. /// The default behavior is to pop the dialog diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index dbe9fc4..2cc6866 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -92,38 +92,74 @@ class ExprollablePageController extends PageController { /// Create a page controller. /// /// `snapViewportOffsets` is used to specify the viewport offsets that the active page will snap to. - /// [ViewportOffset.explored] and [ViewportOffset.shrunk] are set to be snaped by default. + /// [ViewportOffset.expanded] and [ViewportOffset.shrunk] are set to be snaped by default. /// If you specify additional offsets, you may need to also specify `maxViewportOffset` /// to be able to drag the page to the additional snap offsets larger than [ViewportOffset.shrunk]. /// /// Specifying `viewportFractionBehavior` allows you to control how the viewport fraction changes /// along with vertical scrolling. [DefaultViewportFractionBehavior] is used by default. + /// + /// If [overshootEffect] is enabled, the upper segment of the active page will slightly exceed the top of the viewport when it goes fullscreen. + /// To be precise, this means that the viewport offset will take a negative value when the viewport fraction is 1.0. + /// This trick creates a dynamic visual effect when the page goes fullscreen. + /// The figures below are a demonstration of how the overshoot effect affects (disabled in the left, enabled in the right). + /// + /// ![overshoot-disabled](https://user-images.githubusercontent.com/68946713/231827343-155a750d-b21f-4a96-b81a-74c8873c46cb.gif) ![overshoot-enabled](https://user-images.githubusercontent.com/68946713/231827364-40843efc-5a91-49ff-ab74-c9af1e4b0c62.gif) + /// + /// Overshoot effect will works correctly only if: + /// + /// - `MediaQuery.padding.bottom` > 0 + /// - Ther lower segment of `ExprollablePageView` is behind a widget such as `NavigationBar`, `BottomAppBar` + /// + /// Perhaps the most common use is to wrap an `ExprollablePageView` with a `Scaffold`. In that case, do not forget to enable `Scaffold.extentBody` and then everything should be fine. + /// + /// ```dart + /// controller = ExprollablePageController(overshootEffect: true); + /// + /// Widget build(BuildContext context) { + /// return Scaffold( + /// extendBody: true, + /// bottomNavigationBar: BottomNavigationBar(...), + /// body: ExprollablePageView( + /// controller: controller, + /// itemBuilder: (context, page) { ... }, + /// ), + /// ); + /// } + /// ``` + /// ExprollablePageController({ super.initialPage, super.keepPage, double minViewportFraction = 0.9, bool overshootEffect = false, - ViewportOffset initialViewportOffset = ViewportOffset.shrunk, - ViewportOffset maxViewportOffset = ViewportOffset.shrunk, - List snapViewportOffsets = const [ - ViewportOffset.expanded, - ViewportOffset.shrunk, - ], + ViewportOffset shrunkViewportOffset = ViewportOffset.shrunk, + ViewportOffset? maxViewportOffset, + ViewportOffset? initialViewportOffset, + List? snapViewportOffsets, ViewportFractionBehavior viewportFractionBehavior = const DefaultViewportFractionBehavior(), }) : assert(0 <= minViewportFraction && minViewportFraction <= 1.0), super(viewportFraction: minViewportFraction) { - final snapOffsets = [...snapViewportOffsets]..sort(); viewport = PageViewport( minFraction: viewportFraction, absorber: _absorberGroup, fractionBehavior: viewportFractionBehavior, - overshootEffect: overshootEffect, - initialOffset: initialViewportOffset, - maxOffset: maxViewportOffset, + initialOffset: initialViewportOffset ?? shrunkViewportOffset, + maxOffset: maxViewportOffset ?? shrunkViewportOffset, + expandedOffset: ViewportOffset.expanded( + overshootEffect: overshootEffect, + ), + shrunkOffset: shrunkViewportOffset, ); _snapPhysics = _SnapViewportOffsetPhysics( - snapOffsets: snapOffsets, + snapOffsets: snapViewportOffsets ?? + [ + ViewportOffset.expanded( + overshootEffect: overshootEffect, + ), + shrunkViewportOffset, + ], viewport: viewport, ); _currentPage = _CurrentPageNotifier(controller: this); @@ -148,11 +184,12 @@ class ExprollablePageController extends PageController { }) { assert(additionalSnapOffsets.isNotEmpty); final snapViewportOffsets = { - ViewportOffset.expanded, + ViewportOffset.expanded( + overshootEffect: overshootEffect, + ), ViewportOffset.shrunk, ...additionalSnapOffsets, - }.toList() - ..sort(); + }.toList(); return ExprollablePageController( initialPage: initialPage, keepPage: keepPage, @@ -337,38 +374,6 @@ mixin ViewportMetrics { /// A description of the state of the **conceptual** viewport. mixin PageViewportMetrics on ViewportMetrics { - /// Inidicates if overshoot effect is enabled. If [overshootEffect] is enabled, - /// the upper segment of the active page will slightly exceed the top of the viewport when it goes fullscreen. - /// To be precise, this means that the viewport offset will take a negative value when the viewport fraction is 1.0. - /// This trick creates a dynamic visual effect when the page goes fullscreen. - /// The figures below are a demonstration of how the overshoot effect affects (disabled in the left, enabled in the right). - /// - /// ![overshoot-disabled](https://user-images.githubusercontent.com/68946713/231827343-155a750d-b21f-4a96-b81a-74c8873c46cb.gif) ![overshoot-enabled](https://user-images.githubusercontent.com/68946713/231827364-40843efc-5a91-49ff-ab74-c9af1e4b0c62.gif) - /// - /// Overshoot effect will works correctly only if: - /// - /// - `MediaQuery.padding.bottom` > 0 - /// - Ther lower segment of `ExprollablePageView` is behind a widget such as `NavigationBar`, `BottomAppBar` - /// - /// Perhaps the most common use is to wrap an `ExprollablePageView` with a `Scaffold`. In that case, do not forget to enable `Scaffold.extentBody` and then everything should be fine. - /// - /// ```dart - /// controller = ExprollablePageController(overshootEffect: true); - /// - /// Widget build(BuildContext context) { - /// return Scaffold( - /// extendBody: true, - /// bottomNavigationBar: BottomNavigationBar(...), - /// body: ExprollablePageView( - /// controller: controller, - /// itemBuilder: (context, page) { ... }, - /// ), - /// ); - /// } - /// ``` - /// - bool get overshootEffect; - /// The lower bound of the offset at which the viewport is fully shrunk. double get shrunkOffset; @@ -398,7 +403,6 @@ class StaticPageViewportMetrics with ViewportMetrics, PageViewportMetrics { required this.shrunkOffset, required this.expandedOffset, required this.dimensions, - required this.overshootEffect, }); /// Create a [StaticPageViewportMetrics] copying another [PageViewportMetrics]. @@ -415,7 +419,6 @@ class StaticPageViewportMetrics with ViewportMetrics, PageViewportMetrics { shrunkOffset: metrics.shrunkOffset, expandedOffset: metrics.expandedOffset, dimensions: metrics.dimensions, - overshootEffect: metrics.overshootEffect, ); @override @@ -445,9 +448,6 @@ class StaticPageViewportMetrics with ViewportMetrics, PageViewportMetrics { @override final ViewportDimensions dimensions; - @override - final bool overshootEffect; - @override bool get hasDimensions => true; @@ -505,10 +505,11 @@ abstract class ViewportFractionBehavior { } /// The default implementation of [ViewportFractionBehavior]. +/// +/// The calculated viewport fractions take values between [PageViewportMetrics.minFraction] +/// and [PageViewportMetrics.maxFraction], along the [curve]. class DefaultViewportFractionBehavior implements ViewportFractionBehavior { /// Create the default implementation of [ViewportFractionBehavior]. - /// - /// The calculated viewport fractions take values between 0 and 1 along the [curve]. const DefaultViewportFractionBehavior({this.curve = Curves.easeIn}); /// The curve of the viewport fraction. @@ -542,15 +543,18 @@ class PageViewport extends ChangeNotifier /// Creates an object that represents the state of the **conceptual** viewport. PageViewport({ required this.minFraction, - required this.overshootEffect, required this.fractionBehavior, required ScrollAbsorber absorber, required ViewportOffset initialOffset, required ViewportOffset maxOffset, + required ViewportOffset expandedOffset, + required ViewportOffset shrunkOffset, }) : assert(0.0 <= minFraction && minFraction <= 1.0), _absorber = absorber, _maxOffset = maxOffset, - _initialOffset = initialOffset { + _initialOffset = initialOffset, + _expandedOffset = expandedOffset, + _shrunkOffset = shrunkOffset { _absorber.addListener(_invalidateState); } @@ -559,11 +563,10 @@ class PageViewport extends ChangeNotifier final ViewportOffset _maxOffset; final ViewportOffset _initialOffset; + final ViewportOffset _expandedOffset; + final ViewportOffset _shrunkOffset; final ScrollAbsorber _absorber; - @override - final bool overshootEffect; - @override PageViewportMetrics get value => this; @@ -607,11 +610,10 @@ class PageViewport extends ChangeNotifier } @override - double get expandedOffset => - const ExpandedViewportOffset().toConcreteValue(this); + double get expandedOffset => _expandedOffset.toConcreteValue(this); @override - double get shrunkOffset => const ShrunkViewportOffset().toConcreteValue(this); + double get shrunkOffset => _shrunkOffset.toConcreteValue(this); double get _initialAbsorberPixels { final initialOffset = _initialOffset.toConcreteValue(this); @@ -625,6 +627,26 @@ class PageViewport extends ChangeNotifier @internal void correctForNewDimensions(ViewportDimensions dimensions) { _dimensions = dimensions; + + assert( + minOffset <= expandedOffset, + "Invalid order of offset properties: " + "minOffset <= expandedOffset must be satisfied, " + "but minOffset is $minOffset and expandedOffset is $expandedOffset.", + ); + assert( + expandedOffset <= shrunkOffset, + "Invalid order of offset properties: " + "expandedOffset <= shrunkOffset must be satisfied, " + "but expandedOffset is $expandedOffset and shrunkOffset is $shrunkOffset.", + ); + assert( + shrunkOffset <= maxOffset, + "Invalid order of offset properties: " + "shrunkOffset <= maxOffset must be satisfied, " + "but shrunkOffset is $shrunkOffset and maxOffset is $maxOffset.", + ); + _absorber.correct((it) { it.capacity = deltaOffset; if (it.pixels == null) { @@ -659,10 +681,8 @@ class PageViewport extends ChangeNotifier assert(hasDimensions); final dim = dimensions; final offset = _computeOffset(); - final pixels = max(0.0, offset); - final lowerBoundFraction = overshootEffect - ? (dim.height - dim.padding.bottom - pixels) / dim.height - : (dim.height - pixels) / dim.height; + final lowerBoundFraction = + (dim.height - dim.padding.bottom - max(0.0, offset)) / dim.height; final preferredFraction = fractionBehavior.preferredFraction(this, offset); return max(lowerBoundFraction, preferredFraction); } @@ -851,13 +871,16 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { if (velocity.abs() > thresholdVelocity) return null; if (snapOffsets.isEmpty) return null; - assert( - listEquals(snapOffsets, [...snapOffsets]..sort()), - "'snapOffsets' must be sorted in ascending order.", - ); + assert((() { + final snaps = + snapOffsets.map((s) => s.toConcreteValue(viewport)).toList(); + return listEquals(snaps, [...snaps]..sort()); + })(), "'snapOffsets' must be sorted in ascending order."); - final minSnap = snapOffsets.last.toScrollOffset(viewport); - final maxSnap = snapOffsets.first.toScrollOffset(viewport); + final snapScrollOffsets = + snapOffsets.map((s) => s.toScrollOffset(viewport)).toList(); + final minSnap = snapScrollOffsets.last; + final maxSnap = snapScrollOffsets.first; if (position.pixels < minSnap || position.pixels > maxSnap) { return null; } @@ -866,7 +889,7 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { double nearest(double p, double q) => (pixels - p).abs() < (pixels - q).abs() ? p : q; - return snapOffsets.map((it) => it.toScrollOffset(viewport)).reduce(nearest); + return snapScrollOffsets.reduce(nearest); } @override @@ -901,20 +924,24 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { /// - `ViewportOffset.fractional(1.0) < ViewportOffset.fractional(0.0)` /// /// ![viewport-offsets](https://user-images.githubusercontent.com/68946713/231827251-fed9575c-980a-40b8-b01a-da984d58f3ec.png) -@sealed -abstract class ViewportOffset implements Comparable { +abstract class ViewportOffset { /// The offset at which the viewport is fully expanded /// (more precisely, when [PageViewport.fraction] is equal to [PageViewport.maxFraction]). - static const expanded = ExpandedViewportOffset(); + const factory ViewportOffset.expanded({bool overshootEffect}) = + DefaultExpandedViewportOffset; /// The offset at which the viewport is fully shrunk /// (more precisely, when [PageViewport.fraction] is equal to [PageViewport.minFraction]). - static const shrunk = ShrunkViewportOffset(); + static const shrunk = DefaultShrunkViewportOffset(); /// Create an user defined viewport offset from a fractional value. /// [fraction] must be between 0.0 and 1.0. - const factory ViewportOffset.fractional(double fraction) = - FractionalViewportOffset; + const factory ViewportOffset.fractional( + double fraction, { + bool relativeToExpandedViewport, + }) = FractionalViewportOffset; + + const factory ViewportOffset.fixed(double pixels) = FixedViewportOffset; const ViewportOffset(); @@ -923,48 +950,30 @@ abstract class ViewportOffset implements Comparable { double toConcreteValue(PageViewportMetrics metrics); /// Convert the offset to a scroll offset for [ScrollPosition]. + @nonVirtual double toScrollOffset(PageViewportMetrics metrics) { final offset = toConcreteValue(metrics); assert(offset >= metrics.minOffset); return -1 * (offset - metrics.minOffset); } - - bool operator >(ViewportOffset other) => compareTo(other) > 0; - bool operator <(ViewportOffset other) => compareTo(other) < 0; - bool operator >=(ViewportOffset other) => this > other || this == other; - bool operator <=(ViewportOffset other) => this < other || this == other; } /// The upper bound of the offset at which the viewport is fully expanded. -class ExpandedViewportOffset extends ViewportOffset { - const ExpandedViewportOffset(); - - @override - double toConcreteValue(PageViewportMetrics metrics) { - return metrics.overshootEffect - ? -1 * metrics.dimensions.padding.bottom - : 0.0; - } - - @override - int compareTo(ViewportOffset other) { - if (other is FractionalViewportOffset || other is ShrunkViewportOffset) { - return -1; - } - assert(other is ExpandedViewportOffset); - return 0; - } +class DefaultExpandedViewportOffset extends ViewportOffset { + const DefaultExpandedViewportOffset({ + this.overshootEffect = false, + }); - @override - bool operator ==(Object other) => runtimeType == other.runtimeType; + final bool overshootEffect; @override - int get hashCode => runtimeType.hashCode; + double toConcreteValue(PageViewportMetrics metrics) => + overshootEffect ? -1 * metrics.dimensions.padding.bottom : 0.0; } /// The lower bound of the offset at which the viewport is fully shrunk. -class ShrunkViewportOffset extends ViewportOffset { - const ShrunkViewportOffset(); +class DefaultShrunkViewportOffset extends ViewportOffset { + const DefaultShrunkViewportOffset(); @override double toConcreteValue(PageViewportMetrics metrics) { @@ -975,21 +984,6 @@ class ShrunkViewportOffset extends ViewportOffset { (1.0 - metrics.minFraction) * metrics.dimensions.height; return max(preferredOffset, lowerBoundOffset); } - - @override - int compareTo(ViewportOffset other) { - if (other is ExpandedViewportOffset) return 1; - if (other is ShrunkViewportOffset) return 0; - assert(other is FractionalViewportOffset); - final fraction = (other as FractionalViewportOffset).fraction; - return fraction == 0.0 ? 0 : -1; - } - - @override - bool operator ==(Object other) => runtimeType == other.runtimeType; - - @override - int get hashCode => runtimeType.hashCode; } /// A viewport offset that is defined by a fractional value. @@ -999,36 +993,37 @@ class ShrunkViewportOffset extends ViewportOffset { class FractionalViewportOffset extends ViewportOffset { /// Creates a viewport offset from a fractional value. /// [fraction] must be between 0.0 and 1.0. - const FractionalViewportOffset(this.fraction) - : assert(0.0 <= fraction && fraction <= 1.0); + const FractionalViewportOffset( + this.fraction, { + this.relativeToExpandedViewport = false, + }) : assert(0.0 <= fraction && fraction <= 1.0); /// The fractional value of the offset. final double fraction; + final bool relativeToExpandedViewport; + @override double toConcreteValue(PageViewportMetrics metrics) { - return fraction * - (metrics.dimensions.height - - metrics.dimensions.padding.bottom - - metrics.shrunkOffset) + - metrics.shrunkOffset; + return relativeToExpandedViewport + ? fraction * + (metrics.dimensions.height - + metrics.dimensions.padding.bottom - + max(0.0, metrics.expandedOffset)) + + max(0.0, metrics.expandedOffset) + : fraction * + (metrics.dimensions.height - + metrics.dimensions.padding.bottom - + metrics.shrunkOffset) + + metrics.shrunkOffset; } +} - @override - int compareTo(ViewportOffset other) { - if (other is ExpandedViewportOffset) return 1; - if (other is ShrunkViewportOffset) return fraction == 0.0 ? 0 : 1; - assert(other is FractionalViewportOffset); - return fraction.compareTo((other as FractionalViewportOffset).fraction); - } +class FixedViewportOffset extends ViewportOffset { + const FixedViewportOffset(this.pixels); - @override - bool operator ==(Object other) => - identical(this, other) || - (other is FractionalViewportOffset && - runtimeType == other.runtimeType && - fraction == other.fraction); + final double pixels; @override - int get hashCode => Object.hash(runtimeType, fraction); + double toConcreteValue(PageViewportMetrics metrics) => pixels; } From 23268041ea02a259e4f6e6650556a9ede42b28a0 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sun, 14 May 2023 18:33:38 +0900 Subject: [PATCH 07/24] make minOffset customizable --- example/lib/src/overshoot_effect_example.dart | 18 +++-- package/lib/src/core/controller.dart | 81 +++++++++---------- 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/example/lib/src/overshoot_effect_example.dart b/example/lib/src/overshoot_effect_example.dart index d9418db..c3493f4 100644 --- a/example/lib/src/overshoot_effect_example.dart +++ b/example/lib/src/overshoot_effect_example.dart @@ -16,11 +16,19 @@ class _OvershootEffectExampleState extends State { void initState() { super.initState(); controller = ExprollablePageController( - // Make sure that your Scaffold has a bottom navigation bar, - // and Scaffold.extendBody is set true. You should avoid using - // SafeArea for the top of the screen for better visual effect. - overshootEffect: true, - ); + // Make sure that your Scaffold has a bottom navigation bar, + // and Scaffold.extendBody is set true. You should avoid using + // SafeArea for the top of the screen for better visual effect. + overshootEffect: true, + minViewportOffset: ViewportOffset.overshoot, + maxViewportOffset: ViewportOffset.fractional(0.6), + shrunkViewportOffset: ViewportOffset.fractional(0.4), + expandedViewportOffset: ViewportOffset.fractional(0.2), + initialViewportOffset: ViewportOffset.fractional(0.6), + snapViewportOffsets: [ + ViewportOffset.overshoot, + ViewportOffset.fractional(0.6), + ]); } @override diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 2cc6866..d7b8cf5 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -98,7 +98,7 @@ class ExprollablePageController extends PageController { /// /// Specifying `viewportFractionBehavior` allows you to control how the viewport fraction changes /// along with vertical scrolling. [DefaultViewportFractionBehavior] is used by default. - /// + /// /// If [overshootEffect] is enabled, the upper segment of the active page will slightly exceed the top of the viewport when it goes fullscreen. /// To be precise, this means that the viewport offset will take a negative value when the viewport fraction is 1.0. /// This trick creates a dynamic visual effect when the page goes fullscreen. @@ -134,6 +134,8 @@ class ExprollablePageController extends PageController { double minViewportFraction = 0.9, bool overshootEffect = false, ViewportOffset shrunkViewportOffset = ViewportOffset.shrunk, + ViewportOffset? expandedViewportOffset, + ViewportOffset? minViewportOffset, ViewportOffset? maxViewportOffset, ViewportOffset? initialViewportOffset, List? snapViewportOffsets, @@ -141,23 +143,22 @@ class ExprollablePageController extends PageController { const DefaultViewportFractionBehavior(), }) : assert(0 <= minViewportFraction && minViewportFraction <= 1.0), super(viewportFraction: minViewportFraction) { + final expandedOffset = expandedViewportOffset ?? + (overshootEffect ? ViewportOffset.overshoot : ViewportOffset.expanded); viewport = PageViewport( minFraction: viewportFraction, absorber: _absorberGroup, fractionBehavior: viewportFractionBehavior, initialOffset: initialViewportOffset ?? shrunkViewportOffset, + minOffset: minViewportOffset ?? expandedOffset, maxOffset: maxViewportOffset ?? shrunkViewportOffset, - expandedOffset: ViewportOffset.expanded( - overshootEffect: overshootEffect, - ), + expandedOffset: expandedOffset, shrunkOffset: shrunkViewportOffset, ); _snapPhysics = _SnapViewportOffsetPhysics( snapOffsets: snapViewportOffsets ?? [ - ViewportOffset.expanded( - overshootEffect: overshootEffect, - ), + expandedOffset, shrunkViewportOffset, ], viewport: viewport, @@ -183,13 +184,12 @@ class ExprollablePageController extends PageController { ViewportOffset? maxViewportOffset, }) { assert(additionalSnapOffsets.isNotEmpty); - final snapViewportOffsets = { - ViewportOffset.expanded( - overshootEffect: overshootEffect, - ), + final snapViewportOffsets = [ + if (overshootEffect) ViewportOffset.overshoot, + if (!overshootEffect) ViewportOffset.expanded, ViewportOffset.shrunk, ...additionalSnapOffsets, - }.toList(); + ]; return ExprollablePageController( initialPage: initialPage, keepPage: keepPage, @@ -546,11 +546,13 @@ class PageViewport extends ChangeNotifier required this.fractionBehavior, required ScrollAbsorber absorber, required ViewportOffset initialOffset, + required ViewportOffset minOffset, required ViewportOffset maxOffset, required ViewportOffset expandedOffset, required ViewportOffset shrunkOffset, }) : assert(0.0 <= minFraction && minFraction <= 1.0), _absorber = absorber, + _minOffset = minOffset, _maxOffset = maxOffset, _initialOffset = initialOffset, _expandedOffset = expandedOffset, @@ -561,6 +563,7 @@ class PageViewport extends ChangeNotifier /// Describes how the [fraction] changes along with vertical scrolling. final ViewportFractionBehavior fractionBehavior; + final ViewportOffset _minOffset; final ViewportOffset _maxOffset; final ViewportOffset _initialOffset; final ViewportOffset _expandedOffset; @@ -585,7 +588,7 @@ class PageViewport extends ChangeNotifier double get maxOffset => _maxOffset.toConcreteValue(this); @override - double get minOffset => expandedOffset; + double get minOffset => _minOffset.toConcreteValue(this); double? _offset; @@ -927,19 +930,18 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { abstract class ViewportOffset { /// The offset at which the viewport is fully expanded /// (more precisely, when [PageViewport.fraction] is equal to [PageViewport.maxFraction]). - const factory ViewportOffset.expanded({bool overshootEffect}) = - DefaultExpandedViewportOffset; + static const expanded = DefaultExpandedViewportOffset(); /// The offset at which the viewport is fully shrunk /// (more precisely, when [PageViewport.fraction] is equal to [PageViewport.minFraction]). static const shrunk = DefaultShrunkViewportOffset(); + static const overshoot = OvershootViewportOffset(); + /// Create an user defined viewport offset from a fractional value. /// [fraction] must be between 0.0 and 1.0. - const factory ViewportOffset.fractional( - double fraction, { - bool relativeToExpandedViewport, - }) = FractionalViewportOffset; + const factory ViewportOffset.fractional(double fraction) = + FractionalViewportOffset; const factory ViewportOffset.fixed(double pixels) = FixedViewportOffset; @@ -958,17 +960,20 @@ abstract class ViewportOffset { } } +class OvershootViewportOffset extends ViewportOffset { + const OvershootViewportOffset(); + + @override + double toConcreteValue(PageViewportMetrics metrics) => + -1 * metrics.dimensions.padding.bottom; +} + /// The upper bound of the offset at which the viewport is fully expanded. class DefaultExpandedViewportOffset extends ViewportOffset { - const DefaultExpandedViewportOffset({ - this.overshootEffect = false, - }); - - final bool overshootEffect; + const DefaultExpandedViewportOffset(); @override - double toConcreteValue(PageViewportMetrics metrics) => - overshootEffect ? -1 * metrics.dimensions.padding.bottom : 0.0; + double toConcreteValue(PageViewportMetrics metrics) => 0.0; } /// The lower bound of the offset at which the viewport is fully shrunk. @@ -993,30 +998,16 @@ class DefaultShrunkViewportOffset extends ViewportOffset { class FractionalViewportOffset extends ViewportOffset { /// Creates a viewport offset from a fractional value. /// [fraction] must be between 0.0 and 1.0. - const FractionalViewportOffset( - this.fraction, { - this.relativeToExpandedViewport = false, - }) : assert(0.0 <= fraction && fraction <= 1.0); + const FractionalViewportOffset(this.fraction) + : assert(0.0 <= fraction && fraction <= 1.0); /// The fractional value of the offset. final double fraction; - final bool relativeToExpandedViewport; - @override - double toConcreteValue(PageViewportMetrics metrics) { - return relativeToExpandedViewport - ? fraction * - (metrics.dimensions.height - - metrics.dimensions.padding.bottom - - max(0.0, metrics.expandedOffset)) + - max(0.0, metrics.expandedOffset) - : fraction * - (metrics.dimensions.height - - metrics.dimensions.padding.bottom - - metrics.shrunkOffset) + - metrics.shrunkOffset; - } + double toConcreteValue(PageViewportMetrics metrics) => + fraction * + (metrics.dimensions.height - metrics.dimensions.padding.bottom); } class FixedViewportOffset extends ViewportOffset { From 049653a72d819e76ba0cd4b50e5d3d9852519fa9 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Sun, 14 May 2023 21:10:41 +0900 Subject: [PATCH 08/24] fix minor bug --- package/lib/src/addon/modal.dart | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/package/lib/src/addon/modal.dart b/package/lib/src/addon/modal.dart index 39b1054..3f35885 100644 --- a/package/lib/src/addon/modal.dart +++ b/package/lib/src/addon/modal.dart @@ -16,7 +16,7 @@ Future showModalExprollable( Color initialBarrierColor = Colors.black54, void Function(BuildContext) dismissBehavior = _defaultDismissBehavior, bool barrierDismissible = true, - ViewportOffset dismissThresholdOffset = const ViewportOffset.fractional(0.1), + ViewportOffset dismissThresholdOffset = const DismissThresholdOffset(), }) => showDialog( context: context, @@ -42,6 +42,18 @@ Future showModalExprollable( void _defaultDismissBehavior(BuildContext context) => Navigator.of(context).pop(); +class DismissThresholdOffset extends ViewportOffset { + const DismissThresholdOffset({ + this.dragMargin = 86.0, + }); + + final double dragMargin; + + @override + double toConcreteValue(PageViewportMetrics metrics) => + metrics.shrunkOffset + dragMargin; +} + /// A widget that makes a modal dialog style [ExprollablePageView]. /// /// This widget adds a translucent background (barrier) and @@ -61,7 +73,7 @@ class ModalExprollable extends StatefulWidget { this.initialBarrierColor = Colors.black54, this.dismissBehavior = _defaultDismissBehavior, this.barrierDismissible = true, - this.dismissThresholdOffset = const ViewportOffset.fractional(0.1), + this.dismissThresholdOffset = const DismissThresholdOffset(), }); /// Called when the dialog should be dismissed. @@ -132,10 +144,11 @@ class _ModalExprollableState extends State { assert(lastViewportMetrics != null); assert(lastViewportMetrics!.hasDimensions); final vp = lastViewportMetrics!; - final maxOverscroll = - widget.dismissThresholdOffset.toConcreteValue(vp) - vp.shrunkOffset; + final dismissThresholdOffset = + widget.dismissThresholdOffset.toConcreteValue(vp); + assert(dismissThresholdOffset > vp.shrunkOffset); + final maxOverscroll = dismissThresholdOffset - vp.shrunkOffset; final overscroll = max(0.0, vp.offset - vp.maxOffset); - assert(maxOverscroll > 0.0); barrierColorFraction.value = (overscroll / maxOverscroll).clamp(0.0, 1.0); } From 29fb7f62e08c1316b4a98509113d851b15c3c37c Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Mon, 15 May 2023 00:49:47 +0900 Subject: [PATCH 09/24] introduce ViewportConfiguration --- package/lib/src/core/controller.dart | 194 +++++++++++++-------------- 1 file changed, 94 insertions(+), 100 deletions(-) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index d7b8cf5..984de5c 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -131,76 +131,23 @@ class ExprollablePageController extends PageController { ExprollablePageController({ super.initialPage, super.keepPage, - double minViewportFraction = 0.9, - bool overshootEffect = false, - ViewportOffset shrunkViewportOffset = ViewportOffset.shrunk, - ViewportOffset? expandedViewportOffset, - ViewportOffset? minViewportOffset, - ViewportOffset? maxViewportOffset, - ViewportOffset? initialViewportOffset, - List? snapViewportOffsets, + ViewportConfiguration viewportConfiguration = + ViewportConfiguration.defaultConfiguration, ViewportFractionBehavior viewportFractionBehavior = const DefaultViewportFractionBehavior(), - }) : assert(0 <= minViewportFraction && minViewportFraction <= 1.0), - super(viewportFraction: minViewportFraction) { - final expandedOffset = expandedViewportOffset ?? - (overshootEffect ? ViewportOffset.overshoot : ViewportOffset.expanded); + }) : super(viewportFraction: viewportConfiguration.minFraction) { viewport = PageViewport( - minFraction: viewportFraction, absorber: _absorberGroup, fractionBehavior: viewportFractionBehavior, - initialOffset: initialViewportOffset ?? shrunkViewportOffset, - minOffset: minViewportOffset ?? expandedOffset, - maxOffset: maxViewportOffset ?? shrunkViewportOffset, - expandedOffset: expandedOffset, - shrunkOffset: shrunkViewportOffset, + configuration: viewportConfiguration, ); _snapPhysics = _SnapViewportOffsetPhysics( - snapOffsets: snapViewportOffsets ?? - [ - expandedOffset, - shrunkViewportOffset, - ], + snapOffsets: viewportConfiguration.snapOffsets, viewport: viewport, ); _currentPage = _CurrentPageNotifier(controller: this); } - /// Crate a page controller with additional snap viewport offsets. - /// - /// [additionalSnapOffsets] must not be empty. The viewport will snap to - /// the offsets given by [additionalSnapOffsets] in addition to - /// [ViewportOffset.expanded] and [ViewportOffset.shrunk]. - /// - /// If [initialViewportOffset] or [maxViewportOffset] is not specified, - /// the max offset in [additionalSnapOffsets] is used. - factory ExprollablePageController.withAdditionalSnapOffsets( - List additionalSnapOffsets, { - int initialPage = 0, - bool keepPage = true, - double minViewportFraction = 0.9, - bool overshootEffect = false, - ViewportOffset? initialViewportOffset, - ViewportOffset? maxViewportOffset, - }) { - assert(additionalSnapOffsets.isNotEmpty); - final snapViewportOffsets = [ - if (overshootEffect) ViewportOffset.overshoot, - if (!overshootEffect) ViewportOffset.expanded, - ViewportOffset.shrunk, - ...additionalSnapOffsets, - ]; - return ExprollablePageController( - initialPage: initialPage, - keepPage: keepPage, - minViewportFraction: minViewportFraction, - overshootEffect: overshootEffect, - initialViewportOffset: initialViewportOffset ?? snapViewportOffsets.last, - maxViewportOffset: maxViewportOffset ?? snapViewportOffsets.last, - snapViewportOffsets: snapViewportOffsets, - ); - } - final _absorberGroup = ScrollAbsorberGroup(); final Map _contentScrollControllers = {}; @@ -526,6 +473,72 @@ class DefaultViewportFractionBehavior implements ViewportFractionBehavior { } } +class ViewportConfiguration { + static const defaultConfiguration = ViewportConfiguration.raw( + minFraction: 0.9, + maxFraction: 1.0, + minOffset: ViewportOffset.expanded, + maxOffset: ViewportOffset.shrunk, + shrunkOffset: ViewportOffset.shrunk, + expandedOffset: ViewportOffset.expanded, + initialOffset: ViewportOffset.shrunk, + snapOffsets: [ViewportOffset.expanded, ViewportOffset.shrunk], + ); + + const ViewportConfiguration.raw({ + required this.minFraction, + required this.maxFraction, + required this.minOffset, + required this.maxOffset, + required this.shrunkOffset, + required this.expandedOffset, + required this.initialOffset, + required this.snapOffsets, + }); + + /// [extraSnapOffsets] must not be empty. The viewport will snap to + /// the offsets given by [extraSnapOffsets] in addition to + /// [ViewportOffset.expanded] and [ViewportOffset.shrunk]. + /// + /// If [initialOffset] is not specified, the max offset in [extraSnapOffsets] is used. + factory ViewportConfiguration({ + bool overshootEffect = false, + double minFraction = 0.9, + double maxFraction = 1.0, + ViewportOffset shrunkOffset = ViewportOffset.shrunk, + ViewportOffset? maxOffset, + ViewportOffset? initialOffset, + List extraSnapOffsets = const [], + }) { + final expandedOffset = + overshootEffect ? ViewportOffset.overshoot : ViewportOffset.expanded; + final snapOffsets = [ + expandedOffset, + shrunkOffset, + ...extraSnapOffsets, + ]; + return ViewportConfiguration.raw( + minFraction: minFraction, + maxFraction: maxFraction, + minOffset: expandedOffset, + maxOffset: snapOffsets.last, + shrunkOffset: shrunkOffset, + expandedOffset: expandedOffset, + initialOffset: initialOffset ?? snapOffsets.last, + snapOffsets: snapOffsets, + ); + } + + final double minFraction; + final double maxFraction; + final ViewportOffset minOffset; + final ViewportOffset maxOffset; + final ViewportOffset shrunkOffset; + final ViewportOffset expandedOffset; + final ViewportOffset initialOffset; + final List snapOffsets; +} + /// An object that represents the state of the **conceptual** viewport. /// /// "Conceptual" means that the actual measurements for each page is calculated according to the state of this object, @@ -542,32 +555,18 @@ class PageViewport extends ChangeNotifier implements ValueListenable { /// Creates an object that represents the state of the **conceptual** viewport. PageViewport({ - required this.minFraction, required this.fractionBehavior, + required this.configuration, required ScrollAbsorber absorber, - required ViewportOffset initialOffset, - required ViewportOffset minOffset, - required ViewportOffset maxOffset, - required ViewportOffset expandedOffset, - required ViewportOffset shrunkOffset, - }) : assert(0.0 <= minFraction && minFraction <= 1.0), - _absorber = absorber, - _minOffset = minOffset, - _maxOffset = maxOffset, - _initialOffset = initialOffset, - _expandedOffset = expandedOffset, - _shrunkOffset = shrunkOffset { + }) : _absorber = absorber { _absorber.addListener(_invalidateState); } /// Describes how the [fraction] changes along with vertical scrolling. final ViewportFractionBehavior fractionBehavior; - final ViewportOffset _minOffset; - final ViewportOffset _maxOffset; - final ViewportOffset _initialOffset; - final ViewportOffset _expandedOffset; - final ViewportOffset _shrunkOffset; + final ViewportConfiguration configuration; + final ScrollAbsorber _absorber; @override @@ -585,10 +584,10 @@ class PageViewport extends ChangeNotifier bool get hasDimensions => _dimensions != null; @override - double get maxOffset => _maxOffset.toConcreteValue(this); + double get maxOffset => configuration.maxOffset.toConcreteValue(this); @override - double get minOffset => _minOffset.toConcreteValue(this); + double get minOffset => configuration.minOffset.toConcreteValue(this); double? _offset; @@ -599,10 +598,10 @@ class PageViewport extends ChangeNotifier } @override - final double minFraction; + double get minFraction => configuration.minFraction; @override - double get maxFraction => 1.0; + double get maxFraction => configuration.maxFraction; double? _fraction; @@ -613,13 +612,14 @@ class PageViewport extends ChangeNotifier } @override - double get expandedOffset => _expandedOffset.toConcreteValue(this); + double get expandedOffset => + configuration.expandedOffset.toConcreteValue(this); @override - double get shrunkOffset => _shrunkOffset.toConcreteValue(this); + double get shrunkOffset => configuration.shrunkOffset.toConcreteValue(this); double get _initialAbsorberPixels { - final initialOffset = _initialOffset.toConcreteValue(this); + final initialOffset = configuration.initialOffset.toConcreteValue(this); assert(initialOffset >= minOffset); return initialOffset - minOffset; } @@ -661,8 +661,17 @@ class PageViewport extends ChangeNotifier } void _correctState() { - _fraction = _computeFraction(); - _offset = _computeOffset(); + assert(_absorber.pixels != null); + assert(hasDimensions); + final newOffset = minOffset + _absorber.pixels!; + final dim = dimensions; + final lowerBoundFraction = + (dim.height - dim.padding.bottom - max(0.0, newOffset)) / dim.height; + final preferredFraction = + fractionBehavior.preferredFraction(this, newOffset); + + _fraction = max(lowerBoundFraction, preferredFraction); + _offset = newOffset; } void _invalidateState() { @@ -674,21 +683,6 @@ class PageViewport extends ChangeNotifier notifyListeners(); } } - - double _computeOffset() { - assert(_absorber.pixels != null); - return minOffset + _absorber.pixels!; - } - - double _computeFraction() { - assert(hasDimensions); - final dim = dimensions; - final offset = _computeOffset(); - final lowerBoundFraction = - (dim.height - dim.padding.bottom - max(0.0, offset)) / dim.height; - final preferredFraction = fractionBehavior.preferredFraction(this, offset); - return max(lowerBoundFraction, preferredFraction); - } } /// Stores the actual metrics of the viewport for a specific [page]. From 71e4f827ad35aa0d861800c74d521e673205aa84 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Mon, 15 May 2023 01:13:01 +0900 Subject: [PATCH 10/24] rewrite with ViewportConfiguration --- example/ios/Runner.xcodeproj/project.pbxproj | 1 + example/lib/src/adaptive_padding_example.dart | 9 ++++++--- .../lib/src/complex_example/album_details.dart | 4 +++- .../lib/src/custom_snap_offsets_example.dart | 8 ++++++-- example/lib/src/overshoot_effect_example.dart | 18 ++++++------------ maps_example/lib/main.dart | 10 +++------- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 26389ec..f0f8f7d 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -176,6 +176,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( diff --git a/example/lib/src/adaptive_padding_example.dart b/example/lib/src/adaptive_padding_example.dart index d42ac07..72f660c 100644 --- a/example/lib/src/adaptive_padding_example.dart +++ b/example/lib/src/adaptive_padding_example.dart @@ -6,8 +6,7 @@ class AdaptivePaddingExample extends StatefulWidget { const AdaptivePaddingExample({super.key}); @override - State createState() => - _AdaptivePaddingExampleState(); + State createState() => _AdaptivePaddingExampleState(); } class _AdaptivePaddingExampleState extends State { @@ -16,7 +15,11 @@ class _AdaptivePaddingExampleState extends State { @override void initState() { super.initState(); - controller = ExprollablePageController(overshootEffect: true); + controller = ExprollablePageController( + viewportConfiguration: ViewportConfiguration( + overshootEffect: true, + ), + ); } @override diff --git a/example/lib/src/complex_example/album_details.dart b/example/lib/src/complex_example/album_details.dart index 8b13f37..2669850 100644 --- a/example/lib/src/complex_example/album_details.dart +++ b/example/lib/src/complex_example/album_details.dart @@ -38,7 +38,9 @@ class _AlbumDetailsDialogState extends ConsumerState { super.initState(); controller = ExprollablePageController( initialPage: widget.index, - overshootEffect: true, + viewportConfiguration: ViewportConfiguration( + overshootEffect: true, + ), ); } diff --git a/example/lib/src/custom_snap_offsets_example.dart b/example/lib/src/custom_snap_offsets_example.dart index 986df1c..129b2cb 100644 --- a/example/lib/src/custom_snap_offsets_example.dart +++ b/example/lib/src/custom_snap_offsets_example.dart @@ -16,8 +16,12 @@ class _CustomSnapOffsetsExampleState extends State { @override void initState() { super.initState(); - controller = ExprollablePageController.withAdditionalSnapOffsets( - const [ViewportOffset.fractional(0.5)], + controller = ExprollablePageController( + viewportConfiguration: ViewportConfiguration( + extraSnapOffsets: [ + ViewportOffset.fractional(0.5), + ], + ), ); } diff --git a/example/lib/src/overshoot_effect_example.dart b/example/lib/src/overshoot_effect_example.dart index c3493f4..0cb879e 100644 --- a/example/lib/src/overshoot_effect_example.dart +++ b/example/lib/src/overshoot_effect_example.dart @@ -16,19 +16,13 @@ class _OvershootEffectExampleState extends State { void initState() { super.initState(); controller = ExprollablePageController( - // Make sure that your Scaffold has a bottom navigation bar, - // and Scaffold.extendBody is set true. You should avoid using - // SafeArea for the top of the screen for better visual effect. + // Make sure that your Scaffold has a bottom navigation bar, + // and Scaffold.extendBody is set true. You should avoid using + // SafeArea for the top of the screen for better visual effect. + viewportConfiguration: ViewportConfiguration( overshootEffect: true, - minViewportOffset: ViewportOffset.overshoot, - maxViewportOffset: ViewportOffset.fractional(0.6), - shrunkViewportOffset: ViewportOffset.fractional(0.4), - expandedViewportOffset: ViewportOffset.fractional(0.2), - initialViewportOffset: ViewportOffset.fractional(0.6), - snapViewportOffsets: [ - ViewportOffset.overshoot, - ViewportOffset.fractional(0.6), - ]); + ), + ); } @override diff --git a/maps_example/lib/main.dart b/maps_example/lib/main.dart index 69f4a65..877c567 100644 --- a/maps_example/lib/main.dart +++ b/maps_example/lib/main.dart @@ -25,13 +25,9 @@ class _MapsExampleState extends State { super.initState(); const peekOffset = ViewportOffset.fractional(0.7); _pageController = ExprollablePageController( - maxViewportOffset: peekOffset, - initialViewportOffset: peekOffset, - snapViewportOffsets: [ - ViewportOffset.expanded, - // ViewportOffset.shrunk, - peekOffset, - ], + viewportConfiguration: ViewportConfiguration( + extraSnapOffsets: [peekOffset], + ), ); } From 9347481c727d3ed027fa7e9b60964ab7b6993b75 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Mon, 15 May 2023 23:00:12 +0900 Subject: [PATCH 11/24] add doc comments --- package/lib/src/core/controller.dart | 196 ++++++++++++++++++--------- 1 file changed, 130 insertions(+), 66 deletions(-) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 984de5c..99dd093 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -6,7 +6,6 @@ import 'package:exprollable_page_view/src/core/view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; /// An inherited widget used in [ExprollablePageView] to provides @@ -86,48 +85,16 @@ class _CurrentPageNotifier extends ValueNotifier { /// A controller for [ExprollablePageView]. /// -/// A [ExprollablePageController] lets you manipulate which page is visible in a [ExprollablePageView]. +/// This lets you manipulate which page is visible in a [ExprollablePageView]. /// It also can be used to programmatically change the viewport state. class ExprollablePageController extends PageController { /// Create a page controller. /// - /// `snapViewportOffsets` is used to specify the viewport offsets that the active page will snap to. - /// [ViewportOffset.expanded] and [ViewportOffset.shrunk] are set to be snaped by default. - /// If you specify additional offsets, you may need to also specify `maxViewportOffset` - /// to be able to drag the page to the additional snap offsets larger than [ViewportOffset.shrunk]. - /// - /// Specifying `viewportFractionBehavior` allows you to control how the viewport fraction changes + /// Specifying [viewportFractionBehavior] allows you to control how the viewport fraction changes /// along with vertical scrolling. [DefaultViewportFractionBehavior] is used by default. /// - /// If [overshootEffect] is enabled, the upper segment of the active page will slightly exceed the top of the viewport when it goes fullscreen. - /// To be precise, this means that the viewport offset will take a negative value when the viewport fraction is 1.0. - /// This trick creates a dynamic visual effect when the page goes fullscreen. - /// The figures below are a demonstration of how the overshoot effect affects (disabled in the left, enabled in the right). - /// - /// ![overshoot-disabled](https://user-images.githubusercontent.com/68946713/231827343-155a750d-b21f-4a96-b81a-74c8873c46cb.gif) ![overshoot-enabled](https://user-images.githubusercontent.com/68946713/231827364-40843efc-5a91-49ff-ab74-c9af1e4b0c62.gif) - /// - /// Overshoot effect will works correctly only if: - /// - /// - `MediaQuery.padding.bottom` > 0 - /// - Ther lower segment of `ExprollablePageView` is behind a widget such as `NavigationBar`, `BottomAppBar` - /// - /// Perhaps the most common use is to wrap an `ExprollablePageView` with a `Scaffold`. In that case, do not forget to enable `Scaffold.extentBody` and then everything should be fine. - /// - /// ```dart - /// controller = ExprollablePageController(overshootEffect: true); - /// - /// Widget build(BuildContext context) { - /// return Scaffold( - /// extendBody: true, - /// bottomNavigationBar: BottomNavigationBar(...), - /// body: ExprollablePageView( - /// controller: controller, - /// itemBuilder: (context, page) { ... }, - /// ), - /// ); - /// } - /// ``` - /// + /// To configure the metrics of the viewport, specify [viewportConfiguration] with the desired values. + /// [ViewportConfiguration.defaultConfiguration] is used as the default configuration. ExprollablePageController({ super.initialPage, super.keepPage, @@ -286,10 +253,14 @@ mixin ViewportMetrics { /// [fraction] is between [minFraction] and [maxFraction] including both edges. double get fraction; + /// {@template exprollable_page_view.controller.ViewportMetrics.minFraction} /// The lower bound of [fraction]. + /// {@endtemplate} double get minFraction; + /// {@template exprollable_page_view.controller.ViewportMetrics.maxFraction} /// The upper bound of [fraction]. + /// {@endtemplate} double get maxFraction; /// The distance from the top of the viewport to the top of the current page. @@ -300,10 +271,14 @@ mixin ViewportMetrics { /// [offset] will exceeds [maxOffset] according to the physics. double get offset; + /// {@template exprollable_page_view.controller.ViewportMetrics.minOffset} /// The lower bound of the offset. + /// {@endtemplate} double get minOffset; + /// {@template exprollable_page_view.controller.ViewportMetrics.maxOffset} /// The upper bound of the offset. The actual [offset] might exceeds this value. + /// {@endtemplate} double get maxOffset; /// Calculate the difference between [minOffset] and [maxOffset]. @@ -321,10 +296,14 @@ mixin ViewportMetrics { /// A description of the state of the **conceptual** viewport. mixin PageViewportMetrics on ViewportMetrics { + /// {@template exprollable_page_view.controller.PageViewportMetrics.shrunkOffset} /// The lower bound of the offset at which the viewport is fully shrunk. + /// {@endtemplate} double get shrunkOffset; + /// {@template exprollable_page_view.controller.PageViewportMetrics.expandedOffset} /// The upper bound of the offset at which the viewport is fully expanded. + /// {@endtemplate} double get expandedOffset; /// Indicates if the viewport is fully shrunk. @@ -473,7 +452,9 @@ class DefaultViewportFractionBehavior implements ViewportFractionBehavior { } } +/// A configuration for the viewport. class ViewportConfiguration { + /// A const object to be used as the default configuration of [PageViewport]. static const defaultConfiguration = ViewportConfiguration.raw( minFraction: 0.9, maxFraction: 1.0, @@ -485,6 +466,10 @@ class ViewportConfiguration { snapOffsets: [ViewportOffset.expanded, ViewportOffset.shrunk], ); + /// A general constructor for [ViewportConfiguration]. + /// + /// It is recommended to use [ViewportConfiguration.new], + /// which is a convenient constructor sufficient for most use cases. const ViewportConfiguration.raw({ required this.minFraction, required this.maxFraction, @@ -496,17 +481,57 @@ class ViewportConfiguration { required this.snapOffsets, }); - /// [extraSnapOffsets] must not be empty. The viewport will snap to - /// the offsets given by [extraSnapOffsets] in addition to - /// [ViewportOffset.expanded] and [ViewportOffset.shrunk]. + /// Create a configuration for standard use cases. + /// + /// If [extraSnapOffsets] is not empty, viewport will snap to the offsets + /// given by [extraSnapOffsets] in addition to [ViewportOffset.expanded] and [ViewportOffset.shrunk]. + /// The list must be sorted in ascending order by the actual offset value + /// calculated from [ViewportOffset.toConcreteValue]. + /// + /// If [initialOffset] is not specified, the last element in [extraSnapOffsets] + /// is used as the initial offset. If [extraSnapOffsets] is also not specified, + /// [initialOffset] is set to [shrunkOffset]. + /// + /// If [overshootEffect] is enabled, the upper segment of the active page will + /// slightly exceed the top of the viewport when it goes fullscreen. + /// To be precise, this means that the viewport offset will take a negative value + /// when the viewport fraction is 1.0. This trick creates a dynamic visual effect + /// when the page goes fullscreen. The figures below are a demonstration of + /// how the overshoot effect affects (disabled in the left, enabled in the right). + /// + /// ![overshoot-disabled](https://user-images.githubusercontent.com/68946713/231827343-155a750d-b21f-4a96-b81a-74c8873c46cb.gif) ![overshoot-enabled](https://user-images.githubusercontent.com/68946713/231827364-40843efc-5a91-49ff-ab74-c9af1e4b0c62.gif) + /// + /// Overshoot effect will works correctly only if: /// - /// If [initialOffset] is not specified, the max offset in [extraSnapOffsets] is used. + /// - [MediaQueryData.padding.data] > 0 + /// - Ther lower segment of [ExprollablePageView] is behind a widget such as [NavigationBar], [BottomAppBar] + /// + /// Perhaps the most common use is to wrap an [ExprollablePageView] with a [Scaffold]. + /// In that case, do not forget to enable [Scaffold.extentBody] and then everything should be fine. + /// + /// ```dart + /// controller = ExprollablePageController( + /// viewportConfiguration: ViewportConfiguration( + /// overshootEffect: true, + /// ), + /// ); + /// + /// Widget build(BuildContext context) { + /// return Scaffold( + /// extendBody: true, + /// bottomNavigationBar: BottomNavigationBar(...), + /// body: ExprollablePageView( + /// controller: controller, + /// itemBuilder: (context, page) { ... }, + /// ), + /// ); + /// } + /// ``` factory ViewportConfiguration({ bool overshootEffect = false, double minFraction = 0.9, double maxFraction = 1.0, ViewportOffset shrunkOffset = ViewportOffset.shrunk, - ViewportOffset? maxOffset, ViewportOffset? initialOffset, List extraSnapOffsets = const [], }) { @@ -529,13 +554,31 @@ class ViewportConfiguration { ); } + /// {@macro exprollable_page_view.controller.ViewportMetrics.minFraction} final double minFraction; + + /// {@macro exprollable_page_view.controller.ViewportMetrics.maxFraction} final double maxFraction; + + /// {@macro exprollable_page_view.controller.ViewportMetrics.minOffset} final ViewportOffset minOffset; + + /// {@macro exprollable_page_view.controller.ViewportMetrics.maxOffset} final ViewportOffset maxOffset; + + /// {@macro exprollable_page_view.controller.PageViewportMetrics.shrunkOffset} final ViewportOffset shrunkOffset; + + /// {@macro exprollable_page_view.controller.PageViewportMetrics.expandedOffset} final ViewportOffset expandedOffset; + + /// The initial viewport offset. final ViewportOffset initialOffset; + + /// The list of offsets that the viewport will snap to. + /// + /// The list must be sorted in ascending order by the actual offset value + /// calculated from [ViewportOffset.toConcreteValue]. final List snapOffsets; } @@ -565,6 +608,7 @@ class PageViewport extends ChangeNotifier /// Describes how the [fraction] changes along with vertical scrolling. final ViewportFractionBehavior fractionBehavior; + /// The configuration of the viewport. final ViewportConfiguration configuration; final ScrollAbsorber _absorber; @@ -909,36 +953,32 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { } /// An object that represents a viewport offset. -/// -/// There are 2 pre-defined offsets, [ViewportOffset.expanded] and [ViewportOffset.shrunk], -/// at which the viewport fraction is 1.0 and the minimum, respectively. -/// A user defined offset can be created from a fractional value using [ViewportOffset.fractional]. -/// For example, `ViewportOffset.fractional(1.0)` is equivalent to [ViewportOffset.shrunk], -/// and `ViewportOffset.fractional(0.0)` matches the bottom of the viewport. -/// [ViewportOffset]s are comarable. The order is: -/// - `ViewportOffset.expanded < ViewportOffset.shrunk` -/// - `ViewportOffset.shrunk == ViewportOffset.fractional(1.0)` -/// - `ViewportOffset.fractional(1.0) < ViewportOffset.fractional(0.0)` -/// -/// ![viewport-offsets](https://user-images.githubusercontent.com/68946713/231827251-fed9575c-980a-40b8-b01a-da984d58f3ec.png) +/// +/// There are 3 predefined [ViewportOffset]s: +/// - [ViewportOffset.expanded] : The default offset at which the page viewport is fully expanded. +/// - [ViewportOffset.shrunk] : The default offset at which the page viewport is fully shrunk. +/// - [ViewportOffset.overshoot] : The default offset at which the page viewport is fully expanded and overshot. +/// +/// User defined offsets can be created using [ViewportOffset.fixed] and [ViewportOffset.fractional], +/// or extend [ViewportOffset] to perform more complex calculations. abstract class ViewportOffset { - /// The offset at which the viewport is fully expanded - /// (more precisely, when [PageViewport.fraction] is equal to [PageViewport.maxFraction]). + /// {@macro exprollable_page_view.controller.DefaultExpandedViewportOffset} static const expanded = DefaultExpandedViewportOffset(); - /// The offset at which the viewport is fully shrunk - /// (more precisely, when [PageViewport.fraction] is equal to [PageViewport.minFraction]). + /// {@macro exprollable_page_view.controller.DefaultShrunkViewportOffset} static const shrunk = DefaultShrunkViewportOffset(); + /// {@macro exprollable_page_view.controller.OvershootViewportOffset} static const overshoot = OvershootViewportOffset(); - /// Create an user defined viewport offset from a fractional value. - /// [fraction] must be between 0.0 and 1.0. + /// {@macro exprollable_page_view.controller.FractionalViewportOffset.new} const factory ViewportOffset.fractional(double fraction) = FractionalViewportOffset; + /// {@macro exprollable_page_view.controller.FixedViewportOffset.new} const factory ViewportOffset.fixed(double pixels) = FixedViewportOffset; + /// Contructs a [ViewportOffset]. const ViewportOffset(); /// Calculate the concrete pixels represented by this object @@ -954,7 +994,11 @@ abstract class ViewportOffset { } } +/// {@template exprollable_page_view.controller.OvershootViewportOffset} +/// The default offset at which the viewport will be fully expanded and overshot. +/// {@endtemplate} class OvershootViewportOffset extends ViewportOffset { + /// Create the overshot viewport offset. const OvershootViewportOffset(); @override @@ -962,16 +1006,30 @@ class OvershootViewportOffset extends ViewportOffset { -1 * metrics.dimensions.padding.bottom; } -/// The upper bound of the offset at which the viewport is fully expanded. +/// {@template exprollable_page_view.controller.DefaultExpandedViewportOffset} +/// The default offset at which the viewport is fully expanded. +/// +/// The offset value is always 0.0. +/// {@endtemplate} class DefaultExpandedViewportOffset extends ViewportOffset { + /// Create the default expanded viewport offset. const DefaultExpandedViewportOffset(); @override double toConcreteValue(PageViewportMetrics metrics) => 0.0; } -/// The lower bound of the offset at which the viewport is fully shrunk. +/// {@template exprollable_page_view.controller.DefaultShrunkViewportOffset} +/// The default offset at which the viewport will be fully shrunk. +/// +/// The preferred offset value is the top padding plus 16.0 pixels, +/// but if it is less than the lower limit, it will be clamped to that value. +/// The lower limit is calculated by subtracting the height of the shrunk page +/// from the height of the viewport. This clamping process is necessary to prevent +/// unwanted white space between the bottom of the page and the viewport. +/// {@endtemplate} class DefaultShrunkViewportOffset extends ViewportOffset { + /// Create the default shrunk viewport offset. const DefaultShrunkViewportOffset(); @override @@ -986,12 +1044,13 @@ class DefaultShrunkViewportOffset extends ViewportOffset { } /// A viewport offset that is defined by a fractional value. -/// -/// `fraction == 1.0` is equivalent to [ViewportOffset.shrunk], -/// and `fraction == 0.0` corresponds to the bottom of the viewport excluding the padding. class FractionalViewportOffset extends ViewportOffset { + /// {@template exprollable_page_view.controller.FractionalViewportOffset.new} /// Creates a viewport offset from a fractional value. - /// [fraction] must be between 0.0 and 1.0. + /// + /// [fraction] is a relative value of the viewport height substracted + /// by the bottom padding and must be between 0.0 and 1.0. + /// {@endtemplate} const FractionalViewportOffset(this.fraction) : assert(0.0 <= fraction && fraction <= 1.0); @@ -1004,9 +1063,14 @@ class FractionalViewportOffset extends ViewportOffset { (metrics.dimensions.height - metrics.dimensions.padding.bottom); } +/// A viewport offset that is defined by a fixed value. class FixedViewportOffset extends ViewportOffset { + /// {@template exprollable_page_view.controller.FixedViewportOffset.new} + /// Creates a viewport offset from a fixed value. + /// {@endtemplate} const FixedViewportOffset(this.pixels); + /// The fixed value of the offset in terms of logical pixels. final double pixels; @override From 9e18fdee639303ec335d0e236f0f276b81d60b8b Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 16 May 2023 19:10:30 +0900 Subject: [PATCH 12/24] rename some classes and properties --- .../lib/src/custom_snap_offsets_example.dart | 4 +- maps_example/lib/main.dart | 4 +- package/lib/exprollable_page_view.dart | 2 +- package/lib/src/addon/adaptive_padding.dart | 29 +- package/lib/src/addon/gutter.dart | 2 +- package/lib/src/addon/modal.dart | 45 +- package/lib/src/core/controller.dart | 460 +++++++++--------- package/lib/src/core/view.dart | 24 +- 8 files changed, 276 insertions(+), 294 deletions(-) diff --git a/example/lib/src/custom_snap_offsets_example.dart b/example/lib/src/custom_snap_offsets_example.dart index 129b2cb..62d2199 100644 --- a/example/lib/src/custom_snap_offsets_example.dart +++ b/example/lib/src/custom_snap_offsets_example.dart @@ -18,8 +18,8 @@ class _CustomSnapOffsetsExampleState extends State { super.initState(); controller = ExprollablePageController( viewportConfiguration: ViewportConfiguration( - extraSnapOffsets: [ - ViewportOffset.fractional(0.5), + extraSnapInsets: [ + ViewportInset.fractional(0.5), ], ), ); diff --git a/maps_example/lib/main.dart b/maps_example/lib/main.dart index 877c567..a62c437 100644 --- a/maps_example/lib/main.dart +++ b/maps_example/lib/main.dart @@ -23,10 +23,10 @@ class _MapsExampleState extends State { @override void initState() { super.initState(); - const peekOffset = ViewportOffset.fractional(0.7); + const peekOffset = ViewportInset.fractional(0.7); _pageController = ExprollablePageController( viewportConfiguration: ViewportConfiguration( - extraSnapOffsets: [peekOffset], + extraSnapInsets: [peekOffset], ), ); } diff --git a/package/lib/exprollable_page_view.dart b/package/lib/exprollable_page_view.dart index 3520630..5c6777b 100644 --- a/package/lib/exprollable_page_view.dart +++ b/package/lib/exprollable_page_view.dart @@ -5,4 +5,4 @@ export 'package:exprollable_page_view/src/core/core.dart' hide InheritedExprollablePageController, InheritedPageContentScrollController, - InheritedViewportController; + InheritedPageViewport; diff --git a/package/lib/src/addon/adaptive_padding.dart b/package/lib/src/addon/adaptive_padding.dart index fee2a79..0d193e4 100644 --- a/package/lib/src/addon/adaptive_padding.dart +++ b/package/lib/src/addon/adaptive_padding.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:exprollable_page_view/src/core/controller.dart'; import 'package:exprollable_page_view/src/core/view.dart'; import 'package:exprollable_page_view/src/internal/utils.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart' hide Viewport; /// Inserts appropriate padding into the child widget according to the current viewpor offset. class AdaptivePagePadding extends StatefulWidget { @@ -29,29 +29,32 @@ class AdaptivePagePadding extends StatefulWidget { } class _AdaptivePagePaddingState extends State { - ViewportController? viewport; + Viewport? viewport; + PageViewport? pageViewport; double? padding; @override void dispose() { super.dispose(); - viewport?.removeListener(invalidateState); + pageViewport?.removeListener(invalidateState); } @override void didChangeDependencies() { super.didChangeDependencies(); - final viewport = ViewportController.of(context); + final viewport = ExprollablePageController.of(context)?.viewport; + final pageViewport = PageViewport.of(context); assert( - viewport != null, + pageViewport != null && viewport != null, "$AdaptivePagePadding can only be placed in a subtree of $ExprollablePageView.", ); - if (!identical(viewport, this.viewport)) { - this.viewport?.removeListener(invalidateState); - this.viewport = viewport!..addListener(invalidateState); - correctState(); + if (!identical(pageViewport, this.pageViewport)) { + this.pageViewport?.removeListener(invalidateState); + this.pageViewport = pageViewport!..addListener(invalidateState); } + this.viewport = viewport!; + correctState(); } @override @@ -71,10 +74,12 @@ class _AdaptivePagePaddingState extends State { void correctState() { assert(viewport != null); - final vp = viewport!; + assert(pageViewport != null); + final offset = pageViewport!.offset; + final topPadding = viewport!.dimensions.padding.top; padding = widget.useSafeArea - ? max(0.0, vp.dimensions.padding.top - vp.offset) - : max(0.0, -1 * vp.offset); + ? max(0.0, topPadding - offset) + : max(0.0, -1 * offset); } @override diff --git a/package/lib/src/addon/gutter.dart b/package/lib/src/addon/gutter.dart index ebf6c59..7974386 100644 --- a/package/lib/src/addon/gutter.dart +++ b/package/lib/src/addon/gutter.dart @@ -38,7 +38,7 @@ class _PageGutterState extends State { void didChangeDependencies() { super.didChangeDependencies(); final controller = ExprollablePageController.of(context); - final page = ViewportController.of(context)?.page; + final page = PageViewport.of(context)?.page; assert( controller != null && page != null, diff --git a/package/lib/src/addon/modal.dart b/package/lib/src/addon/modal.dart index 3f35885..d5efa7e 100644 --- a/package/lib/src/addon/modal.dart +++ b/package/lib/src/addon/modal.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:exprollable_page_view/src/core/controller.dart'; import 'package:exprollable_page_view/src/core/view.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Viewport; /// Show an [ExprollablePageView] as a modal dialog. Future showModalExprollable( @@ -16,7 +16,7 @@ Future showModalExprollable( Color initialBarrierColor = Colors.black54, void Function(BuildContext) dismissBehavior = _defaultDismissBehavior, bool barrierDismissible = true, - ViewportOffset dismissThresholdOffset = const DismissThresholdOffset(), + ViewportInset dismissThresholdInset = const DismissThresholdInset(), }) => showDialog( context: context, @@ -34,7 +34,7 @@ Future showModalExprollable( initialBarrierColor: initialBarrierColor, dismissBehavior: dismissBehavior, barrierDismissible: barrierDismissible, - dismissThresholdOffset: dismissThresholdOffset, + dismissThresholdInset: dismissThresholdInset, ), ), ); @@ -42,16 +42,16 @@ Future showModalExprollable( void _defaultDismissBehavior(BuildContext context) => Navigator.of(context).pop(); -class DismissThresholdOffset extends ViewportOffset { - const DismissThresholdOffset({ +class DismissThresholdInset extends ViewportInset { + const DismissThresholdInset({ this.dragMargin = 86.0, }); final double dragMargin; @override - double toConcreteValue(PageViewportMetrics metrics) => - metrics.shrunkOffset + dragMargin; + double toConcreteValue(ViewportMetrics metrics) => + metrics.shrunkInset + dragMargin; } /// A widget that makes a modal dialog style [ExprollablePageView]. @@ -73,17 +73,20 @@ class ModalExprollable extends StatefulWidget { this.initialBarrierColor = Colors.black54, this.dismissBehavior = _defaultDismissBehavior, this.barrierDismissible = true, - this.dismissThresholdOffset = const DismissThresholdOffset(), + this.dismissThresholdInset = const DismissThresholdInset(), }); /// Called when the dialog should be dismissed. + /// /// The default behavior is to pop the dialog /// by calling [Navigator.pop] without result value. final void Function(BuildContext) dismissBehavior; - /// The threshold offset at which the dialog - /// should be dismissed by *swipe down to dismiss* action. - final ViewportOffset dismissThresholdOffset; + /// The threshold inset used to trigger *swipe down to dismiss* action. + /// + /// When the [Viewport.inset] exceeds this threshold, + /// [dismissBehavior] is called to dismiss the dialog. + final ViewportInset dismissThresholdInset; /// Whether the dialog is dismissible by tapping the barrier. final bool barrierDismissible; @@ -106,7 +109,7 @@ class ModalExprollable extends StatefulWidget { class _ModalExprollableState extends State { final ValueNotifier barrierColorFraction = ValueNotifier(null); - PageViewportMetrics? lastViewportMetrics; + ViewportMetrics? lastViewportMetrics; @override void dispose() { @@ -114,7 +117,7 @@ class _ModalExprollableState extends State { barrierColorFraction.dispose(); } - void onViewportChanged(PageViewportMetrics metrics) { + void onViewportChanged(ViewportMetrics metrics) { lastViewportMetrics = metrics; invalidateBarrierColorFraction(); } @@ -128,8 +131,8 @@ class _ModalExprollableState extends State { bool shouldDismiss() { if (lastViewportMetrics != null && lastViewportMetrics!.hasDimensions) { final vp = lastViewportMetrics!; - final threshold = widget.dismissThresholdOffset.toConcreteValue(vp); - if (vp.offset > threshold) return true; + final threshold = widget.dismissThresholdInset.toConcreteValue(vp); + if (vp.inset > threshold) return true; } return false; } @@ -144,11 +147,11 @@ class _ModalExprollableState extends State { assert(lastViewportMetrics != null); assert(lastViewportMetrics!.hasDimensions); final vp = lastViewportMetrics!; - final dismissThresholdOffset = - widget.dismissThresholdOffset.toConcreteValue(vp); - assert(dismissThresholdOffset > vp.shrunkOffset); - final maxOverscroll = dismissThresholdOffset - vp.shrunkOffset; - final overscroll = max(0.0, vp.offset - vp.maxOffset); + final dismissThresholdInset = + widget.dismissThresholdInset.toConcreteValue(vp); + assert(dismissThresholdInset > vp.shrunkInset); + final maxOverscroll = dismissThresholdInset - vp.shrunkInset; + final overscroll = max(0.0, vp.inset - vp.maxInset); barrierColorFraction.value = (overscroll / maxOverscroll).clamp(0.0, 1.0); } @@ -174,7 +177,7 @@ class _ModalExprollableState extends State { final pageView = Listener( onPointerUp: (_) => onPointerUp(), - child: NotificationListener( + child: NotificationListener( onNotification: (notification) { onViewportChanged(notification.metrics); return false; diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 99dd093..df5b1c3 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -5,7 +5,7 @@ import 'package:exprollable_page_view/src/internal/utils.dart'; import 'package:exprollable_page_view/src/core/view.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide Viewport; import 'package:meta/meta.dart'; /// An inherited widget used in [ExprollablePageView] to provides @@ -28,21 +28,21 @@ class InheritedExprollablePageController extends InheritedWidget { !identical(controller, oldWidget.controller); } -/// An inherited widget that provides a [ViewportController] to its descendants. +/// An inherited widget that provides a [PageViewport] to its descendants. @internal -class InheritedViewportController extends InheritedWidget { - const InheritedViewportController({ +class InheritedPageViewport extends InheritedWidget { + const InheritedPageViewport({ super.key, required super.child, - required this.controller, + required this.pageView, }); - /// A provided controller. - final ViewportController controller; + /// A page viewport to be provided to the descendants. + final PageViewport pageView; @override - bool updateShouldNotify(InheritedViewportController oldWidget) => - !identical(controller, oldWidget.controller); + bool updateShouldNotify(InheritedPageViewport oldWidget) => + !identical(pageView, oldWidget.pageView); } /// An inherited widget that provides a [PageContentScrollController] to its descendants. @@ -103,13 +103,13 @@ class ExprollablePageController extends PageController { ViewportFractionBehavior viewportFractionBehavior = const DefaultViewportFractionBehavior(), }) : super(viewportFraction: viewportConfiguration.minFraction) { - viewport = PageViewport( + viewport = Viewport( absorber: _absorberGroup, fractionBehavior: viewportFractionBehavior, configuration: viewportConfiguration, ); - _snapPhysics = _SnapViewportOffsetPhysics( - snapOffsets: viewportConfiguration.snapOffsets, + _snapPhysics = _SnapViewportInsetPhysics( + snapInsets: viewportConfiguration.snapInsets, viewport: viewport, ); _currentPage = _CurrentPageNotifier(controller: this); @@ -118,7 +118,7 @@ class ExprollablePageController extends PageController { final _absorberGroup = ScrollAbsorberGroup(); final Map _contentScrollControllers = {}; - late final _SnapViewportOffsetPhysics _snapPhysics; + late final _SnapViewportInsetPhysics _snapPhysics; /// A notifier that stores the index of the current visible page. /// The new index is notified whenever the page that fully occupies the viewport changes. @@ -128,7 +128,7 @@ class ExprollablePageController extends PageController { /// An object that stores the viewport state. /// You can subscribe this object to get notified when the viewport state changes. - late final PageViewport viewport; + late final Viewport viewport; PageContentScrollController get _contentScrollController { assert(_contentScrollControllers.containsKey(currentPage.value)); @@ -168,21 +168,21 @@ class ExprollablePageController extends PageController { /// Animates the controlled [ExprollablePageView] from the current viewport offset /// to the given offset. - Future animateViewportOffsetTo( - ViewportOffset offset, { + Future animateViewportInsetTo( + ViewportInset inset, { required Curve curve, required Duration duration, }) { return _contentScrollController.animateTo( - offset.toScrollOffset(viewport), + inset.toScrollOffset(viewport), curve: curve, duration: duration, ); } /// instantly changes the current viewport offset without animation. - void jumpViewportOffsetTo(ViewportOffset offset) { - _contentScrollController.jumpTo(offset.toScrollOffset(viewport)); + void jumpViewportInsetTo(ViewportInset inset) { + _contentScrollController.jumpTo(inset.toScrollOffset(viewport)); } /// Obtians a controller from an ancestor [InheritedExprollablePageController] @@ -265,85 +265,82 @@ mixin ViewportMetrics { /// The distance from the top of the viewport to the top of the current page. /// - /// [offset] is always greater than or equals to [minOffset], but might exceeds [maxOffset]. + /// [inset] is always greater than or equals to [minInset], but might exceeds [maxInset]. /// For eample, if the scrollable widget in the current page uses [BouncingScrollPhysics] /// as its scroll physics and a user tries to overscroll the page, - /// [offset] will exceeds [maxOffset] according to the physics. - double get offset; + /// [inset] will exceeds [maxInset] according to the physics. + double get inset; /// {@template exprollable_page_view.controller.ViewportMetrics.minOffset} /// The lower bound of the offset. /// {@endtemplate} - double get minOffset; + double get minInset; /// {@template exprollable_page_view.controller.ViewportMetrics.maxOffset} - /// The upper bound of the offset. The actual [offset] might exceeds this value. + /// The upper bound of the offset. The actual [inset] might exceeds this value. /// {@endtemplate} - double get maxOffset; + double get maxInset; - /// Calculate the difference between [minOffset] and [maxOffset]. - double get deltaOffset { - assert(minOffset <= maxOffset); - return maxOffset - minOffset; + /// Calculate the difference between [minInset] and [maxInset]. + double get deltaInset { + assert(minInset <= maxInset); + return maxInset - minInset; } - /// Calculate the difference between [minFraction] and [maxOffset]. + /// Calculate the difference between [minFraction] and [maxInset]. double get deltaFraction { assert(minFraction <= maxFraction); return maxFraction - minFraction; } -} -/// A description of the state of the **conceptual** viewport. -mixin PageViewportMetrics on ViewportMetrics { - /// {@template exprollable_page_view.controller.PageViewportMetrics.shrunkOffset} + /// {@template exprollable_page_view.controller.ViewportMetrics.shrunkOffset} /// The lower bound of the offset at which the viewport is fully shrunk. /// {@endtemplate} - double get shrunkOffset; + double get shrunkInset; - /// {@template exprollable_page_view.controller.PageViewportMetrics.expandedOffset} + /// {@template exprollable_page_view.controller.ViewportMetrics.expandedOffset} /// The upper bound of the offset at which the viewport is fully expanded. /// {@endtemplate} - double get expandedOffset; + double get expandedInset; /// Indicates if the viewport is fully shrunk. - bool get isShrunk => - offset.almostEqualTo(shrunkOffset) || offset > shrunkOffset; + bool get isPageShrunk => + fraction.almostEqualTo(minFraction) || fraction < minFraction; // Indicates if the viewport is fully expanded. - bool get isExpanded => - offset.almostEqualTo(expandedOffset) || offset < expandedOffset; + bool get isPageExpanded => + fraction.almostEqualTo(maxFraction) || fraction > maxFraction; } /// A snapshot of the state of the conceptual viewport. @immutable -class StaticPageViewportMetrics with ViewportMetrics, PageViewportMetrics { +class StaticViewportMetrics with ViewportMetrics { /// Create a snapshot of the viewport state. - const StaticPageViewportMetrics({ + const StaticViewportMetrics({ required this.fraction, required this.minFraction, required this.maxFraction, - required this.offset, - required this.minOffset, - required this.maxOffset, - required this.shrunkOffset, - required this.expandedOffset, + required this.inset, + required this.minInset, + required this.maxInset, + required this.shrunkInset, + required this.expandedInset, required this.dimensions, }); - /// Create a [StaticPageViewportMetrics] copying another [PageViewportMetrics]. - factory StaticPageViewportMetrics.from( - PageViewportMetrics metrics, + /// Create a [StaticViewportMetrics] copying another [ViewportMetrics]. + factory StaticViewportMetrics.from( + ViewportMetrics metrics, ) => - StaticPageViewportMetrics( + StaticViewportMetrics( fraction: metrics.fraction, minFraction: metrics.minFraction, maxFraction: metrics.maxFraction, - offset: metrics.offset, - minOffset: metrics.minOffset, - maxOffset: metrics.maxOffset, - shrunkOffset: metrics.shrunkOffset, - expandedOffset: metrics.expandedOffset, + inset: metrics.inset, + minInset: metrics.minInset, + maxInset: metrics.maxInset, + shrunkInset: metrics.shrunkInset, + expandedInset: metrics.expandedInset, dimensions: metrics.dimensions, ); @@ -357,19 +354,19 @@ class StaticPageViewportMetrics with ViewportMetrics, PageViewportMetrics { final double maxFraction; @override - final double offset; + final double inset; @override - final double minOffset; + final double minInset; @override - final double maxOffset; + final double maxInset; @override - final double shrunkOffset; + final double shrunkInset; @override - final double expandedOffset; + final double expandedInset; @override final ViewportDimensions dimensions; @@ -380,16 +377,16 @@ class StaticPageViewportMetrics with ViewportMetrics, PageViewportMetrics { @override bool operator ==(Object other) => identical(this, other) || - (other is StaticPageViewportMetrics && + (other is StaticViewportMetrics && runtimeType == other.runtimeType && fraction == other.fraction && minFraction == other.minFraction && maxFraction == other.maxFraction && - offset == other.offset && - minOffset == other.minOffset && - maxOffset == other.maxOffset && - shrunkOffset == other.shrunkOffset && - expandedOffset == other.expandedOffset && + inset == other.inset && + minInset == other.minInset && + maxInset == other.maxInset && + shrunkInset == other.shrunkInset && + expandedInset == other.expandedInset && dimensions == other.dimensions); @override @@ -398,20 +395,20 @@ class StaticPageViewportMetrics with ViewportMetrics, PageViewportMetrics { fraction, minFraction, maxFraction, - offset, - minOffset, - maxOffset, - shrunkOffset, - expandedOffset, + inset, + minInset, + maxInset, + shrunkInset, + expandedInset, dimensions, ); } /// A notification that bubbles up the widget tree from a [ExprollablePageView] whenever the viewport state changes. /// Listening for this notification is equivalent to observe [ExprollablePageController.viewport]. -class PageViewportUpdateNotification extends Notification { - const PageViewportUpdateNotification(this.metrics); - final PageViewportMetrics metrics; +class ViewportUpdateNotification extends Notification { + const ViewportUpdateNotification(this.metrics); + final ViewportMetrics metrics; } /// Describes how the viewport fraction changes when the page is scrolled vertically . @@ -421,19 +418,19 @@ class PageViewportUpdateNotification extends Notification { abstract class ViewportFractionBehavior { /// Calculate the viewport fraction according to the state of the current viewport and the new offset. /// - /// This method is called by [PageViewport] whenever the fraction should be updated. - /// The calculated fraction must be [PageViewportMetrics.minFraction] - /// when [PageViewportMetrics.offset] is greater than or equal to [PageViewportMetrics.shrunkOffset], - /// and must be [PageViewportMetrics.maxFraction] when [PageViewportMetrics.offset] is less than or equal to [PageViewportMetrics.expandedOffset]. + /// This method is called by [Viewport] whenever the fraction should be updated. + /// The calculated fraction must be [ViewportMetrics.minFraction] + /// when [ViewportMetrics.inset] is greater than or equal to [ViewportMetrics.shrunkInset], + /// and must be [ViewportMetrics.maxFraction] when [ViewportMetrics.inset] is less than or equal to [ViewportMetrics.expandedInset]. /// There's no restriction in the other cases, but it will usually took a value - /// between [PageViewportMetrics.minFraction] and [PageViewportMetrics.maxFraction]. - double preferredFraction(PageViewportMetrics viewport, double newOffset); + /// between [ViewportMetrics.minFraction] and [ViewportMetrics.maxFraction]. + double preferredFraction(ViewportMetrics viewport, double newInset); } /// The default implementation of [ViewportFractionBehavior]. /// -/// The calculated viewport fractions take values between [PageViewportMetrics.minFraction] -/// and [PageViewportMetrics.maxFraction], along the [curve]. +/// The calculated viewport fractions take values between [ViewportMetrics.minFraction] +/// and [ViewportMetrics.maxFraction], along the [curve]. class DefaultViewportFractionBehavior implements ViewportFractionBehavior { /// Create the default implementation of [ViewportFractionBehavior]. const DefaultViewportFractionBehavior({this.curve = Curves.easeIn}); @@ -442,10 +439,10 @@ class DefaultViewportFractionBehavior implements ViewportFractionBehavior { final Curve curve; @override - double preferredFraction(PageViewportMetrics viewport, double newOffset) { + double preferredFraction(ViewportMetrics viewport, double newInset) { assert(viewport.hasDimensions); - final pixels = newOffset - viewport.expandedOffset; - final delta = viewport.shrunkOffset - viewport.expandedOffset; + final pixels = newInset - viewport.expandedInset; + final delta = viewport.shrunkInset - viewport.expandedInset; assert(delta > 0.0); final t = 1.0 - (pixels / delta).clamp(0.0, 1.0); return curve.transform(t) * viewport.deltaFraction + viewport.minFraction; @@ -454,16 +451,16 @@ class DefaultViewportFractionBehavior implements ViewportFractionBehavior { /// A configuration for the viewport. class ViewportConfiguration { - /// A const object to be used as the default configuration of [PageViewport]. + /// A const object to be used as the default configuration of [Viewport]. static const defaultConfiguration = ViewportConfiguration.raw( minFraction: 0.9, maxFraction: 1.0, - minOffset: ViewportOffset.expanded, - maxOffset: ViewportOffset.shrunk, - shrunkOffset: ViewportOffset.shrunk, - expandedOffset: ViewportOffset.expanded, - initialOffset: ViewportOffset.shrunk, - snapOffsets: [ViewportOffset.expanded, ViewportOffset.shrunk], + minInset: ViewportInset.expanded, + maxInset: ViewportInset.shrunk, + shrunkInset: ViewportInset.shrunk, + expandedInset: ViewportInset.expanded, + initialInset: ViewportInset.shrunk, + snapInsets: [ViewportInset.expanded, ViewportInset.shrunk], ); /// A general constructor for [ViewportConfiguration]. @@ -473,24 +470,24 @@ class ViewportConfiguration { const ViewportConfiguration.raw({ required this.minFraction, required this.maxFraction, - required this.minOffset, - required this.maxOffset, - required this.shrunkOffset, - required this.expandedOffset, - required this.initialOffset, - required this.snapOffsets, + required this.minInset, + required this.maxInset, + required this.shrunkInset, + required this.expandedInset, + required this.initialInset, + required this.snapInsets, }); /// Create a configuration for standard use cases. /// - /// If [extraSnapOffsets] is not empty, viewport will snap to the offsets - /// given by [extraSnapOffsets] in addition to [ViewportOffset.expanded] and [ViewportOffset.shrunk]. + /// If [extraSnapInsets] is not empty, viewport will snap to the offsets + /// given by [extraSnapInsets] in addition to [ViewportInset.expanded] and [ViewportInset.shrunk]. /// The list must be sorted in ascending order by the actual offset value - /// calculated from [ViewportOffset.toConcreteValue]. + /// calculated from [ViewportInset.toConcreteValue]. /// - /// If [initialOffset] is not specified, the last element in [extraSnapOffsets] - /// is used as the initial offset. If [extraSnapOffsets] is also not specified, - /// [initialOffset] is set to [shrunkOffset]. + /// If [initialInset] is not specified, the last element in [extraSnapInsets] + /// is used as the initial offset. If [extraSnapInsets] is also not specified, + /// [initialInset] is set to [shrunkInset]. /// /// If [overshootEffect] is enabled, the upper segment of the active page will /// slightly exceed the top of the viewport when it goes fullscreen. @@ -531,26 +528,26 @@ class ViewportConfiguration { bool overshootEffect = false, double minFraction = 0.9, double maxFraction = 1.0, - ViewportOffset shrunkOffset = ViewportOffset.shrunk, - ViewportOffset? initialOffset, - List extraSnapOffsets = const [], + ViewportInset shrunkInset = ViewportInset.shrunk, + ViewportInset? initialInset, + List extraSnapInsets = const [], }) { - final expandedOffset = - overshootEffect ? ViewportOffset.overshoot : ViewportOffset.expanded; - final snapOffsets = [ - expandedOffset, - shrunkOffset, - ...extraSnapOffsets, + final expandedInset = + overshootEffect ? ViewportInset.overshoot : ViewportInset.expanded; + final snapInsets = [ + expandedInset, + shrunkInset, + ...extraSnapInsets, ]; return ViewportConfiguration.raw( minFraction: minFraction, maxFraction: maxFraction, - minOffset: expandedOffset, - maxOffset: snapOffsets.last, - shrunkOffset: shrunkOffset, - expandedOffset: expandedOffset, - initialOffset: initialOffset ?? snapOffsets.last, - snapOffsets: snapOffsets, + minInset: expandedInset, + maxInset: snapInsets.last, + shrunkInset: shrunkInset, + expandedInset: expandedInset, + initialInset: initialInset ?? snapInsets.last, + snapInsets: snapInsets, ); } @@ -561,43 +558,43 @@ class ViewportConfiguration { final double maxFraction; /// {@macro exprollable_page_view.controller.ViewportMetrics.minOffset} - final ViewportOffset minOffset; + final ViewportInset minInset; /// {@macro exprollable_page_view.controller.ViewportMetrics.maxOffset} - final ViewportOffset maxOffset; + final ViewportInset maxInset; - /// {@macro exprollable_page_view.controller.PageViewportMetrics.shrunkOffset} - final ViewportOffset shrunkOffset; + /// {@macro exprollable_page_view.controller.ViewportMetrics.shrunkOffset} + final ViewportInset shrunkInset; - /// {@macro exprollable_page_view.controller.PageViewportMetrics.expandedOffset} - final ViewportOffset expandedOffset; + /// {@macro exprollable_page_view.controller.ViewportMetrics.expandedOffset} + final ViewportInset expandedInset; /// The initial viewport offset. - final ViewportOffset initialOffset; + final ViewportInset initialInset; /// The list of offsets that the viewport will snap to. /// /// The list must be sorted in ascending order by the actual offset value - /// calculated from [ViewportOffset.toConcreteValue]. - final List snapOffsets; + /// calculated from [ViewportInset.toConcreteValue]. + final List snapInsets; } /// An object that represents the state of the **conceptual** viewport. /// /// "Conceptual" means that the actual measurements for each page is calculated according to the state of this object, -/// and individually managed by [ViewportController]s attached to the pages. +/// and individually managed by [PageViewport]s attached to the pages. /// This is because the visual position of each page may differ, for example, -/// the default behavior of [PageViewport] is for the offset of the active page to be zero +/// the default behavior of [Viewport] is for the offset of the active page to be zero /// (or negative if overshoot effect is enabled) when it is fully expanded, /// but the offset for the inactive page is positive even if the active page is fully expanded. /// -/// This object subscribes to the given [ScrollAbsorber] to calculates the [offset] and [fraction] +/// This object subscribes to the given [ScrollAbsorber] to calculates the [inset] and [fraction] /// depending on [ScrollAbsorber.pixels], and if there are any changes, notifies its listeners. -class PageViewport extends ChangeNotifier - with ViewportMetrics, PageViewportMetrics - implements ValueListenable { +class Viewport extends ChangeNotifier + with ViewportMetrics, ViewportMetrics + implements ValueListenable { /// Creates an object that represents the state of the **conceptual** viewport. - PageViewport({ + Viewport({ required this.fractionBehavior, required this.configuration, required ScrollAbsorber absorber, @@ -614,7 +611,7 @@ class PageViewport extends ChangeNotifier final ScrollAbsorber _absorber; @override - PageViewportMetrics get value => this; + ViewportMetrics get value => this; ViewportDimensions? _dimensions; @@ -628,17 +625,17 @@ class PageViewport extends ChangeNotifier bool get hasDimensions => _dimensions != null; @override - double get maxOffset => configuration.maxOffset.toConcreteValue(this); + double get maxInset => configuration.maxInset.toConcreteValue(this); @override - double get minOffset => configuration.minOffset.toConcreteValue(this); + double get minInset => configuration.minInset.toConcreteValue(this); - double? _offset; + double? _inset; @override - double get offset { + double get inset { assert(hasDimensions); - return _offset!; + return _inset!; } @override @@ -656,16 +653,15 @@ class PageViewport extends ChangeNotifier } @override - double get expandedOffset => - configuration.expandedOffset.toConcreteValue(this); + double get expandedInset => configuration.expandedInset.toConcreteValue(this); @override - double get shrunkOffset => configuration.shrunkOffset.toConcreteValue(this); + double get shrunkInset => configuration.shrunkInset.toConcreteValue(this); double get _initialAbsorberPixels { - final initialOffset = configuration.initialOffset.toConcreteValue(this); - assert(initialOffset >= minOffset); - return initialOffset - minOffset; + final initialOffset = configuration.initialInset.toConcreteValue(this); + assert(initialOffset >= minInset); + return initialOffset - minInset; } /// Correct the state of this object for the given [dimensions]. @@ -676,26 +672,26 @@ class PageViewport extends ChangeNotifier _dimensions = dimensions; assert( - minOffset <= expandedOffset, + minInset <= expandedInset, "Invalid order of offset properties: " "minOffset <= expandedOffset must be satisfied, " - "but minOffset is $minOffset and expandedOffset is $expandedOffset.", + "but minOffset is $minInset and expandedOffset is $expandedInset.", ); assert( - expandedOffset <= shrunkOffset, + expandedInset <= shrunkInset, "Invalid order of offset properties: " "expandedOffset <= shrunkOffset must be satisfied, " - "but expandedOffset is $expandedOffset and shrunkOffset is $shrunkOffset.", + "but expandedOffset is $expandedInset and shrunkOffset is $shrunkInset.", ); assert( - shrunkOffset <= maxOffset, + shrunkInset <= maxInset, "Invalid order of offset properties: " "shrunkOffset <= maxOffset must be satisfied, " - "but shrunkOffset is $shrunkOffset and maxOffset is $maxOffset.", + "but shrunkOffset is $shrunkInset and maxOffset is $maxInset.", ); _absorber.correct((it) { - it.capacity = deltaOffset; + it.capacity = deltaInset; if (it.pixels == null) { it.absorb(_initialAbsorberPixels); } @@ -707,7 +703,7 @@ class PageViewport extends ChangeNotifier void _correctState() { assert(_absorber.pixels != null); assert(hasDimensions); - final newOffset = minOffset + _absorber.pixels!; + final newOffset = minInset + _absorber.pixels!; final dim = dimensions; final lowerBoundFraction = (dim.height - dim.padding.bottom - max(0.0, newOffset)) / dim.height; @@ -715,15 +711,15 @@ class PageViewport extends ChangeNotifier fractionBehavior.preferredFraction(this, newOffset); _fraction = max(lowerBoundFraction, preferredFraction); - _offset = newOffset; + _inset = newOffset; } void _invalidateState() { - final oldOffset = offset; + final oldOffset = inset; final oldFraction = fraction; _correctState(); if (!oldFraction.almostEqualTo(fraction) || - !oldOffset.almostEqualTo(offset)) { + !oldOffset.almostEqualTo(inset)) { notifyListeners(); } } @@ -732,10 +728,8 @@ class PageViewport extends ChangeNotifier /// Stores the actual metrics of the viewport for a specific [page]. /// Some of the metrics may be different from those of the conceptulal viewport /// depending on whether the page is active or not. -class ViewportController extends ChangeNotifier - with ViewportMetrics - implements ValueListenable { - ViewportController({ +class PageViewport extends ChangeNotifier { + PageViewport({ required this.page, required ExprollablePageController pageController, }) : _pageController = pageController { @@ -763,36 +757,18 @@ class ViewportController extends ChangeNotifier /// How many pixels the page should translate from the actual position in the page view. Offset get translation => _translation; - @override + /// The fraction of the viewport that the page should occupy. double get fraction => _fraction; - @override - double get minFraction => _pageController.viewport.minFraction; - - @override - double get maxFraction => _pageController.viewport.maxFraction; - - @override - ViewportDimensions get dimensions => _pageController.viewport.dimensions; - - @override - bool get hasDimensions => _pageController.viewport.hasDimensions; - - @override double get offset => _isPageActive - ? _pageController.viewport.offset - : max(0.0, _pageController.viewport.offset) + translation.dy; + ? _pageController.viewport.inset + : max(0.0, _pageController.viewport.inset) + translation.dy; - @override double get minOffset => _isPageActive - ? _pageController.viewport.minOffset - : dimensions.padding.top; + ? _pageController.viewport.minInset + : _pageController.viewport.dimensions.padding.top; - @override - double get maxOffset => _pageController.viewport.offset; - - @override - ViewportMetrics get value => this; + double get maxOffset => _pageController.viewport.inset; bool get _isPageActive => page == _pageController.currentPage.value; @@ -804,9 +780,9 @@ class ViewportController extends ChangeNotifier double _computeVerticalTranslation() { final vp = _pageController.viewport; if (_isPageActive) { - return min(vp.offset, 0.0); + return min(vp.inset, 0.0); } else { - return (vp.dimensions.padding.top - vp.offset) + return (vp.dimensions.padding.top - vp.inset) .clamp(0.0, vp.dimensions.padding.top); } } @@ -843,24 +819,24 @@ class ViewportController extends ChangeNotifier } } - /// Obtains the [ViewportController] of a page that is the nearest ancestor from [context]. - static ViewportController? of(BuildContext context) => context - .dependOnInheritedWidgetOfExactType() - ?.controller; + /// Obtains the [PageViewport] of a page that is the nearest ancestor from [context]. + static PageViewport? of(BuildContext context) => context + .dependOnInheritedWidgetOfExactType() + ?.pageView; } /// A [ScrollController] that must be attached to a [Scrollable] widget in each page. /// -/// Since [PageViewport] subscribes to [PageContentScrollController.absorber] +/// Since [Viewport] subscribes to [PageContentScrollController.absorber] /// to calculate the viewport state according to the scroll position, /// it is important that the [PageContentScrollController] obtained from /// [PageContentScrollController.of] is attached to a [Scrollable] widget in each page. class PageContentScrollController extends AbsorbScrollController { PageContentScrollController._( - {required _SnapViewportOffsetPhysics? snapPhysics}) + {required _SnapViewportInsetPhysics? snapPhysics}) : _snapPhysics = snapPhysics; - final _SnapViewportOffsetPhysics? _snapPhysics; + final _SnapViewportInsetPhysics? _snapPhysics; @override double get initialScrollOffset { @@ -888,21 +864,21 @@ class PageContentScrollController extends AbsorbScrollController { ?.controller; } -class _SnapViewportOffsetPhysics extends ScrollPhysics { +class _SnapViewportInsetPhysics extends ScrollPhysics { // ignore: prefer_const_constructors_in_immutables - _SnapViewportOffsetPhysics({ + _SnapViewportInsetPhysics({ super.parent, - required this.snapOffsets, + required this.snapInsets, required this.viewport, }); - final List snapOffsets; - final PageViewport viewport; + final List snapInsets; + final Viewport viewport; @override - ScrollPhysics applyTo(ScrollPhysics? ancestor) => _SnapViewportOffsetPhysics( + ScrollPhysics applyTo(ScrollPhysics? ancestor) => _SnapViewportInsetPhysics( parent: buildParent(ancestor), - snapOffsets: snapOffsets, + snapInsets: snapInsets, viewport: viewport, ); @@ -910,16 +886,16 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { // TODO; Find more appropriate threshold const thresholdVelocity = 2000; if (velocity.abs() > thresholdVelocity) return null; - if (snapOffsets.isEmpty) return null; + if (snapInsets.isEmpty) return null; assert((() { final snaps = - snapOffsets.map((s) => s.toConcreteValue(viewport)).toList(); + snapInsets.map((s) => s.toConcreteValue(viewport)).toList(); return listEquals(snaps, [...snaps]..sort()); })(), "'snapOffsets' must be sorted in ascending order."); final snapScrollOffsets = - snapOffsets.map((s) => s.toScrollOffset(viewport)).toList(); + snapInsets.map((s) => s.toScrollOffset(viewport)).toList(); final minSnap = snapScrollOffsets.last; final maxSnap = snapScrollOffsets.first; if (position.pixels < minSnap || position.pixels > maxSnap) { @@ -953,56 +929,56 @@ class _SnapViewportOffsetPhysics extends ScrollPhysics { } /// An object that represents a viewport offset. -/// -/// There are 3 predefined [ViewportOffset]s: -/// - [ViewportOffset.expanded] : The default offset at which the page viewport is fully expanded. -/// - [ViewportOffset.shrunk] : The default offset at which the page viewport is fully shrunk. -/// - [ViewportOffset.overshoot] : The default offset at which the page viewport is fully expanded and overshot. -/// +/// +/// There are 3 predefined [ViewportInset]s: +/// - [ViewportInset.expanded] : The default offset at which the page viewport is fully expanded. +/// - [ViewportInset.shrunk] : The default offset at which the page viewport is fully shrunk. +/// - [ViewportInset.overshoot] : The default offset at which the page viewport is fully expanded and overshot. +/// /// User defined offsets can be created using [ViewportOffset.fixed] and [ViewportOffset.fractional], -/// or extend [ViewportOffset] to perform more complex calculations. -abstract class ViewportOffset { +/// or extend [ViewportInset] to perform more complex calculations. +abstract class ViewportInset { /// {@macro exprollable_page_view.controller.DefaultExpandedViewportOffset} - static const expanded = DefaultExpandedViewportOffset(); + static const expanded = DefaultExpandedViewportInset(); /// {@macro exprollable_page_view.controller.DefaultShrunkViewportOffset} - static const shrunk = DefaultShrunkViewportOffset(); + static const shrunk = DefaultShrunkViewportInset(); /// {@macro exprollable_page_view.controller.OvershootViewportOffset} - static const overshoot = OvershootViewportOffset(); + static const overshoot = OvershootViewportInset(); /// {@macro exprollable_page_view.controller.FractionalViewportOffset.new} - const factory ViewportOffset.fractional(double fraction) = - FractionalViewportOffset; + const factory ViewportInset.fractional(double fraction) = + FractionalViewportInset; /// {@macro exprollable_page_view.controller.FixedViewportOffset.new} - const factory ViewportOffset.fixed(double pixels) = FixedViewportOffset; + const factory ViewportInset.fixed(double pixels) = FixedViewportInset; - /// Contructs a [ViewportOffset]. - const ViewportOffset(); + /// Contructs a [ViewportInset]. + const ViewportInset(); /// Calculate the concrete pixels represented by this object /// from the current viewport dimensions. - double toConcreteValue(PageViewportMetrics metrics); + double toConcreteValue(ViewportMetrics metrics); /// Convert the offset to a scroll offset for [ScrollPosition]. @nonVirtual - double toScrollOffset(PageViewportMetrics metrics) { + double toScrollOffset(ViewportMetrics metrics) { final offset = toConcreteValue(metrics); - assert(offset >= metrics.minOffset); - return -1 * (offset - metrics.minOffset); + assert(offset >= metrics.minInset); + return -1 * (offset - metrics.minInset); } } /// {@template exprollable_page_view.controller.OvershootViewportOffset} /// The default offset at which the viewport will be fully expanded and overshot. /// {@endtemplate} -class OvershootViewportOffset extends ViewportOffset { +class OvershootViewportInset extends ViewportInset { /// Create the overshot viewport offset. - const OvershootViewportOffset(); + const OvershootViewportInset(); @override - double toConcreteValue(PageViewportMetrics metrics) => + double toConcreteValue(ViewportMetrics metrics) => -1 * metrics.dimensions.padding.bottom; } @@ -1011,12 +987,12 @@ class OvershootViewportOffset extends ViewportOffset { /// /// The offset value is always 0.0. /// {@endtemplate} -class DefaultExpandedViewportOffset extends ViewportOffset { +class DefaultExpandedViewportInset extends ViewportInset { /// Create the default expanded viewport offset. - const DefaultExpandedViewportOffset(); + const DefaultExpandedViewportInset(); @override - double toConcreteValue(PageViewportMetrics metrics) => 0.0; + double toConcreteValue(ViewportMetrics metrics) => 0.0; } /// {@template exprollable_page_view.controller.DefaultShrunkViewportOffset} @@ -1028,12 +1004,12 @@ class DefaultExpandedViewportOffset extends ViewportOffset { /// from the height of the viewport. This clamping process is necessary to prevent /// unwanted white space between the bottom of the page and the viewport. /// {@endtemplate} -class DefaultShrunkViewportOffset extends ViewportOffset { +class DefaultShrunkViewportInset extends ViewportInset { /// Create the default shrunk viewport offset. - const DefaultShrunkViewportOffset(); + const DefaultShrunkViewportInset(); @override - double toConcreteValue(PageViewportMetrics metrics) { + double toConcreteValue(ViewportMetrics metrics) { assert(metrics.hasDimensions); const margin = 16.0; final preferredOffset = metrics.dimensions.padding.top + margin; @@ -1044,35 +1020,35 @@ class DefaultShrunkViewportOffset extends ViewportOffset { } /// A viewport offset that is defined by a fractional value. -class FractionalViewportOffset extends ViewportOffset { +class FractionalViewportInset extends ViewportInset { /// {@template exprollable_page_view.controller.FractionalViewportOffset.new} /// Creates a viewport offset from a fractional value. /// /// [fraction] is a relative value of the viewport height substracted /// by the bottom padding and must be between 0.0 and 1.0. /// {@endtemplate} - const FractionalViewportOffset(this.fraction) + const FractionalViewportInset(this.fraction) : assert(0.0 <= fraction && fraction <= 1.0); /// The fractional value of the offset. final double fraction; @override - double toConcreteValue(PageViewportMetrics metrics) => + double toConcreteValue(ViewportMetrics metrics) => fraction * (metrics.dimensions.height - metrics.dimensions.padding.bottom); } /// A viewport offset that is defined by a fixed value. -class FixedViewportOffset extends ViewportOffset { +class FixedViewportInset extends ViewportInset { /// {@template exprollable_page_view.controller.FixedViewportOffset.new} /// Creates a viewport offset from a fixed value. /// {@endtemplate} - const FixedViewportOffset(this.pixels); + const FixedViewportInset(this.pixels); /// The fixed value of the offset in terms of logical pixels. final double pixels; @override - double toConcreteValue(PageViewportMetrics metrics) => pixels; + double toConcreteValue(ViewportMetrics metrics) => pixels; } diff --git a/package/lib/src/core/view.dart b/package/lib/src/core/view.dart index d04e424..0a9dc2b 100644 --- a/package/lib/src/core/view.dart +++ b/package/lib/src/core/view.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:exprollable_page_view/src/core/controller.dart'; import 'package:exprollable_page_view/src/internal/paging.dart'; -import 'package:exprollable_page_view/src/internal/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; @@ -74,7 +73,7 @@ class ExprollablePageView extends StatefulWidget { /// Called whnever the viewport fraction or offset changes. Providing this callback /// is equivalent to subscribing to [ExprollablePageController.viewport]. - final void Function(PageViewportMetrics metrics)? onViewportChanged; + final void Function(ViewportMetrics metrics)? onViewportChanged; /// Called whenever the focused page changes. Providing this callback /// is equivalent to subscribing to [ExprollablePageController.currentPage]. @@ -128,8 +127,8 @@ class _ExprollablePageViewState extends State { void onViewportChanged() { allowPaging.value = checkIfPagingIsAllowed(); widget.onViewportChanged?.call(controller.viewport); - PageViewportUpdateNotification( - StaticPageViewportMetrics.from(controller.viewport), + ViewportUpdateNotification( + StaticViewportMetrics.from(controller.viewport), ).dispatch(context); } @@ -137,8 +136,7 @@ class _ExprollablePageViewState extends State { widget.onPageChanged?.call(controller.currentPage.value); } - bool checkIfPagingIsAllowed() => controller.viewport.fraction - .almostEqualTo(controller.viewport.minFraction); + bool checkIfPagingIsAllowed() => controller.viewport.isPageShrunk; @override Widget build(BuildContext context) { @@ -162,7 +160,7 @@ class _ExprollablePageViewState extends State { valueListenable: controller.viewport, builder: (context, viewport, child) { return Transform.translate( - offset: Offset(0, max(0.0, viewport.offset)), + offset: Offset(0, max(0.0, viewport.inset)), child: child, ); }, @@ -217,13 +215,13 @@ class _PageItemContainer extends StatefulWidget { class _PageItemContainerState extends State<_PageItemContainer> { late PageContentScrollController scrollController; - late ViewportController viewport; + late PageViewport viewport; late ValueNotifier isActive; @override void initState() { super.initState(); - viewport = ViewportController( + viewport = PageViewport( page: widget.page, pageController: widget.controller, ); @@ -260,7 +258,7 @@ class _PageItemContainerState extends State<_PageItemContainer> { if (oldWidget.controller != widget.controller || oldWidget.page != widget.page) { viewport.dispose(); - viewport = ViewportController( + viewport = PageViewport( page: widget.page, pageController: widget.controller, ); @@ -273,8 +271,8 @@ class _PageItemContainerState extends State<_PageItemContainer> { @override Widget build(BuildContext context) { - return InheritedViewportController( - controller: viewport, + return InheritedPageViewport( + pageView: viewport, child: InheritedPageContentScrollController( controller: scrollController, child: _PageItem( @@ -295,7 +293,7 @@ class _PageItem extends StatelessWidget { }); final ValueListenable isActive; - final ViewportController viewport; + final PageViewport viewport; final WidgetBuilder builder; @override From d450d0940d9f59203703aa1ae0a98096030ec0da Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 16 May 2023 20:33:40 +0900 Subject: [PATCH 13/24] fix doc comments --- package/lib/src/addon/adaptive_padding.dart | 10 +- package/lib/src/addon/gutter.dart | 2 +- package/lib/src/addon/modal.dart | 4 +- package/lib/src/core/controller.dart | 251 ++++++++++---------- package/lib/src/core/view.dart | 28 ++- 5 files changed, 159 insertions(+), 136 deletions(-) diff --git a/package/lib/src/addon/adaptive_padding.dart b/package/lib/src/addon/adaptive_padding.dart index 0d193e4..2f74c7d 100644 --- a/package/lib/src/addon/adaptive_padding.dart +++ b/package/lib/src/addon/adaptive_padding.dart @@ -5,12 +5,12 @@ import 'package:exprollable_page_view/src/core/view.dart'; import 'package:exprollable_page_view/src/internal/utils.dart'; import 'package:flutter/widgets.dart' hide Viewport; -/// Inserts appropriate padding into the child widget according to the current viewpor offset. +/// Inserts appropriate padding into the child widget according to the current viewpor inset. class AdaptivePagePadding extends StatefulWidget { - /// Creates a widget that inserts appropriate padding - /// into the top of the child widget according to the current viewpor offset. - /// It also adds extra padding if [useSafeArea] is enabled - /// to prevents the child from being obscured by the system UI such as a status bar. + /// Creates a widget that inserts appropriate padding into + /// the top of the child widget according to the current viewport inset. + /// It also adds extra padding if [useSafeArea] is enabled to prevents + /// the child from being obscured by the system UI such as the status bar. const AdaptivePagePadding({ super.key, required this.child, diff --git a/package/lib/src/addon/gutter.dart b/package/lib/src/addon/gutter.dart index 7974386..a5760d8 100644 --- a/package/lib/src/addon/gutter.dart +++ b/package/lib/src/addon/gutter.dart @@ -3,7 +3,7 @@ import 'package:exprollable_page_view/src/core/view.dart'; import 'package:exprollable_page_view/src/internal/utils.dart'; import 'package:flutter/widgets.dart'; -/// Insert spaces at both sides of the wrapped page. +/// Inserts spaces at both sides of the wrapped page. class PageGutter extends StatefulWidget { /// Creates a widget that inserts spaces of [gutterWidth] at both sides of [child]. const PageGutter({ diff --git a/package/lib/src/addon/modal.dart b/package/lib/src/addon/modal.dart index d5efa7e..e948075 100644 --- a/package/lib/src/addon/modal.dart +++ b/package/lib/src/addon/modal.dart @@ -4,7 +4,7 @@ import 'package:exprollable_page_view/src/core/controller.dart'; import 'package:exprollable_page_view/src/core/view.dart'; import 'package:flutter/material.dart' hide Viewport; -/// Show an [ExprollablePageView] as a modal dialog. +/// Shows an [ExprollablePageView] as a modal dialog. Future showModalExprollable( BuildContext context, { required WidgetBuilder builder, @@ -57,7 +57,7 @@ class DismissThresholdInset extends ViewportInset { /// A widget that makes a modal dialog style [ExprollablePageView]. /// /// This widget adds a translucent background (barrier) and -/// *swipe down to dismiss* action to the child page view. +/// *swipe down to dismiss* action to the decendant page view. /// Use [showModalExprollable] as a convenience method /// to show the [ExprollablePageView] as a dialog, /// which wraps the page view with [ModalExprollable]. diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index df5b1c3..d382887 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -10,6 +10,7 @@ import 'package:meta/meta.dart'; /// An inherited widget used in [ExprollablePageView] to provides /// its [ExprollablePageController] to its descendants. +/// /// [ExprollablePageController.of] is a convenience method that obtains /// the controller sotred in this inherited widget. @internal @@ -20,7 +21,7 @@ class InheritedExprollablePageController extends InheritedWidget { required this.controller, }); - /// A controller that attached to the ancestor [ExprollablePageView]. + /// A controller to be provided to the descendants. final ExprollablePageController controller; @override @@ -54,6 +55,7 @@ class InheritedPageContentScrollController extends InheritedWidget { required this.controller, }); + /// A scroll controller to be provided to the descendants. final PageContentScrollController controller; @override @@ -85,7 +87,7 @@ class _CurrentPageNotifier extends ValueNotifier { /// A controller for [ExprollablePageView]. /// -/// This lets you manipulate which page is visible in a [ExprollablePageView]. +/// This controller lets you manipulate which page is visible in a [ExprollablePageView]. /// It also can be used to programmatically change the viewport state. class ExprollablePageController extends PageController { /// Create a page controller. @@ -93,7 +95,7 @@ class ExprollablePageController extends PageController { /// Specifying [viewportFractionBehavior] allows you to control how the viewport fraction changes /// along with vertical scrolling. [DefaultViewportFractionBehavior] is used by default. /// - /// To configure the metrics of the viewport, specify [viewportConfiguration] with the desired values. + /// To configure the properties of the viewport, specify [viewportConfiguration] with the desired values. /// [ViewportConfiguration.defaultConfiguration] is used as the default configuration. ExprollablePageController({ super.initialPage, @@ -120,13 +122,15 @@ class ExprollablePageController extends PageController { late final _SnapViewportInsetPhysics _snapPhysics; - /// A notifier that stores the index of the current visible page. + /// A notifier that stores the index of the current active page. + /// /// The new index is notified whenever the page that fully occupies the viewport changes. - /// [ExprollablePageController.initialPage] is used as an initial value. + /// The initial value is [ExprollablePageController.initialPage]. ValueListenable get currentPage => _currentPage; late final _CurrentPageNotifier _currentPage; /// An object that stores the viewport state. + /// /// You can subscribe this object to get notified when the viewport state changes. late final Viewport viewport; @@ -166,8 +170,8 @@ class ExprollablePageController extends PageController { _currentPage.dispose(); } - /// Animates the controlled [ExprollablePageView] from the current viewport offset - /// to the given offset. + /// Animates the viewport inset of the controlled [ExprollablePageView] + /// from the current inset to the given inset. Future animateViewportInsetTo( ViewportInset inset, { required Curve curve, @@ -180,7 +184,8 @@ class ExprollablePageController extends PageController { ); } - /// instantly changes the current viewport offset without animation. + /// instantly changes the current viewport inset of + /// the controlled [ExprollablePageView] without animation. void jumpViewportInsetTo(ViewportInset inset) { _contentScrollController.jumpTo(inset.toScrollOffset(viewport)); } @@ -212,7 +217,7 @@ class ViewportDimensions { /// (e.g., software keyboard padding shown on the screen). final EdgeInsets padding; - /// A description of the viewport mesurements. + /// Construct a description of the viewport mesurements. const ViewportDimensions({ required this.width, required this.height, @@ -234,15 +239,14 @@ class ViewportDimensions { /// A description of the viewport state. /// -/// The state of the viewport is described by the 2 mesurements: fraction and offset. -/// A fraction indicates how much space each page should occupy in the viewport, -/// and it must be between 0.0 and 1.0. An offset is the distance from the top of the viewport -/// to the top of a page. -/// -/// ![viewport-fraction-offset](https://user-images.githubusercontent.com/68946713/231830114-f4d9bec4-cb85-41f8-a9fd-7b3f21ff336a.png) -/// +/// {@template exprollable_page_view.controller.ViewportMetrics} +/// The state of the viewport is described by the 2 mesurements: fraction and inset. +/// The fraction indicates how much space each page should occupy in the viewport, +/// and hhe inset is the distance from the top of the viewport to the top of the current page viewport. +/// {@endtemplate} mixin ViewportMetrics { - /// A static description of the viewport mesurements. + /// The mesurements of the viewport. + /// /// Available only if [hasDimensions] is true. ViewportDimensions get dimensions; @@ -250,7 +254,8 @@ mixin ViewportMetrics { bool get hasDimensions; /// Indicates how much space each page should occupy in the viewport. - /// [fraction] is between [minFraction] and [maxFraction] including both edges. + /// + /// [fraction] must be between [minFraction] and [maxFraction] including both edges. double get fraction; /// {@template exprollable_page_view.controller.ViewportMetrics.minFraction} @@ -263,56 +268,63 @@ mixin ViewportMetrics { /// {@endtemplate} double get maxFraction; - /// The distance from the top of the viewport to the top of the current page. + /// The distance from the top of the viewport to the top of the current page vieewport. /// /// [inset] is always greater than or equals to [minInset], but might exceeds [maxInset]. - /// For eample, if the scrollable widget in the current page uses [BouncingScrollPhysics] + /// For example, if a scrollable widget in the current page uses [BouncingScrollPhysics] /// as its scroll physics and a user tries to overscroll the page, /// [inset] will exceeds [maxInset] according to the physics. double get inset; - /// {@template exprollable_page_view.controller.ViewportMetrics.minOffset} - /// The lower bound of the offset. + /// {@template exprollable_page_view.controller.ViewportMetrics.minInset} + /// The lower bound of the inset. /// {@endtemplate} double get minInset; - /// {@template exprollable_page_view.controller.ViewportMetrics.maxOffset} - /// The upper bound of the offset. The actual [inset] might exceeds this value. + /// {@template exprollable_page_view.controller.ViewportMetrics.maxInset} + /// The upper bound of the inset. + /// + /// In certain cases, the [inset] might exceeds this value, + /// but it will eventually settle to this value. /// {@endtemplate} double get maxInset; /// Calculate the difference between [minInset] and [maxInset]. + /// + /// Always returns zero or a positive value. double get deltaInset { assert(minInset <= maxInset); return maxInset - minInset; } /// Calculate the difference between [minFraction] and [maxInset]. + /// + /// Always returns zero or a positive value. double get deltaFraction { assert(minFraction <= maxFraction); return maxFraction - minFraction; } - /// {@template exprollable_page_view.controller.ViewportMetrics.shrunkOffset} - /// The lower bound of the offset at which the viewport is fully shrunk. + /// {@template exprollable_page_view.controller.ViewportMetrics.shrunkInset} + /// The lower bound of the [inset] at which the current page viewport is fully shrunk. /// {@endtemplate} double get shrunkInset; - /// {@template exprollable_page_view.controller.ViewportMetrics.expandedOffset} - /// The upper bound of the offset at which the viewport is fully expanded. + /// {@template exprollable_page_view.controller.ViewportMetrics.expandedInset} + /// The upper bound of the [inset] at which the current page viewport is fully expanded. /// {@endtemplate} double get expandedInset; - /// Indicates if the viewport is fully shrunk. + /// Indicates if the current page viewport is fully shrunk. bool get isPageShrunk => fraction.almostEqualTo(minFraction) || fraction < minFraction; - // Indicates if the viewport is fully expanded. + /// Indicates if the current page viewport is fully expanded. bool get isPageExpanded => fraction.almostEqualTo(maxFraction) || fraction > maxFraction; } -/// A snapshot of the state of the conceptual viewport. +/// A snapshot of the viewport state. @immutable class StaticViewportMetrics with ViewportMetrics { /// Create a snapshot of the viewport state. @@ -405,20 +417,21 @@ class StaticViewportMetrics with ViewportMetrics { } /// A notification that bubbles up the widget tree from a [ExprollablePageView] whenever the viewport state changes. +/// /// Listening for this notification is equivalent to observe [ExprollablePageController.viewport]. class ViewportUpdateNotification extends Notification { const ViewportUpdateNotification(this.metrics); final ViewportMetrics metrics; } -/// Describes how the viewport fraction changes when the page is scrolled vertically . +/// Describes how the viewport fraction changes when the current page is scrolled vertically. /// /// Use the convenient [DefaultViewportFractionBehavior] which implements the default behavior, -/// or extend this class and override [preferredFraction] to create a custom behavior. +/// or extend this class and override [preferredFraction] to create your own behavior. abstract class ViewportFractionBehavior { - /// Calculate the viewport fraction according to the state of the current viewport and the new offset. + /// Calculate a viewport fraction according to the current viewport state and the new inset. /// - /// This method is called by [Viewport] whenever the fraction should be updated. + /// This method is called by [Viewport] whenever the [Viewport.fraction] should be updated. /// The calculated fraction must be [ViewportMetrics.minFraction] /// when [ViewportMetrics.inset] is greater than or equal to [ViewportMetrics.shrunkInset], /// and must be [ViewportMetrics.maxFraction] when [ViewportMetrics.inset] is less than or equal to [ViewportMetrics.expandedInset]. @@ -480,20 +493,20 @@ class ViewportConfiguration { /// Create a configuration for standard use cases. /// - /// If [extraSnapInsets] is not empty, viewport will snap to the offsets + /// If [extraSnapInsets] is not empty, viewport will snap to the insets /// given by [extraSnapInsets] in addition to [ViewportInset.expanded] and [ViewportInset.shrunk]. - /// The list must be sorted in ascending order by the actual offset value + /// The list must be sorted in ascending order by the actual inset value /// calculated from [ViewportInset.toConcreteValue]. /// /// If [initialInset] is not specified, the last element in [extraSnapInsets] - /// is used as the initial offset. If [extraSnapInsets] is also not specified, + /// is used as the initial inset. If [extraSnapInsets] is also not specified, /// [initialInset] is set to [shrunkInset]. /// - /// If [overshootEffect] is enabled, the upper segment of the active page will + /// If [overshootEffect] is enabled, the upper segment of the current page viewport will /// slightly exceed the top of the viewport when it goes fullscreen. - /// To be precise, this means that the viewport offset will take a negative value - /// when the viewport fraction is 1.0. This trick creates a dynamic visual effect - /// when the page goes fullscreen. The figures below are a demonstration of + /// To be precise, this means that [inset] will take a negative value + /// when the viewport fraction is [maxFraction]. This trick creates a dynamic visual effect + /// when the page goes fullscreen. The figures below are demonstrations of /// how the overshoot effect affects (disabled in the left, enabled in the right). /// /// ![overshoot-disabled](https://user-images.githubusercontent.com/68946713/231827343-155a750d-b21f-4a96-b81a-74c8873c46cb.gif) ![overshoot-enabled](https://user-images.githubusercontent.com/68946713/231827364-40843efc-5a91-49ff-ab74-c9af1e4b0c62.gif) @@ -557,36 +570,31 @@ class ViewportConfiguration { /// {@macro exprollable_page_view.controller.ViewportMetrics.maxFraction} final double maxFraction; - /// {@macro exprollable_page_view.controller.ViewportMetrics.minOffset} + /// {@macro exprollable_page_view.controller.ViewportMetrics.minInset} final ViewportInset minInset; - /// {@macro exprollable_page_view.controller.ViewportMetrics.maxOffset} + /// {@macro exprollable_page_view.controller.ViewportMetrics.maxInset} final ViewportInset maxInset; - /// {@macro exprollable_page_view.controller.ViewportMetrics.shrunkOffset} + /// {@macro exprollable_page_view.controller.ViewportMetrics.shrunkInset} final ViewportInset shrunkInset; - /// {@macro exprollable_page_view.controller.ViewportMetrics.expandedOffset} + /// {@macro exprollable_page_view.controller.ViewportMetrics.expandedInset} final ViewportInset expandedInset; - /// The initial viewport offset. + /// The initial viewport inset. final ViewportInset initialInset; - /// The list of offsets that the viewport will snap to. + /// The list of insets that the viewport will snap to. /// - /// The list must be sorted in ascending order by the actual offset value + /// The list must be sorted in ascending order by the actual inset value /// calculated from [ViewportInset.toConcreteValue]. final List snapInsets; } -/// An object that represents the state of the **conceptual** viewport. +/// An object that represents the state of the viewport. /// -/// "Conceptual" means that the actual measurements for each page is calculated according to the state of this object, -/// and individually managed by [PageViewport]s attached to the pages. -/// This is because the visual position of each page may differ, for example, -/// the default behavior of [Viewport] is for the offset of the active page to be zero -/// (or negative if overshoot effect is enabled) when it is fully expanded, -/// but the offset for the inactive page is positive even if the active page is fully expanded. +/// {@macro exprollable_page_view.controller.ViewportMetrics} /// /// This object subscribes to the given [ScrollAbsorber] to calculates the [inset] and [fraction] /// depending on [ScrollAbsorber.pixels], and if there are any changes, notifies its listeners. @@ -659,12 +667,13 @@ class Viewport extends ChangeNotifier double get shrunkInset => configuration.shrunkInset.toConcreteValue(this); double get _initialAbsorberPixels { - final initialOffset = configuration.initialInset.toConcreteValue(this); - assert(initialOffset >= minInset); - return initialOffset - minInset; + final initialInset = configuration.initialInset.toConcreteValue(this); + assert(initialInset >= minInset); + return initialInset - minInset; } /// Correct the state of this object for the given [dimensions]. + /// /// This method should be called whenever the dimensions of the viewport changes in [ExprollablePageView.build]. /// Therefore this method does not notify its listeners even if the state changes after recalculation. @internal @@ -673,21 +682,21 @@ class Viewport extends ChangeNotifier assert( minInset <= expandedInset, - "Invalid order of offset properties: " - "minOffset <= expandedOffset must be satisfied, " - "but minOffset is $minInset and expandedOffset is $expandedInset.", + "Invalid order of inset properties: " + "minInset <= expandedInset must be satisfied, " + "but minInset is $minInset and expandedInset is $expandedInset.", ); assert( expandedInset <= shrunkInset, - "Invalid order of offset properties: " - "expandedOffset <= shrunkOffset must be satisfied, " - "but expandedOffset is $expandedInset and shrunkOffset is $shrunkInset.", + "Invalid order of inset properties: " + "expandedInset <= shrunkInset must be satisfied, " + "but expandedInset is $expandedInset and shrunkInset is $shrunkInset.", ); assert( shrunkInset <= maxInset, - "Invalid order of offset properties: " - "shrunkOffset <= maxOffset must be satisfied, " - "but shrunkOffset is $shrunkInset and maxOffset is $maxInset.", + "Invalid order of inset properties: " + "shrunkInset <= maxInset must be satisfied, " + "but shrunkInset is $shrunkInset and maxInset is $maxInset.", ); _absorber.correct((it) { @@ -703,31 +712,29 @@ class Viewport extends ChangeNotifier void _correctState() { assert(_absorber.pixels != null); assert(hasDimensions); - final newOffset = minInset + _absorber.pixels!; + final newInset = minInset + _absorber.pixels!; final dim = dimensions; final lowerBoundFraction = - (dim.height - dim.padding.bottom - max(0.0, newOffset)) / dim.height; + (dim.height - dim.padding.bottom - max(0.0, newInset)) / dim.height; final preferredFraction = - fractionBehavior.preferredFraction(this, newOffset); + fractionBehavior.preferredFraction(this, newInset); _fraction = max(lowerBoundFraction, preferredFraction); - _inset = newOffset; + _inset = newInset; } void _invalidateState() { - final oldOffset = inset; + final oldInset = inset; final oldFraction = fraction; _correctState(); if (!oldFraction.almostEqualTo(fraction) || - !oldOffset.almostEqualTo(inset)) { + !oldInset.almostEqualTo(inset)) { notifyListeners(); } } } -/// Stores the actual metrics of the viewport for a specific [page]. -/// Some of the metrics may be different from those of the conceptulal viewport -/// depending on whether the page is active or not. +/// An object that sotres the page viewport state for a specific page. class PageViewport extends ChangeNotifier { PageViewport({ required this.page, @@ -746,7 +753,7 @@ class PageViewport extends ChangeNotifier { ..viewport.removeListener(_invalidateState); } - /// The page corresponding to the viewport that this object represents. + /// The page corresponding to this page viewport. final int page; final ExprollablePageController _pageController; @@ -760,14 +767,19 @@ class PageViewport extends ChangeNotifier { /// The fraction of the viewport that the page should occupy. double get fraction => _fraction; + /// The distance from the top of the viewport to the top of this page viewport. + /// + /// This value will be equal to [Viewport.inset] if [page] is the current page. double get offset => _isPageActive ? _pageController.viewport.inset : max(0.0, _pageController.viewport.inset) + translation.dy; + /// The lower bound of [offset]. double get minOffset => _isPageActive ? _pageController.viewport.minInset : _pageController.viewport.dimensions.padding.top; + /// The upper bound of [offset]. double get maxOffset => _pageController.viewport.inset; bool get _isPageActive => page == _pageController.currentPage.value; @@ -819,7 +831,7 @@ class PageViewport extends ChangeNotifier { } } - /// Obtains the [PageViewport] of a page that is the nearest ancestor from [context]. + /// Obtains the [PageViewport] attached to the page that is the nearest ancestor from [context]. static PageViewport? of(BuildContext context) => context .dependOnInheritedWidgetOfExactType() ?.pageView; @@ -857,7 +869,7 @@ class PageContentScrollController extends AbsorbScrollController { oldPosition, ); - /// Obtains the [PageContentScrollController] for a page that is the nearest ancestor from [context]. + /// Obtains the [PageContentScrollController] for the page that is the nearest ancestor from [context]. static PageContentScrollController? of(BuildContext context) => context .dependOnInheritedWidgetOfExactType< InheritedPageContentScrollController>() @@ -889,15 +901,14 @@ class _SnapViewportInsetPhysics extends ScrollPhysics { if (snapInsets.isEmpty) return null; assert((() { - final snaps = - snapInsets.map((s) => s.toConcreteValue(viewport)).toList(); + final snaps = snapInsets.map((s) => s.toConcreteValue(viewport)).toList(); return listEquals(snaps, [...snaps]..sort()); - })(), "'snapOffsets' must be sorted in ascending order."); + })(), "'snapInsets' must be sorted in ascending order."); - final snapScrollOffsets = + final snapScrollInsets = snapInsets.map((s) => s.toScrollOffset(viewport)).toList(); - final minSnap = snapScrollOffsets.last; - final maxSnap = snapScrollOffsets.first; + final minSnap = snapScrollInsets.last; + final maxSnap = snapScrollInsets.first; if (position.pixels < minSnap || position.pixels > maxSnap) { return null; } @@ -906,7 +917,7 @@ class _SnapViewportInsetPhysics extends ScrollPhysics { double nearest(double p, double q) => (pixels - p).abs() < (pixels - q).abs() ? p : q; - return snapScrollOffsets.reduce(nearest); + return snapScrollInsets.reduce(nearest); } @override @@ -928,30 +939,30 @@ class _SnapViewportInsetPhysics extends ScrollPhysics { } } -/// An object that represents a viewport offset. +/// An object that represents a viewport inset. /// /// There are 3 predefined [ViewportInset]s: -/// - [ViewportInset.expanded] : The default offset at which the page viewport is fully expanded. -/// - [ViewportInset.shrunk] : The default offset at which the page viewport is fully shrunk. -/// - [ViewportInset.overshoot] : The default offset at which the page viewport is fully expanded and overshot. +/// - [ViewportInset.expanded] : The default inset at which the current page is fully expanded. +/// - [ViewportInset.shrunk] : The default inset at which the current page is fully shrunk. +/// - [ViewportInset.overshoot] : The default inset at which the current page is fully expanded and overshot. /// -/// User defined offsets can be created using [ViewportOffset.fixed] and [ViewportOffset.fractional], +/// User defined insets can be created using [ViewportInset.fixed] and [ViewportInset.fractional], /// or extend [ViewportInset] to perform more complex calculations. abstract class ViewportInset { - /// {@macro exprollable_page_view.controller.DefaultExpandedViewportOffset} + /// {@macro exprollable_page_view.controller.DefaultExpandedViewportInset} static const expanded = DefaultExpandedViewportInset(); - /// {@macro exprollable_page_view.controller.DefaultShrunkViewportOffset} + /// {@macro exprollable_page_view.controller.DefaultShrunkViewportInset} static const shrunk = DefaultShrunkViewportInset(); - /// {@macro exprollable_page_view.controller.OvershootViewportOffset} + /// {@macro exprollable_page_view.controller.OvershootViewportInset} static const overshoot = OvershootViewportInset(); - /// {@macro exprollable_page_view.controller.FractionalViewportOffset.new} + /// {@macro exprollable_page_view.controller.FractionalViewportInset.new} const factory ViewportInset.fractional(double fraction) = FractionalViewportInset; - /// {@macro exprollable_page_view.controller.FixedViewportOffset.new} + /// {@macro exprollable_page_view.controller.FixedViewportInset.new} const factory ViewportInset.fixed(double pixels) = FixedViewportInset; /// Contructs a [ViewportInset]. @@ -961,7 +972,7 @@ abstract class ViewportInset { /// from the current viewport dimensions. double toConcreteValue(ViewportMetrics metrics); - /// Convert the offset to a scroll offset for [ScrollPosition]. + /// Convert the inset to a scroll offset for [ScrollPosition]. @nonVirtual double toScrollOffset(ViewportMetrics metrics) { final offset = toConcreteValue(metrics); @@ -970,11 +981,11 @@ abstract class ViewportInset { } } -/// {@template exprollable_page_view.controller.OvershootViewportOffset} -/// The default offset at which the viewport will be fully expanded and overshot. +/// {@template exprollable_page_view.controller.OvershootViewportInset} +/// The default inset at which the current page will be fully expanded and overshot. /// {@endtemplate} class OvershootViewportInset extends ViewportInset { - /// Create the overshot viewport offset. + /// Create the overshot viewport inset. const OvershootViewportInset(); @override @@ -982,47 +993,47 @@ class OvershootViewportInset extends ViewportInset { -1 * metrics.dimensions.padding.bottom; } -/// {@template exprollable_page_view.controller.DefaultExpandedViewportOffset} -/// The default offset at which the viewport is fully expanded. +/// {@template exprollable_page_view.controller.DefaultExpandedViewportInset} +/// The default inset at which the current page is fully expanded. /// -/// The offset value is always 0.0. +/// The inset value is always 0.0. /// {@endtemplate} class DefaultExpandedViewportInset extends ViewportInset { - /// Create the default expanded viewport offset. + /// Create the default expanded viewport inset. const DefaultExpandedViewportInset(); @override double toConcreteValue(ViewportMetrics metrics) => 0.0; } -/// {@template exprollable_page_view.controller.DefaultShrunkViewportOffset} -/// The default offset at which the viewport will be fully shrunk. +/// {@template exprollable_page_view.controller.DefaultShrunkViewportInset} +/// The default inset at which the current page will be fully shrunk. /// -/// The preferred offset value is the top padding plus 16.0 pixels, +/// The preferred inset value is the top padding plus 16.0 pixels, /// but if it is less than the lower limit, it will be clamped to that value. /// The lower limit is calculated by subtracting the height of the shrunk page /// from the height of the viewport. This clamping process is necessary to prevent /// unwanted white space between the bottom of the page and the viewport. /// {@endtemplate} class DefaultShrunkViewportInset extends ViewportInset { - /// Create the default shrunk viewport offset. + /// Create the default shrunk viewport inset. const DefaultShrunkViewportInset(); @override double toConcreteValue(ViewportMetrics metrics) { assert(metrics.hasDimensions); const margin = 16.0; - final preferredOffset = metrics.dimensions.padding.top + margin; - final lowerBoundOffset = + final preferredInset = metrics.dimensions.padding.top + margin; + final lowerBoundInset = (1.0 - metrics.minFraction) * metrics.dimensions.height; - return max(preferredOffset, lowerBoundOffset); + return max(preferredInset, lowerBoundInset); } } -/// A viewport offset that is defined by a fractional value. +/// A viewport inset that is defined by a fractional value. class FractionalViewportInset extends ViewportInset { - /// {@template exprollable_page_view.controller.FractionalViewportOffset.new} - /// Creates a viewport offset from a fractional value. + /// {@template exprollable_page_view.controller.FractionalViewportInset.new} + /// Creates a viewport inset from a fractional value. /// /// [fraction] is a relative value of the viewport height substracted /// by the bottom padding and must be between 0.0 and 1.0. @@ -1030,7 +1041,7 @@ class FractionalViewportInset extends ViewportInset { const FractionalViewportInset(this.fraction) : assert(0.0 <= fraction && fraction <= 1.0); - /// The fractional value of the offset. + /// The fractional value of the inset. final double fraction; @override @@ -1039,14 +1050,14 @@ class FractionalViewportInset extends ViewportInset { (metrics.dimensions.height - metrics.dimensions.padding.bottom); } -/// A viewport offset that is defined by a fixed value. +/// A viewport inset that is defined by a fixed value. class FixedViewportInset extends ViewportInset { - /// {@template exprollable_page_view.controller.FixedViewportOffset.new} - /// Creates a viewport offset from a fixed value. + /// {@template exprollable_page_view.controller.FixedViewportInset.new} + /// Creates a viewport inset from a fixed value. /// {@endtemplate} const FixedViewportInset(this.pixels); - /// The fixed value of the offset in terms of logical pixels. + /// The fixed value of the inset in terms of logical pixels. final double pixels; @override diff --git a/package/lib/src/core/view.dart b/package/lib/src/core/view.dart index 0a9dc2b..c288036 100644 --- a/package/lib/src/core/view.dart +++ b/package/lib/src/core/view.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -/// A page view that expands its viewport while scrolling the page. +/// A page view that expands the viewport of the page while scrolling it. class ExprollablePageView extends StatefulWidget { const ExprollablePageView({ super.key, @@ -26,57 +26,69 @@ class ExprollablePageView extends StatefulWidget { }); /// A builder that creates a scrollable page for a given index. + /// /// Note that **[ExprollablePageView] will not works as expected /// if a [ScrollController] obtained by [PageContentScrollController.of] /// is not attached to the scrollable widget that is returned**. final IndexedWidgetBuilder itemBuilder; - /// The number of pages. Providing null makes the [ExprollablePageView] to scroll infinitely. + /// The number of pages. + /// + /// Providing null makes the [ExprollablePageView] to scroll infinitely. final int? itemCount; - /// An object that can be used to control the position to which this page view is scrolled. - /// It also controlls how the viewport changes as the page scrolls. + /// A controller for the page view. final ExprollablePageController? controller; /// Whether the page view scrolls in the reading direction. + /// /// See [PageView.reverse] for more details. final bool reverse; /// How the page view should respond to user input. + /// /// See [PageView.physics] for more details. final ScrollPhysics? physics; /// Determines the way that drag start behavior is handled. + /// /// See [PageView.dragStartBehavior] for more details. final DragStartBehavior dragStartBehavior; /// Controls whether the widget's pages will respond to [RenderObject.showOnScreen], /// which will allow for implicit accessibility scrolling. + /// /// See [PageView.allowImplicitScrolling] for more detials. final bool allowImplicitScrolling; /// Restoration ID to save and restore the scroll offset of the scrollable. + /// /// See [PageView.restorationId] for more details. final String? restorationId; /// The content will be clipped (or not) according to this option. + /// /// See [PageView.clipBehavior] for more details. final Clip clipBehavior; /// A [ScrollBehavior] that will be applied to this widget individually. + /// /// See [PageView.scrollBehavior] for more detials. final ScrollBehavior? scrollBehavior; /// Whether to add padding to both ends of the list. + /// /// See [PageView.padEnds] for more details. final bool padEnds; - /// Called whnever the viewport fraction or offset changes. Providing this callback - /// is equivalent to subscribing to [ExprollablePageController.viewport]. + /// Called whnever the viewport fraction or inset changes. + /// + /// Providing this callback is equivalent to subscribing to [ExprollablePageController.viewport]. final void Function(ViewportMetrics metrics)? onViewportChanged; - /// Called whenever the focused page changes. Providing this callback - /// is equivalent to subscribing to [ExprollablePageController.currentPage]. + /// Called whenever the focused page changes. + /// + /// Providing this callback is equivalent to subscribing to [ExprollablePageController.currentPage]. final void Function(int page)? onPageChanged; @override From 81e7482c51363f2276b40ab2d69fde61de1113ec Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 16 May 2023 21:41:07 +0900 Subject: [PATCH 14/24] replace 'offset' with 'inset' --- example/lib/main.dart | 10 +++++----- ...ts_example.dart => custom_snap_insets_example.dart} | 10 +++++----- maps_example/lib/main.dart | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) rename example/lib/src/{custom_snap_offsets_example.dart => custom_snap_insets_example.dart} (81%) diff --git a/example/lib/main.dart b/example/lib/main.dart index 08e3bf2..5c164b6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,6 +1,6 @@ import 'package:example/src/adaptive_padding_example.dart'; import 'package:example/src/complex_example/complex_example.dart'; -import 'package:example/src/custom_snap_offsets_example.dart'; +import 'package:example/src/custom_snap_insets_example.dart'; import 'package:example/src/gutter_example.dart'; import 'package:example/src/modal_dialog_example.dart'; import 'package:example/src/overshoot_effect_example.dart'; @@ -34,8 +34,8 @@ class Home extends StatelessWidget { @override Widget build(BuildContext context) { - void pushRoute(Widget widget) => Navigator.of(context) - .push(MaterialPageRoute(builder: (_) => widget)); + void pushRoute(Widget widget) => + Navigator.of(context).push(MaterialPageRoute(builder: (_) => widget)); return ListView( children: [ @@ -48,8 +48,8 @@ class Home extends StatelessWidget { onTap: () => pushRoute(const OvershootEffectExample()), ), ListTile( - title: const Text("Custom Snap Offsets Example"), - onTap: () => pushRoute(const CustomSnapOffsetsExample()), + title: const Text("Custom Snap Insets Example"), + onTap: () => pushRoute(const CustomSnapInsetsExample()), ), ListTile( title: const Text("Gutter Example"), diff --git a/example/lib/src/custom_snap_offsets_example.dart b/example/lib/src/custom_snap_insets_example.dart similarity index 81% rename from example/lib/src/custom_snap_offsets_example.dart rename to example/lib/src/custom_snap_insets_example.dart index 62d2199..7dca385 100644 --- a/example/lib/src/custom_snap_offsets_example.dart +++ b/example/lib/src/custom_snap_insets_example.dart @@ -2,15 +2,15 @@ import 'package:example/src/common.dart'; import 'package:exprollable_page_view/exprollable_page_view.dart'; import 'package:flutter/material.dart'; -class CustomSnapOffsetsExample extends StatefulWidget { - const CustomSnapOffsetsExample({super.key}); +class CustomSnapInsetsExample extends StatefulWidget { + const CustomSnapInsetsExample({super.key}); @override - State createState() => - _CustomSnapOffsetsExampleState(); + State createState() => + _CustomSnapInsetsExampleState(); } -class _CustomSnapOffsetsExampleState extends State { +class _CustomSnapInsetsExampleState extends State { late final ExprollablePageController controller; @override diff --git a/maps_example/lib/main.dart b/maps_example/lib/main.dart index a62c437..18b1e28 100644 --- a/maps_example/lib/main.dart +++ b/maps_example/lib/main.dart @@ -23,10 +23,10 @@ class _MapsExampleState extends State { @override void initState() { super.initState(); - const peekOffset = ViewportInset.fractional(0.7); + const peekInset = ViewportInset.fractional(0.7); _pageController = ExprollablePageController( viewportConfiguration: ViewportConfiguration( - extraSnapInsets: [peekOffset], + extraSnapInsets: [peekInset], ), ); } From cfb8f4da221a07250da715c52f8fc3450ee5d9bf Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 16 May 2023 21:43:08 +0900 Subject: [PATCH 15/24] fix the analysis issue --- maps_example/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/maps_example/pubspec.yaml b/maps_example/pubspec.yaml index 672f7e4..e23de0b 100644 --- a/maps_example/pubspec.yaml +++ b/maps_example/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: path: ../package http: ^0.13.5 json_serializable: ^6.6.1 + json_annotation: ^4.8.1 dev_dependencies: flutter_test: From e60d853c094794da388a7d8cf12b7e0c72c4145d Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 16 May 2023 22:08:10 +0900 Subject: [PATCH 16/24] fix doc comment --- package/lib/src/core/view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/lib/src/core/view.dart b/package/lib/src/core/view.dart index c288036..a70a843 100644 --- a/package/lib/src/core/view.dart +++ b/package/lib/src/core/view.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; -/// A page view that expands the viewport of the page while scrolling it. +/// A page view that expands the viewport of the current page while scrolling it. class ExprollablePageView extends StatefulWidget { const ExprollablePageView({ super.key, From fdce968edd89e7e143263607e86077848f73689d Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Tue, 16 May 2023 23:48:07 +0900 Subject: [PATCH 17/24] typo --- package/lib/src/core/controller.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index d382887..b2edb66 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -242,7 +242,7 @@ class ViewportDimensions { /// {@template exprollable_page_view.controller.ViewportMetrics} /// The state of the viewport is described by the 2 mesurements: fraction and inset. /// The fraction indicates how much space each page should occupy in the viewport, -/// and hhe inset is the distance from the top of the viewport to the top of the current page viewport. +/// and the inset is the distance from the top of the viewport to the top of the current page viewport. /// {@endtemplate} mixin ViewportMetrics { /// The mesurements of the viewport. @@ -253,7 +253,7 @@ mixin ViewportMetrics { /// Indicates if [dimensions] property is available. bool get hasDimensions; - /// Indicates how much space each page should occupy in the viewport. + /// The fraction of the viewport that the each page should occupy. /// /// [fraction] must be between [minFraction] and [maxFraction] including both edges. double get fraction; @@ -768,7 +768,7 @@ class PageViewport extends ChangeNotifier { double get fraction => _fraction; /// The distance from the top of the viewport to the top of this page viewport. - /// + /// /// This value will be equal to [Viewport.inset] if [page] is the current page. double get offset => _isPageActive ? _pageController.viewport.inset From b6219b64e7622a325fc76bed160bc89a3add3e93 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 17 May 2023 01:27:33 +0900 Subject: [PATCH 18/24] remove duplicated mixin --- package/lib/src/core/controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index b2edb66..98253e0 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -599,7 +599,7 @@ class ViewportConfiguration { /// This object subscribes to the given [ScrollAbsorber] to calculates the [inset] and [fraction] /// depending on [ScrollAbsorber.pixels], and if there are any changes, notifies its listeners. class Viewport extends ChangeNotifier - with ViewportMetrics, ViewportMetrics + with ViewportMetrics implements ValueListenable { /// Creates an object that represents the state of the **conceptual** viewport. Viewport({ From fc1f88ccbd4fda3f41b2fe8dcb9c248dcb1ddff6 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 17 May 2023 01:37:44 +0900 Subject: [PATCH 19/24] Add some props for backward compatibility --- package/lib/src/core/controller.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package/lib/src/core/controller.dart b/package/lib/src/core/controller.dart index 98253e0..e24d831 100644 --- a/package/lib/src/core/controller.dart +++ b/package/lib/src/core/controller.dart @@ -767,6 +767,12 @@ class PageViewport extends ChangeNotifier { /// The fraction of the viewport that the page should occupy. double get fraction => _fraction; + /// The lower bound of [fraction]. + double get minFraction => _pageController.viewport.minFraction; + + /// The upper bound of [fraction]. + double get maxFraction => _pageController.viewport.maxFraction; + /// The distance from the top of the viewport to the top of this page viewport. /// /// This value will be equal to [Viewport.inset] if [page] is the current page. From 59b17c86d43b680042b52aa7ae745498b6bb0451 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 17 May 2023 02:15:54 +0900 Subject: [PATCH 20/24] update README for version 1.0.0-pre --- package/README.md | 272 ++++++++++++------ .../viewport-fraction-offset.png | Bin resources/{ => Archives}/viewport-offsets.png | Bin resources/viewport-fraction-and-inset.png | Bin 0 -> 14782 bytes resources/viewport-insets.png | Bin 0 -> 109194 bytes 5 files changed, 188 insertions(+), 84 deletions(-) rename resources/{ => Archives}/viewport-fraction-offset.png (100%) rename resources/{ => Archives}/viewport-offsets.png (100%) create mode 100644 resources/viewport-fraction-and-inset.png create mode 100644 resources/viewport-insets.png diff --git a/package/README.md b/package/README.md index 3c8afa8..ba95394 100644 --- a/package/README.md +++ b/package/README.md @@ -4,39 +4,55 @@ # ExprollablePageView -Yet another PageView widget that expands its viewport as it scrolls. **Exprollable** is a coined word combining the words expandable and scrollable. This project is an attemt to clone a modal sheet UI used in [Apple Books](https://www.apple.com/jp/apple-books/) app on iOS. +Yet another PageView widget that expands the viewport of the current page while scrolling it. **Exprollable** is a coined word combining the words expandable and scrollable. This project is an attemt to clone a modal sheet UI used in [Apple Books](https://www.apple.com/jp/apple-books/) app on iOS. Here is an example of what you can do with this widget: +## Announcement + +### 05-17-2023 + +Version 1.0.0-pre has been released 🎉. This version contains some break changes, please follow [the migration guide](#100-beta-to-100-pre). + + + ## Index - [ExprollablePageView](#exprollablepageview) + - [Announcement](#announcement) + - [05-17-2023](#05-17-2023) - [Index](#index) - [Try it](#try-it) - [Install](#install) - [Usage](#usage) - [ExprollablePageView](#exprollablepageview-1) - [ExprollablePageController](#exprollablepagecontroller) - - [Viewport fraction and offset](#viewport-fraction-and-offset) + - [Viewport fraction and inset](#viewport-fraction-and-inset) - [Overshoot effect](#overshoot-effect) - [ModalExprollable](#modalexprollable) - [Slidable list items](#slidable-list-items) - [How to](#how-to) - [get the curret page?](#get-the-curret-page) - - [make the PageView like a BottomSheet?](#make-the-pageview-like-a-bottomsheet) - - [observe the state of the viewport?](#observe-the-state-of-the-viewport) + - [make the page view like a BottomSheet?](#make-the-page-view-like-a-bottomsheet) + - [observe the viewport state?](#observe-the-viewport-state) - [1. Listen `ExprollablePageController.viewport`](#1-listen-exprollablepagecontrollerviewport) - - [2. Listen `PageViewportUpdateNotification`](#2-listen-pageviewportupdatenotification) + - [2. Listen `ViewportUpdateNotification`](#2-listen-viewportupdatenotification) - [3. Use `onViewportChanged` callback](#3-use-onviewportchanged-callback) - [add space between pages?](#add-space-between-pages) - - [prevent my AppBar going off the screen when `overshootEffect` is true?](#prevent-my-appbar-going-off-the-screen-when-overshooteffect-is-true) + - [prevent my app bar going off the screen when overshoote ffect is true?](#prevent-my-app-bar-going-off-the-screen-when-overshoote-ffect-is-true) - [animate the viewport state?](#animate-the-viewport-state) + - [Migration guide](#migration-guide) + - [^1.0.0-beta to ^1.0.0-pre](#100-beta-to-100-pre) + - [PageViewportMetrics update](#pageviewportmetrics-update) + - [ViewportController update](#viewportcontroller-update) + - [ViewportOffset update](#viewportoffset-update) + - [ExprollablePageController update](#exprollablepagecontroller-update) + - [Other renamed classes](#other-renamed-classes) - [Questions](#questions) - [Contributing](#contributing) - ## Try it Run the example application and explore the all features of this package. @@ -59,12 +75,12 @@ flutter pub add exprollable_page_view ## Usage -See [how-to](#how-to) section If you are looking for specific usages. +See [how-to](#how-to) section if you are looking for specific usages. ### ExprollablePageView -You can use `ExprollablePageView` just as built-in `PageView` as bellow. `ExprollablePageView` works with any scrollable widget that can accept a `ScrollController`. Note, however, that **it will not work as expected unless you use a `ScrollController` obtained from `PageContentScrollController.of`**. +You can use `ExprollablePageView` just as built-in `PageView` as bellow. `ExprollablePageView` works with any scrollable widgets that can accept a `ScrollController`. Note, however, that **it will not work as expected unless you use a `ScrollController` obtained from `PageContentScrollController.of`**. ```dart import 'package:exprollable_page_view/exprollable_page_view.dart'; @@ -87,7 +103,7 @@ Widget build(BuildContext context) { } ``` -The constructor of `ExprollablePageView` has almost the same signature as `PageView.builder`. See [PageView's docs](https://api.flutter.dev/flutter/widgets/PageView/PageView.builder.html) for more details on each parameter. +The constructor of `ExprollablePageView` has almost the same signature as `PageView.builder`. See [the document of PageView](https://api.flutter.dev/flutter/widgets/PageView/PageView.builder.html) for more details on each parameter. ```dart const ExprollablePageView({ @@ -110,52 +126,58 @@ The constructor of `ExprollablePageView` has almost the same signature as `PageV ### ExprollablePageController -A subclass of `PageController` that is used by the internal `PageView`. It also controlls how the `ExprollablePageView` changes its viewport as it scrolls. +A subclass of `PageController` that will be attached to the internal `PageView`. It also controlls how the viewport of the current page changes along with vertical scrolling. ```dart - ExprollablePageController({ - super.initialPage, - super.keepPage, - double minViewportFraction = 0.9, - bool overshootEffect = false, - ViewportOffset initialViewportOffset = ViewportOffset.shrunk, - ViewportOffset maxViewportOffset = ViewportOffset.shrunk, - List snapViewportOffsets = const [ - ViewportOffset.expanded, - ViewportOffset.shrunk, - ], - }); +final controller = ExprollablePageController( + initialPage: 0, + viewportConfiguration: ViewportConfiguration( + minFraction: 0.9, + ), +); ``` -- `initialPage`: The page to show when first creating the `ExprollablePageView`. - -- `minViewportFraction`: The minimum fraction of the viewport that each page should occupy. It must be between 0.0 ~ 1.0. - -- `initialViewportOffset`: The initial offset of the viewport. +Specify a `ViewportConfiguration` with the desired values to tweak the behavior of the page view. -- `maxViewportOffset`: The maximum offset of the viewport. Typically used with custom `snapViewportOffsets`. - -- `snapViewportOffsets`: A list of offsets to snap the viewport to. An example of this feature can be found in [make the PageView like a BottomSheet](#make-the-pageview-like-a-bottomsheet) section. +```dart +factory ViewportConfiguration({ + bool overshootEffect = false, + double minFraction = 0.9, + double maxFraction = 1.0, + ViewportInset shrunkInset = ViewportInset.shrunk, + ViewportInset? initialInset, + List extraSnapInsets = const [], +}); +``` +- `minFraction`: The fraction of the viewport that each page should occupy when it is shrunk by vertical scrolling. +- `initialInset`: The initial [viewport inset](#viewport-fraction-and-inset). +- `shrunkInset`: A viewport inset at which the current page is fully shrunk. +- `extraSnapInsets`: A list of extra insets the viewport will snap to. An example of the use of this feature can be found in [make the page view like a BottomSheet](#make-the-page-view-like-a-bottomsheet) section. - `overshootEffect`: Indicates if overshoot effect is enabled. See [Overshoot effect](#overshoot-effect) section for more details. - +#### Viewport fraction and inset + +The state of the viewport is described by the 2 mesurements: **fraction** and **inset**. The fraction indicates how much space each page should occupy in the viewport, and the inset is the distance from the top of the viewport to the top of the current page viewport. These measurements are managed in `Viewport` class, and can be referenced via the controller as it is exposed as `ExprollablePageController.viewport`. See [observe the vewport state](#observe-the-viewport-state) section for more details. -#### Viewport fraction and offset +![viewport-fraction-and-inset](https://github.com/fujidaiti/exprollable_page_view/assets/68946713/128a7788-112f-45fd-957f-626b0176b052) -The state of the viewport is described by the 2 mesurements: **fraction** and **offset**. A fraction indicates how much space each page should occupy in the viewport, and it must be between 0.0 and 1.0. An offset is the distance from the top of the viewport to the top of the current page. +`ViewportInset` is a class that represents an inset. There are 3 predefined `ViewportInset`s: -![viewport-fraction-offset](https://user-images.githubusercontent.com/68946713/231830114-f4d9bec4-cb85-41f8-a9fd-7b3f21ff336a.png) +- `ViewportInset.expanded`: The default inset at which the current page is fully expanded. +- `ViewportInset.shrunk` : The default inset at which the current page is fully shrunk. +- `ViewportInset.overshoot` : The default inset at which the current page is fully expanded and overshot (see [Overshoot effect](#overshoot-effect)). -`ViewportOffset` is a class that represents an offset. It has 2 pre-defined offsets, `ViewportOffset.expanded` and `ViewportOffset.shrunk`, at which the viewport fraction is 1.0 and the minimum, respectively. It also has a factory constructor `ViewportOffset.fractional` that creates an offset from a fractional value. For example, `ViewportOffset.fractional(1.0)` is equivalent to `ViewportOffset.shrunk`, and `ViewportOffset.fractional(0.0)` matches the bottom of the viewport. Some examples of the use of this class can be found in [make the PageView like a BottomSheet](#make-the-pageview-like-a-bottomsheet), [observe the state of the viewport](#observe-the-state-of-the-viewport). +User defined insets can be created using `ViewportInset.fixed` and `ViewportInset.fractional`, or you can extend `ViewportInset` to perform more complex calculations. Some examples of the use of this class can be found in [make the PageView like a BottomSheet](#make-the-pageview-like-a-bottomsheet), [observe the state of the viewport](#observe-the-state-of-the-viewport). -![viewport-offsets](https://user-images.githubusercontent.com/68946713/231827251-fed9575c-980a-40b8-b01a-da984d58f3ec.png) +![viewport-insets](https://github.com/fujidaiti/exprollable_page_view/assets/68946713/23aa944c-61d4-4578-b194-cf4224fe757c) #### Overshoot effect -If `ExprollablePageController.overshootEffect` is enabled, the upper segment of the current page will slightly exceed the top of the viewport when it goes fullscreen. To be precise, this means that the viewport offset will take a negative value when the viewport fraction is 1.0. This trick creates a dynamic visual effect when the page goes fullscreen. The figures below are a demonstration of how the overshoot effect affects (disabled in the left, enabled in the right). + If the overshoot effect is enabled, the upper segment of the current page viewport will + slightly exceed the top of the viewport when it goes fullscreen. To be precise, this means that the viewport inset will take a negative value when the viewport fraction is 1. This trick creates a dynamic visual effect when the page goes fullscreen. The figures below are demonstrations of how the overshoot effect affects (disabled in the left, enabled in the right). ![overshoot-disabled](https://user-images.githubusercontent.com/68946713/231827343-155a750d-b21f-4a96-b81a-74c8873c46cb.gif) ![overshoot-enabled](https://user-images.githubusercontent.com/68946713/231827364-40843efc-5a91-49ff-ab74-c9af1e4b0c62.gif) @@ -167,25 +189,29 @@ Overshoot effect will works correctly only if: Perhaps the most common use is to wrap an `ExprollablePageView` with a `Scaffold`. In that case, do not forget to enable `Scaffold.extentBody` and then everything should be fine. ```dart -controller = ExprollablePageController(overshootEffect: true); - -Widget build(BuildContext context) { - return Scaffold( - extendBody: true, - bottomNavigationBar: BottomNavigationBar(...), - body: ExprollablePageView( - controller: controller, - itemBuilder: (context, page) { ... }, + controller = ExprollablePageController( + viewportConfiguration: ViewportConfiguration( + overshootEffect: true, // Enable the overshoot effect ), ); -} + + Widget build(BuildContext context) { + return Scaffold( + extendBody: true, // Do not forget this line + bottomNavigationBar: BottomNavigationBar(...), + body: ExprollablePageView( + controller: controller, + itemBuilder: (context, page) { ... }, + ), + ); + } ``` ### ModalExprollable -Use `ModalExprollable` to create modal dialog style PageViews. This widget adds a translucent background and *swipe down to dismiss* action to your `ExprollablePageView`. You can use `showModalExprollable` a convenience function that wraps your `ExprollablePageView` with `ModalExprollable` and display it as a dialog. If you want to customize reveal/dismiss behavior of the dialog, create your own `PageRoute` and use `ModalExprollable` in it. +Use `ModalExprollable` to create modal dialog style page views. This widget adds a translucent background and *swipe down to dismiss* action to your `ExprollablePageView`. You can use `showModalExprollable` a convenience function that wraps your `ExprollablePageView` with `ModalExprollable` and display it as a dialog. If you want to customize reveal/dismiss behavior of the dialog, create your own `PageRoute` and use `ModalExprollable` in it. ```dart showModalExprollable( @@ -201,7 +227,7 @@ showModalExprollable( ### Slidable list items -One of the advantages of `ExprollablePageView` over built-in `PageView` is that widgets with horizontal slide action such as [flutter_slidable](https://pub.dev/packages/flutter_slidable) can be used within each page. You can see an example that uses flutter_slidable in `example/lib/src/complex_example/album_details.dart`. +One of the advantages of `ExprollablePageView` over the built-in `PageView` is that widgets with horizontal slide action such as [flutter_slidable](https://pub.dev/packages/flutter_slidable) can be used within a page. You can see an example that uses flutter_slidable in `example/lib/src/complex_example/album_details.dart`. ![SlideActionDemo](https://user-images.githubusercontent.com/68946713/231349155-aa6bb0a7-f85f-4bab-b7d0-30692338f61b.gif) @@ -235,61 +261,52 @@ ExprollablePageView( -### make the PageView like a BottomSheet? +### make the page view like a BottomSheet? -Use `ExprollablePageController` . Below is an example controller for snapping to the three states: +Use `ExprollablePageController` and `ViewportConfiguration`. Below is an example controller for snapping to the three states: -1. The viewport is completely expanded (`viewportFraction == 1.0`) -2. The viewport is slightly smaller than the screen (`viewportFraction == 0.9`) -3. `viewportFraction == 0.9` and the PageView covers only half of the screen like BottomSheet. +1. The page is completely expanded (`Viewport.fraction == 1.0`) +2. The page is slightly smaller than the viewport (`Viewport.fraction == 0.9`) +3. `Viewport.fraction == 0.9` and the page view covers only half of the screen like a bottom sheet. For complete code, see [custom_snap_offsets_example.dart](https://github.com/fujidaiti/exprollable_page_view/blob/master/example/lib/src/custom_snap_offsets_example.dart). ```dart -const peekOffset = ViewportOffset.fractional(0.5); controller = ExprollablePageController( - minViewportFraction: 0.9, - initialViewportOffset: peekOffset, - maxViewportOffset: peekOffset, - snapViewportOffsets: [ - ViewportOffset.expanded, - ViewportOffset.shrunk, - peekOffset, - ], + viewportConfiguration: ViewportConfiguration( + minFraction: 0.9, + extraSnapInsets: [ + ViewportInset.fractional(0.5), + ], + ), ); ``` -You can also use `ExprollablePageController.withAdditionalSnapOffsets` as a shorthand. The following snippet is equevalent to the above: - -```dart -controller = ExprollablePageController.withAdditionalSnapOffsets([peekOffset]); -``` - -### observe the state of the viewport? +### observe the viewport state? There are 3 ways to observe changes of the viewport state. #### 1. Listen `ExprollablePageController.viewport` -`ExprollablePageController.viewport` is a `ValueListenable` and `PageViewportMetrics` contains the current state of the viewport. Thus, you can listen and use it to perfom some actions that depend on the viewport state. +`ExprollablePageController.viewport` is a `ValueListenable` and `ViewportMetrics` contains the current state of the viewport. Thus, you can listen and use it to perfom some actions that depend on the viewport state. ```dart controller.viewport.addListener(() { - final PageViewportMetrics vp = controller.viewport.value; - final bool isShrunk = vp.isShrunk; - final bool isExpanded = vp.isExpanded; + final ViewportMetrics vp = controller.viewport.value; + final bool isShrunk = vp.isPageShrunk; + final bool isExpanded = vp.isPageExpanded; }); ``` -#### 2. Listen `PageViewportUpdateNotification` +#### 2. Listen `ViewportUpdateNotification` -`ExprollablePageView` dispatches `PageViewportUpdateNotification` every time its state changes, and it contains a `PageViewportMetrics`. You can listen the notifications using `NotificationListener` widget. Make sure that the `NotificationListener` is an ancestor of the `ExprollablePageView` in your widget tree. This method is useful when you want to perform a state-dependent action, but do not have a controller. +`ExprollablePageView` dispatches `ViewportUpdateNotification` every time its state changes, and it contains a `ViewportMetrics`. You can listen the notifications using `NotificationListener` widget. Make sure that the `NotificationListener` is an ancestor of the `ExprollablePageView` in your widget tree. This method is useful when you want to perform a state dependent action, but do not have a controller. ```dart -NotificationListener( +NotificationListener( onNotification: (notification) { - final PageViewportMetrics vp = notification.metrics; + final ViewportMetrics vp = notification.metrics; return false; }, child: ..., @@ -301,7 +318,7 @@ The constructor of `ExprollablePageView` accepts a callback that is invoked when ```dart ExprollablePageView( - onPageViewChanged: (PageViewportMetrics vp) {...}, + onPageViewChanged: (ViewportMetrics vp) {...}, ); ``` @@ -321,7 +338,7 @@ ExprollablePageView( ); ``` -### prevent my AppBar going off the screen when `overshootEffect` is true? +### prevent my app bar going off the screen when overshoote ffect is true? Use `AdaptivePagePadding`. This widget adds appropriate padding to its child according to the current viewpor offset. An example code is found in [adaptive_padding_example.dart](https://github.com/fujidaiti/exprollable_page_view/blob/master/example/lib/src/adaptive_padding_example.dart). @@ -339,17 +356,104 @@ Container( ### animate the viewport state? -Use `ExprollablePageController.animateViewportOffsetTo`. +Use `ExprollablePageController.animateViewportInsetTo`. ```dart -controller.animateViewportOffsetTo( - ViewportOffset.shrunk, +// Shrunk the current page with scroll animation in 1 second. +controller.animateViewportInsetTo( + ViewportInset.shrunk, curve: Curves.easeInOutSine, duration: Duration(seconds: 1), ); ``` + +## Migration guide + +### ^1.0.0-beta to ^1.0.0-pre + +With the release of version 1.0.0-pre, there are several breaking changes. + +#### PageViewportMetrics update + +`PageViewportMetrics` mixin was merged into `ViewportMetrics` mixin and now deleted, and some properties were renamed. Replace the symbols in your code according to the table below: + +- `PageViewportMetrics` 👉 `ViewportMetrics` +- `PageViewportMetrics.isShrunk` 👉 `ViewportMetrics.isPageShrunk` +- `PageViewportMetrics.isExpanded` 👉 `ViewportMetrics.isPageExpanded` +- `PageViewportMetrics.xxxOffset` 👉 `ViewportMetrics.xxxInset` (the all properties with suffix `Offset` was renamed with the new suffix `Inset`) +- `PageViewportMetrics.overshootEffect` was deleted + +#### ViewportController update + + `ViewportController` class was renamed to `PageViewport` and no longer mixins `ViewportMetrics`. + +#### ViewportOffset update + +For `ViewportOffset` and its inherited classes, the suffix `Offset` was replaced with the new suffix `Inset`, and 2 new inherited classes were introduced (see [Viewport fraction and inset](#viewport-fraction-and-inset)). + +- `ViewportOffset` 👉 `ViewportInset` + +- `ExpandedViewportOffset` 👉 `DefaultExpandedViewportinset` +- `ShrunkViewportOffset` 👉 `DefaultShrunkViewportInset` + +#### ExprollablePageController update + +With the introduction of `ViewportConfiguration`, the signature of `ExprollablePageController`'s constructor was changed. + +Before: + +```dart + final controller = ExprollablePageController( + initialPage: 0, + minViewportFraction: 0.9, + overshootEffect: true, + initialViewportOffset: ViewportOffset.shrunk, + ); +``` + +After: + +```dart +final controller = ExprollablePageController( + initialPage: 0, + viewportConfiguration: ViewportConfiguration( + minFraction: 0.9, + overshootEffect: true, + initialInset: ViewportInset.shrunk, + ), +); +``` + +In addition, `ExprollablePageController.withAdditionalSnapOffsets` was removed, use `ViewportConfiguration.extraSnapInsets` instead. See [ExprollablePageController](#exprollablepagecontroller) section for more details. + +Before: + +```dart +final controller = ExprollablePageController.withAdditionalSnapOffsets([ + ViewportOffset.fractional(0.5), +]); +``` + +After: + +```dart +final controller = ExprollablePageController( + viewportConfiguration: ViewportConfiguration( + extraSnapOffset: [ViewportInset.fractional(0.5)], + ), +); +``` + +#### Other renamed classes + +- `StaticPageViewportMetrics` 👉 `StaticViewportMetrics` +- `PageViewportUpdateNotification` 👉 `ViewportUpdateNotification` +- `PageViewport` 👉 `Viewport` + + + ## Questions If you have any question, feel free to ask them on the [discussions page](https://github.com/fujidaiti/exprollable_page_view/discussions/categories/q-a). diff --git a/resources/viewport-fraction-offset.png b/resources/Archives/viewport-fraction-offset.png similarity index 100% rename from resources/viewport-fraction-offset.png rename to resources/Archives/viewport-fraction-offset.png diff --git a/resources/viewport-offsets.png b/resources/Archives/viewport-offsets.png similarity index 100% rename from resources/viewport-offsets.png rename to resources/Archives/viewport-offsets.png diff --git a/resources/viewport-fraction-and-inset.png b/resources/viewport-fraction-and-inset.png new file mode 100644 index 0000000000000000000000000000000000000000..11f8901bdacd04ef403a0aa272bd6e41285c43e7 GIT binary patch literal 14782 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 CMEs}# literal 0 HcmV?d00001 diff --git a/resources/viewport-insets.png b/resources/viewport-insets.png new file mode 100644 index 0000000000000000000000000000000000000000..63fb31e39a31456d26c57bd7e298f2c90bdb389b GIT binary patch literal 109194 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 Y_}%6_aryF%T}|M@eeHY2cPzvH8!NOCW&i*H literal 0 HcmV?d00001 From c5aeda70499461a55d0ded8b623661211334043c Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 17 May 2023 02:17:29 +0900 Subject: [PATCH 21/24] typo --- package/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/README.md b/package/README.md index ba95394..7230ce2 100644 --- a/package/README.md +++ b/package/README.md @@ -14,7 +14,7 @@ Here is an example of what you can do with this widget: ### 05-17-2023 -Version 1.0.0-pre has been released 🎉. This version contains some break changes, please follow [the migration guide](#100-beta-to-100-pre). +Version 1.0.0-pre has been released 🎉. This version contains some breaking changes, please follow [the migration guide](#100-beta-to-100-pre). From 5e8e31f71fa90313c31809208405c60ace6bfd59 Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 17 May 2023 02:51:15 +0900 Subject: [PATCH 22/24] Some improvements --- package/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package/README.md b/package/README.md index 7230ce2..f1a8b52 100644 --- a/package/README.md +++ b/package/README.md @@ -14,7 +14,7 @@ Here is an example of what you can do with this widget: ### 05-17-2023 -Version 1.0.0-pre has been released 🎉. This version contains some breaking changes, please follow [the migration guide](#100-beta-to-100-pre). +Version 1.0.0-rc.1 has been released 🎉. This version includes several breaking changes, so if you are already using ^1.0.0-beta, you may need to migrate according to [Migration Guide](#100-beta-to-100-rc1). @@ -44,7 +44,7 @@ Version 1.0.0-pre has been released 🎉. This version contains some breaking ch - [prevent my app bar going off the screen when overshoote ffect is true?](#prevent-my-app-bar-going-off-the-screen-when-overshoote-ffect-is-true) - [animate the viewport state?](#animate-the-viewport-state) - [Migration guide](#migration-guide) - - [^1.0.0-beta to ^1.0.0-pre](#100-beta-to-100-pre) + - [^1.0.0-beta to ^1.0.0-rc.1](#100-beta-to-100-rc1) - [PageViewportMetrics update](#pageviewportmetrics-update) - [ViewportController update](#viewportcontroller-update) - [ViewportOffset update](#viewportoffset-update) @@ -53,6 +53,7 @@ Version 1.0.0-pre has been released 🎉. This version contains some breaking ch - [Questions](#questions) - [Contributing](#contributing) + ## Try it Run the example application and explore the all features of this package. @@ -371,9 +372,9 @@ controller.animateViewportInsetTo( ## Migration guide -### ^1.0.0-beta to ^1.0.0-pre +### ^1.0.0-beta to ^1.0.0-rc.1 -With the release of version 1.0.0-pre, there are several breaking changes. +With the release of version 1.0.0-rc.1, there are several breaking changes. #### PageViewportMetrics update From 2bb3d8989aea25e9150de310308391c632167c5c Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 17 May 2023 02:52:41 +0900 Subject: [PATCH 23/24] typo --- package/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/README.md b/package/README.md index f1a8b52..ce4824a 100644 --- a/package/README.md +++ b/package/README.md @@ -12,7 +12,7 @@ Here is an example of what you can do with this widget: ## Announcement -### 05-17-2023 +### 17-05-2023 Version 1.0.0-rc.1 has been released 🎉. This version includes several breaking changes, so if you are already using ^1.0.0-beta, you may need to migrate according to [Migration Guide](#100-beta-to-100-rc1). @@ -22,7 +22,7 @@ Version 1.0.0-rc.1 has been released 🎉. This version includes several breakin - [ExprollablePageView](#exprollablepageview) - [Announcement](#announcement) - - [05-17-2023](#05-17-2023) + - [17-05-2023](#17-05-2023) - [Index](#index) - [Try it](#try-it) - [Install](#install) From f1d3371a99bea7b43fd02c2c8158bf61616db6ee Mon Sep 17 00:00:00 2001 From: fujidaiti Date: Wed, 17 May 2023 03:04:25 +0900 Subject: [PATCH 24/24] release 1.0.0-rc.1 :tada: --- package/CHANGELOG.md | 8 ++++++++ package/pubspec.yaml | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/package/CHANGELOG.md b/package/CHANGELOG.md index 27978a5..ea8d58d 100644 --- a/package/CHANGELOG.md +++ b/package/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.0.0-rc.1 17-05-2023 + +This version contains some breaking changes (see the migraiton guide in README). + +- The behavior of the viewport is now more customizable (#24) +- Terminology was reorganized and some classes and properties were renamed accordingly +- Some improvements in the documents + ## 1.0.0-beta.8 05-05-2023 - Fix #25, #26 diff --git a/package/pubspec.yaml b/package/pubspec.yaml index d2b3ba2..8ca0640 100644 --- a/package/pubspec.yaml +++ b/package/pubspec.yaml @@ -1,10 +1,10 @@ name: exprollable_page_view -description: Yet another PageView widget that expands its viewport as it scrolls. Exprollable is a coined word combining the words expandable and scrollable. -version: 1.0.0-beta.8 +description: Yet another PageView widget that expands its page while scrolling it. Exprollable is a coined word combining the words expandable and scrollable. +version: 1.0.0-rc.1 repository: https://github.com/fujidaiti/exprollable_page_view environment: - sdk: ">=2.19.0 <3.0.0" + sdk: ">=2.19.0 <4.0.0" flutter: ">=1.17.0" dependencies: