diff --git a/README.md b/README.md
index 2b365a03..bdf2b718 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
+}=- m
+0[
+
[](https://pub.dartlang.org/packages/getwidget) [](https://travis-ci.com/ionicfirebaseapp/getwidget) [](https://opensource.org/licenses/MIT) [](https://github.com/ionicfirebaseapp/getwidget/blob/master/LICENSE) [](https://twitter.com/getwidgetdev)
+":????????????//// ;/p[[[[`"
+
+
@@ -28,6 +34,7 @@
+
## Quick start
Read the [Getting started page](https://docs.getwidget.dev)
diff --git a/lib/components/sticky_header/gf_sticky_header.dart b/lib/components/sticky_header/gf_sticky_header.dart
new file mode 100644
index 00000000..a18e2008
--- /dev/null
+++ b/lib/components/sticky_header/gf_sticky_header.dart
@@ -0,0 +1,63 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+import 'package:meta/meta.dart';
+import 'package:getwidget/getwidget.dart';
+
+/// Place this widget inside a [ListView], [GridView], [CustomScrollView], [SingleChildScrollView] or similar.
+
+class GFStickyHeader extends MultiChildRenderObjectWidget {
+ GFStickyHeader(
+ {Key key,
+ @required this.stickyContent,
+ @required this.content,
+ this.direction = Axis.vertical,
+ this.enableHeaderOverlap = false,
+ this.callback,
+ this.stickyContentPosition = GFPosition.start})
+ : assert(direction != null),
+ super(
+ key: key,
+ children: stickyContentPosition == GFPosition.start &&
+ direction == Axis.horizontal
+ ? [stickyContent, content]
+ : stickyContentPosition == GFPosition.start &&
+ direction == Axis.vertical
+ ? [content, stickyContent]
+ : [content, stickyContent]);
+
+ /// widget can be used to define [stickyContent].
+ final Widget stickyContent;
+
+ /// widget can be used to define [content].
+ final Widget content;
+
+ /// On state true, the [stickyContent] will overlap the [content].
+ /// Only works when direction is [Axis.vertical]. Default set to false.
+ final bool enableHeaderOverlap;
+
+ /// [GFPosition] allows to [stickyContentPosition] to stick at top in [Axis.vertical] and stick at start in [Axis.horizontal]
+ /// Defaults to [GFPosition.start]
+ final GFPosition stickyContentPosition;
+
+ /// Allows to add custom stickyHeader stuck offset value
+ final RenderGFStickyHeaderCallback callback;
+
+ /// [direction] allows children to align in vertical / horizontal way
+ /// Defaults to [Axis.vertical]
+ final Axis direction;
+
+ @override
+ RenderGFStickyHeader createRenderObject(BuildContext context) {
+ final scrollable = Scrollable.of(context);
+ assert(scrollable != null);
+ return RenderGFStickyHeader(
+ direction: direction,
+ scrollable: scrollable,
+ enableHeaderOverlap: enableHeaderOverlap,
+ callback: callback,
+ stickyContentPosition: stickyContentPosition,
+ );
+ }
+}
diff --git a/lib/components/sticky_header/gf_sticky_header_builder.dart b/lib/components/sticky_header/gf_sticky_header_builder.dart
new file mode 100644
index 00000000..1abb64e0
--- /dev/null
+++ b/lib/components/sticky_header/gf_sticky_header_builder.dart
@@ -0,0 +1,71 @@
+import 'package:flutter/material.dart';
+import 'package:getwidget/getwidget.dart';
+
+typedef StickyHeaderWidgetBuilder = Widget Function(
+ BuildContext context, double stuckValue);
+
+/// Place this widget inside a [ListView], [GridView], [CustomScrollView], [SingleChildScrollView] or similar.
+
+class GFStickyHeaderBuilder extends StatefulWidget {
+ /// Constructs a new [GFStickyHeaderBuilder] widget.
+ const GFStickyHeaderBuilder({
+ Key key,
+ @required this.stickyContentBuilder,
+ @required this.content,
+ this.direction = Axis.vertical,
+ this.enableHeaderOverlap = false,
+ this.callback,
+ this.stickyContentPosition = GFPosition.start,
+ }) : assert(direction != null),
+ super(key: key);
+
+ /// widget can be used to define [stickyContentBuilder].
+ final StickyHeaderWidgetBuilder stickyContentBuilder;
+
+ /// widget can be used to define [content].
+ final Widget content;
+
+ /// On state true, the [stickyContentBuilder] will overlap the [content].
+ /// Only works when direction is [Axis.vertical]. Default set to false.
+ final bool enableHeaderOverlap;
+
+ /// [GFPosition] allows to [stickyContentPosition] to stick at top in [Axis.vertical] and stick at start in [Axis.horizontal]
+ /// Defaults to [GFPosition.start]
+ final GFPosition stickyContentPosition;
+
+ /// Allows to add custom stickyHeader stuck offset value
+ final RenderGFStickyHeaderCallback callback;
+
+ /// [direction] allows children to align in vertical / horizontal way
+ /// Defaults to [Axis.vertical]
+ final Axis direction;
+
+ @override
+ _GFStickyHeaderBuilderState createState() => _GFStickyHeaderBuilderState();
+}
+
+class _GFStickyHeaderBuilderState extends State {
+ double _stuckValue;
+
+ @override
+ Widget build(BuildContext context) => GFStickyHeader(
+ enableHeaderOverlap: widget.enableHeaderOverlap,
+ direction: widget.direction,
+ stickyContentPosition: widget.stickyContentPosition,
+ stickyContent: LayoutBuilder(
+ builder: (context, _) =>
+ widget.stickyContentBuilder(context, _stuckValue ?? 0.0),
+ ),
+ content: widget.content,
+ callback: (double stuckValue) {
+ if (_stuckValue != stuckValue) {
+ _stuckValue = stuckValue;
+ WidgetsBinding.instance.endOfFrame.then((_) {
+ if (mounted) {
+ setState(() {});
+ }
+ });
+ }
+ },
+ );
+}
diff --git a/lib/components/sticky_header/render_gf_sticky_header.dart b/lib/components/sticky_header/render_gf_sticky_header.dart
new file mode 100644
index 00000000..1d550be0
--- /dev/null
+++ b/lib/components/sticky_header/render_gf_sticky_header.dart
@@ -0,0 +1,268 @@
+import 'dart:math' show min, max;
+import 'dart:math' as math;
+import 'dart:ui';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+import 'package:getwidget/getwidget.dart';
+
+typedef RenderGFStickyHeaderCallback = void Function(double stuckValue);
+
+class RenderGFStickyHeader extends RenderBox
+ with
+ ContainerRenderObjectMixin,
+ RenderBoxContainerDefaultsMixin,
+ DebugOverflowIndicatorMixin {
+ RenderGFStickyHeader({
+ List children,
+ Axis direction = Axis.horizontal,
+ bool enableHeaderOverlap = false,
+ @required ScrollableState scrollable,
+ RenderGFStickyHeaderCallback callback,
+ GFPosition stickyContentPosition,
+ }) : assert(direction != null),
+ assert(scrollable != null),
+ _scrollable = scrollable,
+ _direction = direction,
+ _callback = callback,
+ _stickyContentPosition = stickyContentPosition,
+ _enableHeaderOverlap = enableHeaderOverlap {
+ addAll(children);
+ }
+
+ RenderGFStickyHeaderCallback _callback;
+ final ScrollableState _scrollable;
+ final bool _enableHeaderOverlap;
+ final GFPosition _stickyContentPosition;
+
+
+ Axis get direction => _direction;
+ Axis _direction;
+ set direction(Axis value) {
+ assert(value != null);
+ if (_direction != value) {
+ _direction = value;
+ markNeedsLayout();
+ }
+ }
+
+ // ignore: avoid_setters_without_getters
+ set callback(RenderGFStickyHeaderCallback value) {
+ if (_callback == value) {
+ return;
+ }
+ _callback = value;
+ markNeedsLayout();
+ }
+
+ @override
+ void setupParentData(RenderBox child) {
+ if (child.parentData is! FlexParentData) {
+ child.parentData = FlexParentData();
+ }
+ }
+
+ int _getFlex(RenderBox child) {
+ final FlexParentData childParentData = child.parentData;
+ return childParentData.flex ?? 0;
+ }
+
+ double _getCrossSize(RenderBox child) {
+ switch (_direction) {
+ case Axis.horizontal:
+ return child.size.height;
+ case Axis.vertical:
+ return child.size.width;
+ }
+ // ignore: avoid_returning_null
+ return null;
+ }
+
+ double _getMainSize(RenderBox child) {
+ switch (_direction) {
+ case Axis.horizontal:
+ return child.size.width;
+ case Axis.vertical:
+ return child.size.height;
+ }
+ // ignore: avoid_returning_null
+ return null;
+ }
+
+ @override
+ void attach(PipelineOwner owner) {
+ super.attach(owner);
+ _scrollable.position.addListener(markNeedsLayout);
+ }
+
+ @override
+ void detach() {
+ _scrollable.position.removeListener(markNeedsLayout);
+ super.detach();
+ }
+
+ RenderBox get _stickyContentBody => _stickyContentPosition ==
+ GFPosition.start &&
+ _direction == Axis.horizontal
+ ? firstChild
+ : _stickyContentPosition == GFPosition.end && _direction == Axis.vertical
+ ? firstChild
+ : lastChild;
+ RenderBox get _contentBody => _stickyContentPosition == GFPosition.start &&
+ _direction == Axis.horizontal
+ ? lastChild
+ : _stickyContentPosition == GFPosition.end && _direction == Axis.vertical
+ ? lastChild
+ : firstChild;
+
+ double getHeaderTileStuckOffset() {
+ final scrollableContent = _scrollable.context.findRenderObject();
+ if (scrollableContent.attached) {
+ try {
+ return localToGlobal(Offset.zero, ancestor: scrollableContent).dy;
+ // ignore: avoid_catches_without_on_clauses
+ } catch (e) {
+ print(e);
+ }
+ }
+ return 0;
+ }
+
+ @override
+ void performLayout() {
+ assert(childCount == 2);
+ _stickyContentBody.layout(constraints.loosen(), parentUsesSize: true);
+ _contentBody.layout(constraints.loosen(), parentUsesSize: true);
+
+ final stickyContentBodyHeight = _stickyContentBody.size.height;
+ final contentBodyHeight = _contentBody.size.height;
+
+ final height = max(
+ constraints.minHeight,
+ _enableHeaderOverlap
+ ? contentBodyHeight
+ : stickyContentBodyHeight + contentBodyHeight);
+ final width = max(constraints.minWidth, contentBodyHeight);
+
+ size = Size(
+ constraints.constrainWidth(width), constraints.constrainHeight(height));
+
+ final double stickyContentBodyOffset = getHeaderTileStuckOffset();
+
+ assert(constraints != null);
+
+ double crossSize = 0;
+ double allottedSize = 0;
+ RenderBox child = firstChild;
+ // ignore: unused_local_variable
+ int totalChildren = 0;
+ while (child != null) {
+ // ignore: avoid_as
+ final FlexParentData childParentData = child.parentData as FlexParentData;
+ totalChildren++;
+ final int flex = _getFlex(child);
+ if (flex > 0) {
+ } else {
+ BoxConstraints innerConstraints;
+ switch (_direction) {
+ case Axis.horizontal:
+ innerConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
+ break;
+ case Axis.vertical:
+ innerConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
+ break;
+ }
+ child.layout(innerConstraints, parentUsesSize: true);
+ allottedSize += _getMainSize(child);
+ crossSize = math.max(crossSize, _getCrossSize(child));
+ }
+ assert(child.parentData == childParentData);
+ child = childParentData.nextSibling;
+ }
+
+ final double idealSize = allottedSize;
+ double actualSize;
+ switch (_direction) {
+ case Axis.horizontal:
+ size = constraints.constrain(Size(idealSize, crossSize));
+ actualSize = size.width;
+ crossSize = size.height;
+ break;
+ case Axis.vertical:
+ size = constraints.constrain(Size(crossSize, idealSize));
+ actualSize = size.height;
+ crossSize = size.width;
+ break;
+ }
+ const double startingSpace = 0;
+ const double betweenSpace = 0;
+ const bool flipMainAxis = !true;
+ double childMainPosition =
+ flipMainAxis ? actualSize - startingSpace : startingSpace;
+ child = _contentBody;
+ // ignore: invariant_booleans
+ while (child != null) {
+ // ignore: avoid_as
+ final FlexParentData childParentData = child.parentData as FlexParentData;
+
+ if (flipMainAxis) {
+ childMainPosition = _getMainSize(child);
+ }
+ switch (_direction) {
+ case Axis.horizontal:
+ final FlexParentData contentBodyParentData = _contentBody.parentData;
+ contentBodyParentData.offset =
+ _stickyContentPosition == GFPosition.start
+ ? Offset(_stickyContentBody.size.width, 0)
+ : const Offset(0, 0);
+ final FlexParentData stickyContentBodyParentData =
+ _stickyContentBody.parentData;
+ stickyContentBodyParentData.offset = Offset(
+ childMainPosition,
+ max(
+ min(-stickyContentBodyOffset,
+ height - stickyContentBodyHeight),
+ 0));
+ break;
+ case Axis.vertical:
+ final FlexParentData contentBodyParentData = _contentBody.parentData;
+ contentBodyParentData.offset =
+ Offset(0, _enableHeaderOverlap ? 0.0 : stickyContentBodyHeight);
+ final FlexParentData stickyContentBodyParentData =
+ _stickyContentBody.parentData;
+ stickyContentBodyParentData.offset = Offset(
+ 0,
+ max(
+ 0,
+ min(-stickyContentBodyOffset,
+ height - stickyContentBodyHeight)));
+ break;
+ }
+ if (_callback != null) {
+ final stuckValue = max(
+ min(stickyContentBodyHeight, stickyContentBodyOffset),
+ -stickyContentBodyHeight) /
+ stickyContentBodyHeight;
+ _callback(stuckValue);
+ }
+ if (flipMainAxis) {
+ childMainPosition -= betweenSpace;
+ } else {
+ childMainPosition += _getMainSize(child) + betweenSpace;
+ }
+ child = childParentData.nextSibling;
+ }
+ }
+
+ @override
+ bool hitTestChildren(BoxHitTestResult result, {Offset position}) =>
+ defaultHitTestChildren(result, position: position);
+
+ @override
+ bool get isRepaintBoundary => true;
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ defaultPaint(context, offset);
+ }
+}
diff --git a/lib/getwidget.dart b/lib/getwidget.dart
index 69e3ab12..c73500d2 100644
--- a/lib/getwidget.dart
+++ b/lib/getwidget.dart
@@ -32,6 +32,9 @@ export 'package:getwidget/components/rating/gf_rating.dart';
export 'package:getwidget/components/search_bar/gf_search_bar.dart';
export 'package:getwidget/components/shimmer/gf_shimmer.dart';
export 'package:getwidget/components/slidable/gf_slidable.dart';
+export 'package:getwidget/components/sticky_header/gf_sticky_header.dart';
+export 'package:getwidget/components/sticky_header/gf_sticky_header_builder.dart';
+export 'package:getwidget/components/sticky_header/render_gf_sticky_header.dart';
export 'package:getwidget/components/tabs/gf_segment_tabs.dart';
export 'package:getwidget/components/tabs/gf_tabbar.dart';
export 'package:getwidget/components/tabs/gf_tabbar_view.dart';