Skip to content

Commit

Permalink
Merge pull request #3 from letsar/feature/2_percentage_observer
Browse files Browse the repository at this point in the history
Fix issue #2
  • Loading branch information
letsar authored Jun 24, 2018
2 parents ea3e6a9 + cf20e65 commit b7c4f5c
Show file tree
Hide file tree
Showing 6 changed files with 318 additions and 77 deletions.
56 changes: 34 additions & 22 deletions lib/src/rendering/sliver_sticky_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,27 @@ import 'dart:math' as math;

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_sticky_header/src/widgets/sliver_sticky_header_scroll_notifier.dart';
import 'package:flutter_sticky_header/src/rendering/sticky_header_constraints.dart';
import 'package:flutter_sticky_header/src/rendering/sticky_header_layout_builder.dart';

/// A sliver with a [RenderBox] as header and a [RenderSliver] as child.
///
/// The [header] stays pinned when it hits the start of the viewport until
/// the [child] scrolls off the viewport.
class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {
RenderSliverStickyHeader({
RenderBox header,
RenderObject header,
RenderSliver child,
overlapsContent: false,
this.sliverStickyHeaderScrollNotifier,
}) : assert(overlapsContent != null),
_overlapsContent = overlapsContent {
this.header = header;
this.child = child;
}

SliverStickyHeaderScrollNotifier sliverStickyHeaderScrollNotifier;
double _oldScrollPercentage;

double _headerExtent;

bool get overlapsContent => _overlapsContent;
bool _overlapsContent;
Expand Down Expand Up @@ -93,9 +95,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {
return result;
}

/// The dimension of the header in the main axis.
@protected
double get headerExtent {
double computeHeaderExtent() {
if (header == null) return 0.0;
assert(header.hasSize);
assert(constraints.axis != null);
Expand All @@ -108,7 +108,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {
return null;
}

double get headerLogicalExtent => overlapsContent ? 0.0 : headerExtent;
double get headerLogicalExtent => overlapsContent ? 0.0 : _headerExtent;

@override
void performLayout() {
Expand All @@ -123,9 +123,13 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {

if (header != null) {
header.layout(
constraints.asBoxConstraints(),
new StickyHeaderConstraints(
scrollPercentage: _oldScrollPercentage ?? 0.0,
boxConstraints: constraints.asBoxConstraints(),
),
parentUsesSize: true,
);
_headerExtent = computeHeaderExtent();
}

// Compute the header extent only one time.
Expand Down Expand Up @@ -215,27 +219,35 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {
0.0,
childScrollExtent -
constraints.scrollOffset -
(overlapsContent ? this.headerExtent : 0.0));

double scrollPercentage =
(headerPosition.abs() / this.headerExtent).clamp(0.0, 1.0);
if (sliverStickyHeaderScrollNotifier != null &&
sliverStickyHeaderScrollNotifier.scrollPercentage !=
scrollPercentage) {
sliverStickyHeaderScrollNotifier.scrollPercentage = scrollPercentage;
(overlapsContent ? _headerExtent : 0.0));

// second layout if scroll percentage changed and header is a RenderStickyHeaderLayoutBuilder.
if (header is RenderStickyHeaderLayoutBuilder) {
double scrollPercentage =
(headerPosition.abs() / _headerExtent).clamp(0.0, 1.0);
if (_oldScrollPercentage != scrollPercentage) {
_oldScrollPercentage = scrollPercentage;
header.layout(
new StickyHeaderConstraints(
scrollPercentage: _oldScrollPercentage,
boxConstraints: constraints.asBoxConstraints(),
),
parentUsesSize: true,
);
}
}

switch (axisDirection) {
case AxisDirection.up:
headerParentData.paintOffset = new Offset(
0.0, geometry.paintExtent - headerPosition - this.headerExtent);
0.0, geometry.paintExtent - headerPosition - _headerExtent);
break;
case AxisDirection.down:
headerParentData.paintOffset = new Offset(0.0, headerPosition);
break;
case AxisDirection.left:
headerParentData.paintOffset = new Offset(
geometry.paintExtent - headerPosition - this.headerExtent, 0.0);
geometry.paintExtent - headerPosition - _headerExtent, 0.0);
break;
case AxisDirection.right:
headerParentData.paintOffset = new Offset(headerPosition, 0.0);
Expand All @@ -248,7 +260,7 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {
bool hitTestChildren(HitTestResult result,
{@required double mainAxisPosition, @required double crossAxisPosition}) {
assert(geometry.hitTestExtent > 0.0);
if (header != null && mainAxisPosition <= headerExtent) {
if (header != null && mainAxisPosition <= _headerExtent) {
return hitTestBoxChild(result, header,
mainAxisPosition: mainAxisPosition,
crossAxisPosition: crossAxisPosition);
Expand All @@ -264,15 +276,15 @@ class RenderSliverStickyHeader extends RenderSliver with RenderSliverHelpers {
double childMainAxisPosition(RenderObject child) {
if (child == header) return -constraints.scrollOffset;
if (child == this.child)
return calculatePaintOffset(constraints, from: 0.0, to: headerExtent);
return calculatePaintOffset(constraints, from: 0.0, to: _headerExtent);
return null;
}

@override
double childScrollOffset(RenderObject child) {
assert(child.parent == this);
if (child == this.child) {
return headerExtent;
return _headerExtent;
} else {
return super.childScrollOffset(child);
}
Expand Down
43 changes: 43 additions & 0 deletions lib/src/rendering/sticky_header_constraints.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:flutter/rendering.dart';

/// Immutable layout constraints for sticky header
class StickyHeaderConstraints extends BoxConstraints {
StickyHeaderConstraints({
this.scrollPercentage,
BoxConstraints boxConstraints,
}) : assert(scrollPercentage != null),
assert(boxConstraints != null),
super(
minWidth: boxConstraints.minWidth,
maxWidth: boxConstraints.maxWidth,
minHeight: boxConstraints.minHeight,
maxHeight: boxConstraints.maxHeight,
);

final double scrollPercentage;

@override
bool get isNormalized =>
scrollPercentage >= 0.0 && scrollPercentage <= 1.0 && super.isNormalized;

@override
bool operator ==(dynamic other) {
assert(debugAssertIsValid());
if (identical(this, other)) return true;
if (other is! StickyHeaderConstraints) return false;
final StickyHeaderConstraints typedOther = other;
assert(typedOther.debugAssertIsValid());
return scrollPercentage == typedOther.scrollPercentage &&
minWidth == typedOther.minWidth &&
maxWidth == typedOther.maxWidth &&
minHeight == typedOther.minHeight &&
maxHeight == typedOther.maxHeight;
}

@override
int get hashCode {
assert(debugAssertIsValid());
return hashValues(
minWidth, maxWidth, minHeight, maxHeight, scrollPercentage);
}
}
81 changes: 81 additions & 0 deletions lib/src/rendering/sticky_header_layout_builder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_sticky_header/src/rendering/sticky_header_constraints.dart';

class RenderStickyHeaderLayoutBuilder extends RenderBox
with RenderObjectWithChildMixin<RenderBox> {
RenderStickyHeaderLayoutBuilder({
LayoutCallback<StickyHeaderConstraints> callback,
}) : _callback = callback;

LayoutCallback<StickyHeaderConstraints> get callback => _callback;
LayoutCallback<StickyHeaderConstraints> _callback;
set callback(LayoutCallback<StickyHeaderConstraints> value) {
if (value == _callback) return;
_callback = value;
markNeedsLayout();
}

// layout input
@override
StickyHeaderConstraints get constraints => super.constraints;

bool _debugThrowIfNotCheckingIntrinsics() {
assert(() {
if (!RenderObject.debugCheckingIntrinsics) {
throw new FlutterError(
'StickyHeaderLayoutBuilder does not support returning intrinsic dimensions.\n'
'Calculating the intrinsic dimensions would require running the layout '
'callback speculatively, which might mutate the live render object tree.');
}
return true;
}());
return true;
}

@override
double computeMinIntrinsicWidth(double height) {
assert(_debugThrowIfNotCheckingIntrinsics());
return 0.0;
}

@override
double computeMaxIntrinsicWidth(double height) {
assert(_debugThrowIfNotCheckingIntrinsics());
return 0.0;
}

@override
double computeMinIntrinsicHeight(double width) {
assert(_debugThrowIfNotCheckingIntrinsics());
return 0.0;
}

@override
double computeMaxIntrinsicHeight(double width) {
assert(_debugThrowIfNotCheckingIntrinsics());
return 0.0;
}

@override
void performLayout() {
assert(callback != null);
invokeLayoutCallback(callback);
if (child != null) {
child.layout(constraints, parentUsesSize: true);
size = constraints.constrain(child.size);
} else {
size = constraints.biggest;
}
}

@override
bool hitTestChildren(HitTestResult result, {Offset position}) {
return child?.hitTest(result, position: position) ?? false;
}

@override
void paint(PaintingContext context, Offset offset) {
if (child != null) context.paintChild(child, offset);
}
}
52 changes: 10 additions & 42 deletions lib/src/widgets/sliver_sticky_header.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_sticky_header/src/rendering/sliver_sticky_header.dart';
import 'package:flutter_sticky_header/src/widgets/sliver_sticky_header_scroll_notifier.dart';
import 'package:flutter_sticky_header/src/widgets/sticky_header_layout_builder.dart';

/// Signature used by [SliverStickyHeaderBuilder] to build the header
/// when the percentage of scroll of the header has changed.
Expand All @@ -23,7 +22,6 @@ class SliverStickyHeader extends RenderObjectWidget {
this.header,
this.sliver,
this.overlapsContent: false,
this.sliverStickyHeaderScrollNotifier,
}) : assert(overlapsContent != null),
super(key: key);

Expand All @@ -37,16 +35,10 @@ class SliverStickyHeader extends RenderObjectWidget {
/// instead of before.
final bool overlapsContent;

/// The controller used to listen to the header's scroll percentage changes.
///
/// Consider using the [SliverStickyHeaderBuilder] if you have to use this.
final SliverStickyHeaderScrollNotifier sliverStickyHeaderScrollNotifier;

@override
RenderSliverStickyHeader createRenderObject(BuildContext context) {
return new RenderSliverStickyHeader(
overlapsContent: overlapsContent,
sliverStickyHeaderScrollNotifier: sliverStickyHeaderScrollNotifier,
);
}

Expand All @@ -57,17 +49,17 @@ class SliverStickyHeader extends RenderObjectWidget {
@override
void updateRenderObject(
BuildContext context, RenderSliverStickyHeader renderObject) {
renderObject
..overlapsContent = overlapsContent
..sliverStickyHeaderScrollNotifier = sliverStickyHeaderScrollNotifier;
renderObject..overlapsContent = overlapsContent;
}
}

/// A widget that builds a [SliverStickyHeader] and calls a [SliverStickyHeaderWidgetBuilder] when
/// the header scroll percentage changes.
///
/// This is useful if you want to change the header layout when it starts to scroll off the viewport.
class SliverStickyHeaderBuilder extends StatefulWidget {
/// You cannot change the main axis extent of the header in this builder otherwise it could result
/// in strange behavior.
class SliverStickyHeaderBuilder extends StatelessWidget {
/// Creates a widget that builds the header of a [SliverStickyHeader]
/// each time its scroll percentage changes.
///
Expand All @@ -94,38 +86,14 @@ class SliverStickyHeaderBuilder extends StatefulWidget {
/// instead of before.
final bool overlapsContent;

@override
_SliverStickyHeaderBuilderState createState() =>
new _SliverStickyHeaderBuilderState();
}

class _SliverStickyHeaderBuilderState extends State<SliverStickyHeaderBuilder> {
double _scrollPercentage;
SliverStickyHeaderScrollNotifier _sliverStickyHeaderScrollNotifier;

@override
void initState() {
super.initState();
_sliverStickyHeaderScrollNotifier = new SliverStickyHeaderScrollNotifier();
_sliverStickyHeaderScrollNotifier.addListener(_scrollPercentageChanged);
}

void _scrollPercentageChanged() {
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
setState(() => _scrollPercentage =
_sliverStickyHeaderScrollNotifier.scrollPercentage);
});
}

@override
Widget build(BuildContext context) {
return new SliverStickyHeader(
overlapsContent: widget.overlapsContent,
sliverStickyHeaderScrollNotifier: _sliverStickyHeaderScrollNotifier,
sliver: widget.sliver,
header: new LayoutBuilder(
builder: (context, _) =>
widget.builder(context, _scrollPercentage ?? 0.0),
overlapsContent: overlapsContent,
sliver: sliver,
header: new StickyHeaderLayoutBuilder(
builder: (context, constraints) =>
builder(context, constraints.scrollPercentage),
),
);
}
Expand Down
13 changes: 0 additions & 13 deletions lib/src/widgets/sliver_sticky_header_scroll_notifier.dart

This file was deleted.

Loading

0 comments on commit b7c4f5c

Please sign in to comment.