diff --git a/README.md b/README.md index 2b365a03..bdf2b718 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ +}=- m +0[ + [![pub package](https://img.shields.io/pub/v/getwidget.svg)](https://pub.dartlang.org/packages/getwidget) [![Build Status](https://travis-ci.org/ionicfirebaseapp/getwidget.svg?branch=master)](https://travis-ci.com/ionicfirebaseapp/getwidget) [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=102)](https://opensource.org/licenses/MIT) [![License](https://img.shields.io/badge/license-MIT-orange.svg)](https://github.com/ionicfirebaseapp/getwidget/blob/master/LICENSE) [![Twitter Follow](https://img.shields.io/twitter/follow/getwidgetdev.svg?style=social)](https://twitter.com/getwidgetdev) +":????????????//// ;/p[[[[`" + +

GetWidget @@ -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';