From 1478c2caff72b02222c0fe4c202e8dcfb8a52a39 Mon Sep 17 00:00:00 2001 From: codiss Date: Sun, 31 Jul 2022 20:24:36 +0800 Subject: [PATCH] feat: NestedScrollView related optimization. --- CHANGELOG.md | 2 +- example/lib/l10n/translations/en.dart | 1 + example/lib/l10n/translations/zh_cn.dart | 1 + .../lib/page/sample/nested_scroll_view.dart | 38 ++-- example/lib/page/sample/sample_page.dart | 6 +- .../lib/page/sample/tab_bar_view_page.dart | 184 ++++++++++-------- example/lib/widget/skeleton_item.dart | 10 +- example/pubspec.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 6 +- lib/src/behavior/scroll_behavior.dart | 5 +- lib/src/easy_refresh.dart | 4 +- lib/src/notifier/indicator_notifier.dart | 17 +- lib/src/physics/scroll_physics.dart | 23 ++- pubspec.yaml | 2 +- 14 files changed, 167 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e319911d..21df0fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Next +## V 3.0.3 > fix: processedDuration == Duration.zero, can't rebound [#572](https://github.com/xuelongqy/flutter_easy_refresh/pull/572). > fix: [clamping] may not have rebound animation. > fix: Indicator overflow [#575](https://github.com/xuelongqy/flutter_easy_refresh/pull/575). diff --git a/example/lib/l10n/translations/en.dart b/example/lib/l10n/translations/en.dart index a064ba19..3291b6c2 100644 --- a/example/lib/l10n/translations/en.dart +++ b/example/lib/l10n/translations/en.dart @@ -87,4 +87,5 @@ const en = { 'Dogecoin donation': 'Dogecoin donation', '%s copied!': '%s copied!', 'Trigger immediately': 'Trigger immediately', + 'TabBarView example': 'NestedScrollView + TabBarView example', }; diff --git a/example/lib/l10n/translations/zh_cn.dart b/example/lib/l10n/translations/zh_cn.dart index 57ff35cb..cc700acf 100644 --- a/example/lib/l10n/translations/zh_cn.dart +++ b/example/lib/l10n/translations/zh_cn.dart @@ -85,4 +85,5 @@ const zhCN = { 'Dogecoin donation': '狗狗币捐赠', '%s copied!': '%s 已复制!', 'Trigger immediately': '立即触发', + 'TabBarView example': 'NestedScrollView + TabBarView示例', }; diff --git a/example/lib/page/sample/nested_scroll_view.dart b/example/lib/page/sample/nested_scroll_view.dart index e4577ff9..770bc31b 100644 --- a/example/lib/page/sample/nested_scroll_view.dart +++ b/example/lib/page/sample/nested_scroll_view.dart @@ -61,31 +61,23 @@ class NestedScrollViewPageState extends State }, body: Column( children: [ - PreferredSize( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, + TabBar( + controller: _tabController, + labelColor: themeData.colorScheme.primary, + indicatorColor: themeData.colorScheme.primary, + onTap: (index) { + setState(() { + _tabIndex = index; + }); + }, + tabs: const [ + Tab( + text: 'List', ), - child: TabBar( - controller: _tabController, - labelColor: themeData.colorScheme.primary, - indicatorColor: themeData.colorScheme.primary, - onTap: (index) { - setState(() { - _tabIndex = index; - }); - }, - tabs: const [ - Tab( - text: 'List', - ), - Tab( - text: 'Grid', - ), - ], + Tab( + text: 'Grid', ), - ), - preferredSize: const Size(double.infinity, 46.0), + ], ), Expanded( child: IndexedStack( diff --git a/example/lib/page/sample/sample_page.dart b/example/lib/page/sample/sample_page.dart index f62b590e..31b1d86a 100644 --- a/example/lib/page/sample/sample_page.dart +++ b/example/lib/page/sample/sample_page.dart @@ -45,9 +45,9 @@ class _SamplePageState extends State { onTap: () => Get.toNamed(Routes.nestedScrollViewSample), ), ListItem( - title: 'TabView', - subtitle: 'NestedScrollView example'.tr, - icon: Icons.line_style, + title: 'TabBarView', + subtitle: 'TabBarView example'.tr, + icon: Icons.tab_rounded, onTap: () => Get.toNamed(Routes.tabBarViewSample), ), ListItem( diff --git a/example/lib/page/sample/tab_bar_view_page.dart b/example/lib/page/sample/tab_bar_view_page.dart index 484194bc..2ce035cb 100644 --- a/example/lib/page/sample/tab_bar_view_page.dart +++ b/example/lib/page/sample/tab_bar_view_page.dart @@ -41,6 +41,7 @@ class TabBarViewPageState extends State header: ClassicHeader( clamping: true, position: IndicatorPosition.locator, + mainAxisAlignment: MainAxisAlignment.end, dragText: 'Pull to refresh'.tr, armedText: 'Release ready'.tr, readyText: 'Refreshing...'.tr, @@ -88,90 +89,95 @@ class TabBarViewPageState extends State }); }, childBuilder: (context, physics) { - return ExtendedNestedScrollView( - physics: physics, - onlyOneScrollInBody: true, - pinnedHeaderSliverHeightBuilder: () { - return MediaQuery.of(context).padding.top + kToolbarHeight; - }, - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - const HeaderLocator.sliver(clearExtent: false), - SliverAppBar( - expandedHeight: 166, - pinned: true, - flexibleSpace: FlexibleSpaceBar( - title: Text( - 'TabBarView', - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color), + return ScrollConfiguration( + behavior: const ERScrollBehavior(), + child: ExtendedNestedScrollView( + physics: physics, + onlyOneScrollInBody: true, + pinnedHeaderSliverHeightBuilder: () { + return MediaQuery.of(context).padding.top + kToolbarHeight; + }, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + const HeaderLocator.sliver(clearExtent: false), + SliverAppBar( + expandedHeight: 120, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: Text( + 'TabBarView', + style: TextStyle( + color: + Theme.of(context).textTheme.titleLarge?.color), + ), + centerTitle: false, ), - titlePadding: const EdgeInsets.only(left: 64, bottom: 60), - centerTitle: false, ), - bottom: PreferredSize( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, + ]; + }, + body: Column( + children: [ + TabBar( + controller: _tabController, + labelColor: themeData.colorScheme.primary, + indicatorColor: themeData.colorScheme.primary, + tabs: const [ + Tab( + text: 'List', ), - child: TabBar( - controller: _tabController, - labelColor: themeData.colorScheme.primary, - indicatorColor: themeData.colorScheme.primary, - tabs: const [ - Tab( - text: 'List', - ), - Tab( - text: 'Grid', - ), - ], + Tab( + text: 'Grid', ), - ), - preferredSize: const Size(double.infinity, 46), - ), - ), - ]; - }, - body: TabBarView( - controller: _tabController, - children: [ - ExtendedVisibilityDetector( - uniqueKey: const Key('Tab0'), - child: CustomScrollView( - physics: physics, - slivers: [ - SliverList( - delegate: - SliverChildBuilderDelegate((context, index) { - return const SkeletonItem(); - }, childCount: _listCount)), - const FooterLocator.sliver(), ], ), - ), - ExtendedVisibilityDetector( - uniqueKey: const Key('Tab1'), - child: CustomScrollView( - physics: physics, - slivers: [ - SliverGrid( - delegate: - SliverChildBuilderDelegate((context, index) { - return const SkeletonItem( - direction: Axis.horizontal, - ); - }, childCount: _gridCount), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 6 / 7, - )), - const FooterLocator.sliver(), - ], + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + ExtendedVisibilityDetector( + uniqueKey: const Key('Tab0'), + child: _AutomaticKeepAlive( + child: CustomScrollView( + physics: physics, + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return const SkeletonItem(); + }, childCount: _listCount)), + const FooterLocator.sliver(), + ], + ), + ), + ), + ExtendedVisibilityDetector( + uniqueKey: const Key('Tab1'), + child: _AutomaticKeepAlive( + child: CustomScrollView( + physics: physics, + slivers: [ + SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, index) { + return const SkeletonItem( + direction: Axis.horizontal, + ); + }, childCount: _gridCount), + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 6 / 7, + )), + const FooterLocator.sliver(), + ], + ), + ), + ), + ], + ), ), - ), - ], + ], + ), ), ); }, @@ -179,3 +185,27 @@ class TabBarViewPageState extends State ); } } + +class _AutomaticKeepAlive extends StatefulWidget { + final Widget child; + + const _AutomaticKeepAlive({ + Key? key, + required this.child, + }) : super(key: key); + + @override + State<_AutomaticKeepAlive> createState() => _AutomaticKeepAliveState(); +} + +class _AutomaticKeepAliveState extends State<_AutomaticKeepAlive> + with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/example/lib/widget/skeleton_item.dart b/example/lib/widget/skeleton_item.dart index 4d8d36cf..279710b6 100644 --- a/example/lib/widget/skeleton_item.dart +++ b/example/lib/widget/skeleton_item.dart @@ -65,7 +65,7 @@ class SkeletonItem extends StatelessWidget { } return Card( elevation: 0, - color: Theme.of(context).colorScheme.surfaceVariant, + color: backgroundColor, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -75,7 +75,7 @@ class SkeletonItem extends StatelessWidget { margin: const EdgeInsets.only(bottom: 16), height: 80, width: 80, - color: Theme.of(context).splashColor, + color: foregroundColor, ), Expanded( child: Row( @@ -88,19 +88,19 @@ class SkeletonItem extends StatelessWidget { constraints: const BoxConstraints( maxHeight: 200, ), - color: Theme.of(context).splashColor, + color: foregroundColor, ), Container( margin: const EdgeInsets.only(left: 16), width: 12, height: 80, - color: Theme.of(context).splashColor, + color: foregroundColor, ), Container( margin: const EdgeInsets.only(left: 8), width: 12, height: 80, - color: Theme.of(context).splashColor, + color: foregroundColor, ), ], ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 8839afa9..a42b2608 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -7,7 +7,7 @@ description: EasyRefresh example. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # Read more about versioning at semver.org. -version: 3.0.2+47 +version: 3.0.3+48 environment: sdk: ">=2.13.0 <3.0.0" @@ -31,7 +31,7 @@ dependencies: rive: ^0.9.0 qr_flutter: ^4.0.0 flutter_svg: ^1.1.1+1 - flutter_sticky_header: ^0.6.3 + flutter_sticky_header: ^0.6.4 easy_refresh: path: ../ easy_refresh_space: diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index 4f788487..d9fdd539 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,9 @@ #include "generated_plugin_registrant.h" -#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); + UrlLauncherPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherPlugin")); } diff --git a/lib/src/behavior/scroll_behavior.dart b/lib/src/behavior/scroll_behavior.dart index 150ace0c..01d24b5b 100644 --- a/lib/src/behavior/scroll_behavior.dart +++ b/lib/src/behavior/scroll_behavior.dart @@ -2,10 +2,10 @@ part of easy_refresh; /// Define [ScrollBehavior] in the scope of EasyRefresh. /// Add support for web and PC. -class _ERScrollBehavior extends ScrollBehavior { +class ERScrollBehavior extends ScrollBehavior { final ScrollPhysics? _physics; - const _ERScrollBehavior([this._physics]); + const ERScrollBehavior([this._physics]); @override ScrollPhysics getScrollPhysics(BuildContext context) { @@ -24,5 +24,6 @@ class _ERScrollBehavior extends ScrollBehavior { PointerDeviceKind.stylus, PointerDeviceKind.invertedStylus, PointerDeviceKind.mouse, + PointerDeviceKind.unknown, }; } diff --git a/lib/src/easy_refresh.dart b/lib/src/easy_refresh.dart index 34faa8b8..e02d80e5 100644 --- a/lib/src/easy_refresh.dart +++ b/lib/src/easy_refresh.dart @@ -556,12 +556,12 @@ class _EasyRefreshState extends State Widget child; if (widget.childBuilder != null) { child = ScrollConfiguration( - behavior: const _ERScrollBehavior(), + behavior: const ERScrollBehavior(), child: widget.childBuilder!(context, _physics), ); } else { child = ScrollConfiguration( - behavior: _ERScrollBehavior(_physics), + behavior: ERScrollBehavior(_physics), child: widget.child!, ); } diff --git a/lib/src/notifier/indicator_notifier.dart b/lib/src/notifier/indicator_notifier.dart index fc3ddc62..bfa4bee5 100644 --- a/lib/src/notifier/indicator_notifier.dart +++ b/lib/src/notifier/indicator_notifier.dart @@ -327,8 +327,8 @@ abstract class IndicatorNotifier extends ChangeNotifier { Simulation? createBallisticSimulation( ScrollMetrics position, double velocity); - /// Calculate distance from boundary. - double get boundaryOffset; + /// Calculate distance from edge. + double get edgeOffset; /// Update parameters. /// When the EasyRefresh parameters is updated. @@ -471,7 +471,7 @@ abstract class IndicatorNotifier extends ChangeNotifier { if (_mode == IndicatorMode.done || // Handling infinite scroll (infiniteOffset != null && - boundaryOffset < infiniteOffset! && + (!_position.isNestedOuter && edgeOffset < infiniteOffset!) && !bySimulation && !_infiniteExclude(position, value))) { // Update mode @@ -525,7 +525,8 @@ abstract class IndicatorNotifier extends ChangeNotifier { return; } // Infinite scroll - if (infiniteOffset != null && boundaryOffset < infiniteOffset!) { + if (infiniteOffset != null && + (!_position.isNestedOuter && edgeOffset < infiniteOffset!)) { if (_mode == IndicatorMode.done && _position.maxScrollExtent != _position.minScrollExtent) { // The state does not change until the end @@ -871,9 +872,9 @@ class HeaderNotifier extends IndicatorNotifier { return null; } - /// See [IndicatorNotifier.boundaryOffset]. + /// See [IndicatorNotifier.edgeOffset]. @override - double get boundaryOffset => _position.pixels; + double get edgeOffset => _position.pixels; @override bool _infiniteExclude(ScrollMetrics position, double value) { @@ -1018,9 +1019,9 @@ class FooterNotifier extends IndicatorNotifier { return null; } - /// See [IndicatorNotifier.boundaryOffset]. + /// See [IndicatorNotifier.edgeOffset]. @override - double get boundaryOffset => _position.maxScrollExtent - _position.pixels; + double get edgeOffset => _position.maxScrollExtent - _position.pixels; @override bool _infiniteExclude(ScrollMetrics position, double value) { diff --git a/lib/src/physics/scroll_physics.dart b/lib/src/physics/scroll_physics.dart index c63068ad..25fd109f 100644 --- a/lib/src/physics/scroll_physics.dart +++ b/lib/src/physics/scroll_physics.dart @@ -382,17 +382,15 @@ class _ERScrollPhysics extends BouncingScrollPhysics { return bounds; } - // Update indicator offset + /// Update indicator offset void _updateIndicatorOffset( ScrollMetrics position, double offset, double value) { - if (position.runtimeType.toString() == '_NestedScrollPosition') { - final debugLabel = (position as ScrollPosition).debugLabel; - if (debugLabel == 'outer' && - headerNotifier._offset > 0 && - value > position.minScrollExtent && - !headerNotifier.modeLocked) { - return; - } + // NestedScrollView special handling. + if (position.isNestedOuter && + headerNotifier._offset > 0 && + value > position.minScrollExtent && + !headerNotifier.modeLocked) { + return; } final hClamping = headerNotifier.clamping && headerNotifier.offset > 0; final fClamping = footerNotifier.clamping && footerNotifier.offset > 0; @@ -478,3 +476,10 @@ class _BallisticSimulationCreationState { return mode != newState.mode || offset != newState.offset; } } + +/// ScrollMetrics extension. +extension _ScrollMetricsExtension on ScrollMetrics { + // NestedScrollView outer. + bool get isNestedOuter => + this is ScrollPosition && (this as ScrollPosition).debugLabel == 'outer'; +} diff --git a/pubspec.yaml b/pubspec.yaml index 4c0252e4..5a8d8970 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: easy_refresh description: A flutter widget that provides pull-down refresh and pull-up load. -version: 3.0.2+2 +version: 3.0.3 homepage: https://xuelongqy.github.io/flutter_easy_refresh repository: https://github.com/xuelongqy/flutter_easy_refresh issue_tracker: https://github.com/xuelongqy/flutter_easy_refresh/issues