diff --git a/example/lib/examples/activity_handler.dart b/example/lib/examples/activity_handler.dart new file mode 100644 index 0000000..76159bf --- /dev/null +++ b/example/lib/examples/activity_handler.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; + +import '../common.dart'; + +class ActivityHandlerExample extends StatefulWidget { + const ActivityHandlerExample({ + Key? key, + }) : super(key: key); + + @override + State createState() => _ActivityHandlerExampleState(); +} + +class _ActivityHandlerExampleState extends State { + int pinnedHeaderIndex = 0; + + @override + Widget build(BuildContext context) { + return AppScaffold( + reverse: false, + title: 'Header #$pinnedHeaderIndex is pinned', + slivers: [ + _StickyHeaderList(index: 0, onHeaderPinned: onHeaderPinned), + _StickyHeaderList(index: 1, onHeaderPinned: onHeaderPinned), + _StickyHeaderList(index: 2, onHeaderPinned: onHeaderPinned), + _StickyHeaderList(index: 3, onHeaderPinned: onHeaderPinned), + ], + ); + } + + void onHeaderPinned(int index) { + setState(() { + pinnedHeaderIndex = index; + }); + } +} + +class _StickyHeaderList extends StatelessWidget { + const _StickyHeaderList({ + Key? key, + this.index, + required this.onHeaderPinned, + }) : super(key: key); + + final int? index; + final void Function(int index) onHeaderPinned; + + @override + Widget build(BuildContext context) { + return SliverStickyHeader( + activityHandler: (activity) { + debugPrint("[Header#$index] $activity"); + if (activity == SliverStickyHeaderActivity.pinned) { + onHeaderPinned(index!); + } + }, + header: Header(index: index), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, i) => ListTile( + leading: CircleAvatar( + child: Text('$index'), + ), + title: Text('List tile #$i'), + ), + childCount: 6, + ), + ), + ); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 339dff2..0dc0f86 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:example/examples/activity_handler.dart'; import 'package:example/examples/nested.dart'; import 'package:flutter/material.dart'; @@ -65,6 +66,10 @@ class _Home extends StatelessWidget { text: 'Reverse List Example', builder: (_) => const ReverseExample(), ), + _Item( + text: 'Activity Handler Example', + builder: (_) => const ActivityHandlerExample(), + ), _Item( text: 'Mixing other slivers', builder: (_) => const MixSliversExample(), diff --git a/lib/src/rendering/sliver_sticky_header.dart b/lib/src/rendering/sliver_sticky_header.dart index 9a4503f..5700eb1 100644 --- a/lib/src/rendering/sliver_sticky_header.dart +++ b/lib/src/rendering/sliver_sticky_header.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:value_layout_builder/value_layout_builder.dart'; @@ -16,6 +17,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { bool overlapsContent: false, bool sticky: true, StickyHeaderController? controller, + this.activityHandler, }) : _overlapsContent = overlapsContent, _sticky = sticky, _controller = controller { @@ -23,6 +25,9 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { this.child = child; } + SliverStickyHeaderActivityHandler? activityHandler; + SliverStickyHeaderActivity? _lastReportedActivity; + SliverStickyHeaderState? _oldState; double? _headerExtent; late bool _isPinned; @@ -261,6 +266,9 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { controller?.stickyHeaderScrollOffset = constraints.precedingScrollExtent; } + + _updateActivity(headerScrollRatio); + // second layout if scroll percentage changed and header is a // RenderStickyHeaderLayoutBuilder. if (header is RenderConstrainedLayoutBuilder< @@ -300,6 +308,30 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers { } } + void _updateActivity(double headerScrollRatio) { + final SliverStickyHeaderActivity activity; + if (!_isPinned) { + activity = SliverStickyHeaderActivity.unpinned; + } else if (headerScrollRatio >= 1.0) { + activity = SliverStickyHeaderActivity.pushed; + } else if (headerScrollRatio > 0.0) { + activity = SliverStickyHeaderActivity.settling; + } else { + activity = SliverStickyHeaderActivity.pinned; + } + + if (activityHandler != null && + _lastReportedActivity != null && + activity != _lastReportedActivity) { + WidgetsBinding.instance.scheduleTask( + () => activityHandler?.call(activity), + Priority.touch, + ); + } + + _lastReportedActivity = activity; + } + @override bool hitTestChildren(SliverHitTestResult result, {required double mainAxisPosition, required double crossAxisPosition}) { diff --git a/lib/src/widgets/sliver_sticky_header.dart b/lib/src/widgets/sliver_sticky_header.dart index 48d0657..7dcda8b 100644 --- a/lib/src/widgets/sliver_sticky_header.dart +++ b/lib/src/widgets/sliver_sticky_header.dart @@ -134,6 +134,28 @@ class SliverStickyHeaderState { } } +/// A callback that handles a [SliverStickyHeaderActivity]. +typedef SliverStickyHeaderActivityHandler = void Function( + SliverStickyHeaderActivity activity); + +/// An event that is dispatched when a sticky header changes its position meaningfully. +enum SliverStickyHeaderActivity { + /// Dispatched when the [settling] sticky header is completely pushed + /// out of the viewport by the subsequent header. + pushed, + + /// Dispatched when the [pinned] sticky header is unpinned. + unpinned, + + /// Dispatched when the sticky header is pinned. + pinned, + + /// Dispatched when the [pushed] sticky header begins to push off the + /// currently pinned header, or the [pinned] sticky header begins to + /// be pushed off by the subsequent header. + settling, +} + /// A sliver that displays a header before its sliver. /// The header scrolls off the viewport only when the sliver does. /// @@ -155,6 +177,7 @@ class SliverStickyHeader extends RenderObjectWidget { this.overlapsContent: false, this.sticky = true, this.controller, + this.activityHandler, }) : super(key: key); /// Creates a widget that builds the header of a [SliverStickyHeader] @@ -171,6 +194,7 @@ class SliverStickyHeader extends RenderObjectWidget { bool overlapsContent: false, bool sticky = true, StickyHeaderController? controller, + SliverStickyHeaderActivityHandler? activityHandler, }) : this( key: key, header: ValueLayoutBuilder( @@ -181,6 +205,7 @@ class SliverStickyHeader extends RenderObjectWidget { overlapsContent: overlapsContent, sticky: sticky, controller: controller, + activityHandler: activityHandler, ); /// The header to display before the sliver. @@ -203,12 +228,16 @@ class SliverStickyHeader extends RenderObjectWidget { /// will be used. final StickyHeaderController? controller; + /// A callback invoked when a [SliverStickyHeaderActivity] is dispatched. + final SliverStickyHeaderActivityHandler? activityHandler; + @override RenderSliverStickyHeader createRenderObject(BuildContext context) { return RenderSliverStickyHeader( overlapsContent: overlapsContent, sticky: sticky, controller: controller ?? DefaultStickyHeaderController.of(context), + activityHandler: activityHandler, ); } @@ -224,7 +253,8 @@ class SliverStickyHeader extends RenderObjectWidget { renderObject ..overlapsContent = overlapsContent ..sticky = sticky - ..controller = controller ?? DefaultStickyHeaderController.of(context); + ..controller = controller ?? DefaultStickyHeaderController.of(context) + ..activityHandler = activityHandler; } }