Skip to content

Instantly share code, notes, and snippets.

@marcoredz
Created October 21, 2022 20:53
Show Gist options
  • Save marcoredz/b2a37ff2cd11f81fa4ff7fb004fbb0ab to your computer and use it in GitHub Desktop.
Save marcoredz/b2a37ff2cd11f81fa4ff7fb004fbb0ab to your computer and use it in GitHub Desktop.
Sliver header that can be floating or pinned based on a parameter
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
class SliverFlexibleHeader extends SingleChildRenderObjectWidget {
const SliverFlexibleHeader({
Key key,
@required Widget child,
this.floating = false, // pinned as default
}) : super(key: key, child: child);
final bool floating;
@override
_RenderSliverFlexibleHeader createRenderObject(context) =>
_RenderSliverFlexibleHeader(floating: floating);
}
class _RenderSliverFlexibleHeader extends RenderSliverSingleBoxAdapter {
_RenderSliverFlexibleHeader({RenderBox child, @required this.floating})
: super(child: child);
final bool floating;
double _lastActualScrollOffset;
double _effectiveScrollOffset = 0;
double _childPosition;
@override
void performLayout() {
if (child == null) {
geometry = SliverGeometry.zero;
return;
}
child.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double maxExtent = childExtent;
final double paintExtent = maxExtent - (_effectiveScrollOffset ?? 0.0);
final double layoutExtent = maxExtent - constraints.scrollOffset;
if (_lastActualScrollOffset != null &&
(constraints.scrollOffset < _lastActualScrollOffset ||
_effectiveScrollOffset < maxExtent)) {
double delta = _lastActualScrollOffset - constraints.scrollOffset;
final bool allowFloatingExpansion =
constraints.userScrollDirection == ScrollDirection.forward;
if (allowFloatingExpansion) {
_effectiveScrollOffset = math.min(_effectiveScrollOffset, maxExtent);
} else {
delta = math.min(delta, 0);
}
_effectiveScrollOffset =
(_effectiveScrollOffset - delta).clamp(0.0, constraints.scrollOffset);
} else {
_effectiveScrollOffset = constraints.scrollOffset;
}
final paintedChildExtent = math.min(
childExtent,
constraints.remainingPaintExtent - constraints.overlap,
);
geometry = SliverGeometry(
paintExtent: floating
? paintExtent.clamp(0.0, constraints.remainingPaintExtent)
: paintedChildExtent,
maxPaintExtent: maxExtent,
paintOrigin:
floating ? math.min(constraints.overlap, 0) : constraints.overlap,
scrollExtent: maxExtent,
layoutExtent: floating
? math.min(
paintExtent.clamp(0.0, constraints.remainingPaintExtent),
layoutExtent.clamp(0.0, constraints.remainingPaintExtent),
)
: math.max(0.0, paintedChildExtent - constraints.scrollOffset),
hasVisualOverflow: floating ? true : paintedChildExtent < childExtent,
);
_childPosition = math.min(0, paintExtent - childExtent);
_lastActualScrollOffset = constraints.scrollOffset;
}
double get childExtent {
if (child == null) {
return 0;
}
assert(child.hasSize);
assert(constraints.axis != null);
switch (constraints.axis) {
case Axis.vertical:
return child.size.height;
case Axis.horizontal:
return child.size.width;
}
return null;
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry.visible) {
assert(constraints.axisDirection != null);
switch (applyGrowthDirectionToAxisDirection(
constraints.axisDirection,
constraints.growthDirection,
)) {
case AxisDirection.up:
offset += Offset(
0,
geometry.paintExtent - childMainAxisPosition(child) - childExtent,
);
break;
case AxisDirection.down:
offset += Offset(0, childMainAxisPosition(child));
break;
case AxisDirection.left:
offset += Offset(
geometry.paintExtent - childMainAxisPosition(child) - childExtent,
0,
);
break;
case AxisDirection.right:
offset += Offset(childMainAxisPosition(child), 0);
break;
}
context.paintChild(child, offset);
}
}
@override
double childMainAxisPosition(RenderBox child) {
assert(child == this.child);
return floating ? _childPosition : 0;
}
}
@AlaaEldeenYsr
Copy link

