Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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