Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a callback that handles the changes in the state of a sticky header such as "onPinned" #92

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
72 changes: 72 additions & 0 deletions example/lib/examples/activity_handler.dart
Original file line number Diff line number Diff line change
@@ -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<ActivityHandlerExample> createState() => _ActivityHandlerExampleState();
}

class _ActivityHandlerExampleState extends State<ActivityHandlerExample> {
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,
),
),
);
}
}
5 changes: 5 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:example/examples/activity_handler.dart';
import 'package:example/examples/nested.dart';
import 'package:flutter/material.dart';

Expand Down Expand Up @@ -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(),
Expand Down
32 changes: 32 additions & 0 deletions lib/src/rendering/sliver_sticky_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -16,13 +17,17 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {
bool overlapsContent: false,
bool sticky: true,
StickyHeaderController? controller,
this.activityHandler,
}) : _overlapsContent = overlapsContent,
_sticky = sticky,
_controller = controller {
this.header = header as RenderBox?;
this.child = child;
}

SliverStickyHeaderActivityHandler? activityHandler;
SliverStickyHeaderActivity? _lastReportedActivity;

SliverStickyHeaderState? _oldState;
double? _headerExtent;
late bool _isPinned;
Expand Down Expand Up @@ -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<
Expand Down Expand Up @@ -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}) {
Expand Down
32 changes: 31 additions & 1 deletion lib/src/widgets/sliver_sticky_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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]
Expand All @@ -171,6 +194,7 @@ class SliverStickyHeader extends RenderObjectWidget {
bool overlapsContent: false,
bool sticky = true,
StickyHeaderController? controller,
SliverStickyHeaderActivityHandler? activityHandler,
}) : this(
key: key,
header: ValueLayoutBuilder<SliverStickyHeaderState>(
Expand All @@ -181,6 +205,7 @@ class SliverStickyHeader extends RenderObjectWidget {
overlapsContent: overlapsContent,
sticky: sticky,
controller: controller,
activityHandler: activityHandler,
);

/// The header to display before the sliver.
Expand All @@ -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,
);
}

Expand All @@ -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;
}
}

Expand Down