null safety support

import 'dart:math' as math;

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

class SliverFlexibleHeader extends SingleChildRenderObjectWidget {
  const SliverFlexibleHeader({
    Key? key,
    required Widget child,
    this.floating = false, // pinned as default
  }) : super(key: key, child: child);

  final bool floating;

  @override
  _RenderSliverFlexibleHeader createRenderObject(BuildContext context) =>
      _RenderSliverFlexibleHeader(floating: floating);
}

class _RenderSliverFlexibleHeader extends RenderSliverSingleBoxAdapter {
  _RenderSliverFlexibleHeader({RenderBox? child, required this.floating}) : super(child: child);

  final bool floating;

  double? _lastActualScrollOffset;
  double _effectiveScrollOffset = 0;
  double _childPosition = 0;

  @override
  void performLayout() {
    if (child == null) {
      geometry = SliverGeometry.zero;
      return;
    }
    child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
    final double maxExtent = childExtent;
    final double paintExtent = maxExtent - (_effectiveScrollOffset);
    final double layoutExtent = maxExtent - constraints.scrollOffset;

    if (_lastActualScrollOffset != null &&
        (constraints.scrollOffset < _lastActualScrollOffset! || _effectiveScrollOffset < maxExtent)) {
      double delta = _lastActualScrollOffset! - constraints.scrollOffset;
      final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward;
      if (allowFloatingExpansion) {
        _effectiveScrollOffset = math.min(_effectiveScrollOffset, maxExtent);
      } else {
        delta = math.min(delta, 0);
      }
      _effectiveScrollOffset = (_effectiveScrollOffset - delta).clamp(0.0, constraints.scrollOffset);
    } else {
      _effectiveScrollOffset = constraints.scrollOffset;
    }

    final paintedChildExtent = math.min(
      childExtent,
      constraints.remainingPaintExtent - constraints.overlap,
    );
    geometry = SliverGeometry(
      paintExtent: floating ? paintExtent.clamp(0.0, constraints.remainingPaintExtent) : paintedChildExtent,
      maxPaintExtent: maxExtent,
      paintOrigin: floating ? math.min(constraints.overlap, 0) : constraints.overlap,
      scrollExtent: maxExtent,
      layoutExtent: floating
          ? math.min(
              paintExtent.clamp(0.0, constraints.remainingPaintExtent),
              layoutExtent.clamp(0.0, constraints.remainingPaintExtent),
            )
          : math.max(0.0, paintedChildExtent - constraints.scrollOffset),
      hasVisualOverflow: floating ? true : paintedChildExtent < childExtent,
    );
    _childPosition = math.min(0, paintExtent - childExtent);
    _lastActualScrollOffset = constraints.scrollOffset;
  }

  double get childExtent {
    if (child == null) {
      return 0;
    }
    assert(child!.hasSize);
    switch (constraints.axis) {
      case Axis.vertical:
        return child!.size.height;
      case Axis.horizontal:
        return child!.size.width;
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null && geometry?.visible == true) {
      switch (applyGrowthDirectionToAxisDirection(
        constraints.axisDirection,
        constraints.growthDirection,
      )) {
        case AxisDirection.up:
          offset += Offset(
            0,
            geometry!.paintExtent - childMainAxisPosition(child!) - childExtent,
          );
          break;
        case AxisDirection.down:
          offset += Offset(0, childMainAxisPosition(child!));
          break;
        case AxisDirection.left:
          offset += Offset(
            geometry!.paintExtent - childMainAxisPosition(child!) - childExtent,
            0,
          );
          break;
        case AxisDirection.right:
          offset += Offset(childMainAxisPosition(child!), 0);
          break;
      }
      context.paintChild(child!, offset);
    }
  }

  @override
  double childMainAxisPosition(RenderBox child) {
    assert(child == this.child);
    return floating ? _childPosition : 0;
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment