diff --git a/example/lib/config/routes.dart b/example/lib/config/routes.dart index d503617..794324c 100644 --- a/example/lib/config/routes.dart +++ b/example/lib/config/routes.dart @@ -7,6 +7,7 @@ import 'package:example/page/sample/chat_page.dart'; import 'package:example/page/sample/listener_header_page.dart'; import 'package:example/page/sample/nested_scroll_view.dart'; import 'package:example/page/sample/page_view_page.dart'; +import 'package:example/page/sample/refresh_multiple_times_page.dart'; import 'package:example/page/sample/refresh_on_start_page.dart'; import 'package:example/page/sample/sample_page.dart'; import 'package:example/page/sample/secondary_page.dart'; @@ -36,6 +37,7 @@ class Routes { static const nestedScrollViewSample = '/sample/nested-scroll-view'; static const carouselSample = '/sample/carousel'; static const refreshOnStartSample = '/sample/refresh-on-start'; + static const refreshMutipleTimes = '/sample/refresh-mutiple-times'; static const listenerSample = '/sample/listener'; static const secondarySample = '/sample/secondary'; static const chatSample = '/sample/chat'; @@ -70,6 +72,7 @@ class Routes { name: nestedScrollViewSample, page: () => const NestedScrollViewPage()), GetPage(name: carouselSample, page: () => const CarouselPage()), GetPage(name: refreshOnStartSample, page: () => const RefreshOnStartPage()), + GetPage(name: refreshMutipleTimes, page: () => const RefreshMutipleTimesPage()), GetPage(name: listenerSample, page: () => const ListenerHeaderPage()), GetPage(name: secondarySample, page: () => const SecondaryPage()), GetPage(name: chatSample, page: () => const ChatPage()), diff --git a/example/lib/page/sample/refresh_multiple_times_page.dart b/example/lib/page/sample/refresh_multiple_times_page.dart new file mode 100644 index 0000000..cd7648c --- /dev/null +++ b/example/lib/page/sample/refresh_multiple_times_page.dart @@ -0,0 +1,211 @@ +import 'package:example/widget/skeleton_item.dart'; +import 'package:flutter/material.dart'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:get/get.dart'; + +class RefreshMutipleTimesPage extends StatefulWidget { + const RefreshMutipleTimesPage({Key? key}) : super(key: key); + + @override + State createState() => + _RefreshMutipleTimesPageState(); +} + +class _RefreshMutipleTimesPageState extends State { + late EasyRefreshController _controller; + int _count = 10; + final Axis _scrollDirection = Axis.vertical; + final _CIProperties _headerProperties = _CIProperties( + name: 'Header', + alignment: MainAxisAlignment.center, + infinite: false, + ); + final _CIProperties _footerProperties = _CIProperties( + name: 'Footer', + alignment: MainAxisAlignment.start, + infinite: true, + ); + var _selectFilterConditions = []; + + @override + void initState() { + super.initState(); + _controller = EasyRefreshController( + controlFinishRefresh: true, + controlFinishLoad: true, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final propertiesItems = [_headerProperties, _footerProperties]; + return Scaffold( + appBar: AppBar( + title: Text('Classic'.tr), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(50), + child: Container( + height: 50, + color: Colors.black12, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: _conditionButtons(), + )), + ), + ), + body: EasyRefresh( + clipBehavior: Clip.none, + controller: _controller, + header: ClassicHeader( + clamping: _headerProperties.clamping, + backgroundColor: _headerProperties.background + ? Theme.of(context).colorScheme.surfaceVariant + : null, + mainAxisAlignment: _headerProperties.alignment, + showMessage: _headerProperties.message, + showText: _headerProperties.text, + infiniteOffset: _headerProperties.infinite ? 70 : null, + triggerWhenReach: _headerProperties.immediately, + dragText: 'Pull to refresh'.tr, + armedText: 'Release ready'.tr, + readyText: 'Refreshing...'.tr, + processingText: 'Refreshing...'.tr, + processedText: 'Succeeded'.tr, + noMoreText: 'No more'.tr, + failedText: 'Failed'.tr, + messageText: 'Last updated at %T'.tr, + ), + footer: ClassicFooter( + clamping: _footerProperties.clamping, + backgroundColor: _footerProperties.background + ? Theme.of(context).colorScheme.surfaceVariant + : null, + mainAxisAlignment: _footerProperties.alignment, + showMessage: _footerProperties.message, + showText: _footerProperties.text, + infiniteOffset: _footerProperties.infinite ? 70 : null, + triggerWhenReach: _footerProperties.immediately, + dragText: 'Pull to load'.tr, + armedText: 'Release ready'.tr, + readyText: 'Loading...'.tr, + processingText: 'Loading...'.tr, + processedText: 'Succeeded'.tr, + noMoreText: 'No more'.tr, + failedText: 'Failed'.tr, + messageText: 'Last updated at %T'.tr, + ), + onRefresh: _headerProperties.disable + ? null + : () async { + _refreshData(); + }, + onLoad: _footerProperties.disable + ? null + : () async { + await Future.delayed(const Duration(seconds: 2)); + if (!mounted) { + return; + } + setState(() { + _count += 5; + }); + _controller.finishLoad(_count >= 20 + ? IndicatorResult.noMore + : IndicatorResult.success); + }, + child: ListView.builder( + clipBehavior: Clip.none, + scrollDirection: _scrollDirection, + padding: EdgeInsets.zero, + itemCount: _count, + itemBuilder: (ctx, index) { + return SkeletonItem( + direction: _scrollDirection, + ); + }, + ), + ), + ); + } + + /// refresh data + void _refreshData() async { + await Future.delayed(const Duration(seconds: 2)); + if (!mounted) { + return; + } + setState(() { + _count = 10; + }); + _controller.finishRefresh(); + _controller.resetFooter(); + } + + /// filter condition list + List _conditionButtons() { + List titles = ["筛选条件1", "筛选条件2", "筛选条件3", "筛选条件4"]; + return titles + .map( + (e) => Container( + padding: EdgeInsets.symmetric(horizontal: 3), + child: TextButton( + child: Text(e), + style: ButtonStyle( + minimumSize: MaterialStateProperty.resolveWith( + (states) { + return const Size(50, 30); + }, + ), + shape: MaterialStateProperty.resolveWith((states) { + return RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ); + }), + backgroundColor: MaterialStateProperty.resolveWith( + (states) { + return _selectFilterConditions.contains(e) ? Colors.orange : Colors.white; + }, + ), + ), + onPressed: () { + setState(() { + if (_selectFilterConditions.contains(e)) { + _selectFilterConditions.remove(e); + } else { + _selectFilterConditions.add(e); + } + _controller.callRefresh(); + _refreshData(); + }); + }, + ), + ), + ) + .toList(); + } +} + +/// Classic indicator properties. +class _CIProperties { + final String name; + bool disable = false; + bool clamping = false; + bool background = false; + MainAxisAlignment alignment; + bool message = true; + bool text = true; + bool infinite; + bool immediately = false; + + _CIProperties({ + required this.name, + required this.alignment, + required this.infinite, + }); +} diff --git a/example/lib/page/sample/sample_page.dart b/example/lib/page/sample/sample_page.dart index 31b1d86..420fc51 100644 --- a/example/lib/page/sample/sample_page.dart +++ b/example/lib/page/sample/sample_page.dart @@ -64,6 +64,14 @@ class _SamplePageState extends State { icon: Icons.refresh, onTap: () => Get.toNamed(Routes.refreshOnStartSample), ), + ListItem( + title: 'Refresh mutiple times'.tr, + subtitle: + 'Modify the filter conditions during the refresh process and refresh again' + .tr, + icon: Icons.refresh, + onTap: () => Get.toNamed(Routes.refreshMutipleTimes), + ), ListItem( title: 'Listener'.tr, subtitle: 'Use listener to respond anywhere'.tr, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 656e7f2..9bf227c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -24,8 +24,8 @@ dependencies: url_launcher: ^6.1.5 flutter_spinkit: ^5.1.0 flare_flutter: ^3.0.2 - extended_nested_scroll_view: - path: ../../extended_nested_scroll_view + extended_nested_scroll_view: 5.1.3 +# path: ../../extended_nested_scroll_view get: ^4.6.5 convert: ^3.0.2 carousel_slider: ^4.1.1 diff --git a/lib/src/notifier/indicator_notifier.dart b/lib/src/notifier/indicator_notifier.dart index fe955ef..1ae4094 100644 --- a/lib/src/notifier/indicator_notifier.dart +++ b/lib/src/notifier/indicator_notifier.dart @@ -665,7 +665,7 @@ abstract class IndicatorNotifier extends ChangeNotifier { /// Finish task and return the result. /// [result] Result of task completion. void _finishTask([IndicatorResult result = IndicatorResult.success]) { - if (!_waitTaskResult) { + if (!_waitTaskResult && _result != result && _mode != IndicatorMode.inactive) { _result = result; _setMode(IndicatorMode.processed); _processing = false; @@ -724,6 +724,9 @@ abstract class IndicatorNotifier extends ChangeNotifier { if (processedDuration == Duration.zero) { _ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((timeStamp) { _mode = IndicatorMode.done; + if (offset == 0) { + _mode = IndicatorMode.inactive; + } // Trigger [Scrollable] rollback if (oldMode == IndicatorMode.processing && !userOffsetNotifier.value) { @@ -733,6 +736,9 @@ abstract class IndicatorNotifier extends ChangeNotifier { } else { Future.delayed(processedDuration, () { _setMode(IndicatorMode.done); + if (offset == 0) { + _mode = IndicatorMode.inactive; + } // Trigger [Scrollable] rollback if (!userOffsetNotifier.value) { _resetBallistic();