Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Last active May 18, 2023 18:46
Show Gist options
  • Save HansMuller/7e0a38380d059058b71d6e7b03b9cd6d to your computer and use it in GitHub Desktop.
Save HansMuller/7e0a38380d059058b71d6e7b03b9cd6d to your computer and use it in GitHub Desktop.
// https://gist.github.com/HansMuller/7e0a38380d059058b71d6e7b03b9cd6d
/*
PinnedSliver is a distillation of SliverPersistentHeader
that just supports the pinned:true configuration and whose size is
the size of the child.
The child widget is built with a callback rather than a sliver
delegate subclass. The callback is passed the parent BuildContext
and an overlapsContent flag. The flag is true if content has
scrolled under the header.
PinnedSliver(
builder: (BuildContext context, bool overlapsContent) {
return TitleBar(
topPadding: MediaQuery.paddingOf(context).top,
overlapsContent: overlapsContent,
);
},
)
In the implementation that follows there is a PinnedSliver widget,
renderer, and element class. The renderer and element point at
each other. Most of the action is in _RenderPinnedSliver. Having
boiled this out fo SliverPersistentHeader I can say that
understand most of it, but not everything.
The demo code follows _PinnedSliverElement. It fades in the PinnedSliver's
TitleBar child's title when overlapsContent becomes true.
*/
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
typedef PinnedSliverChildBuilder = Widget Function(BuildContext context, bool overlapsContent);
class PinnedSliver extends RenderObjectWidget {
const PinnedSliver ({
required this.builder,
});
final PinnedSliverChildBuilder builder;
@override
RenderObjectElement createElement() => _PinnedSliverElement(this);
@override
RenderObject createRenderObject(BuildContext context) => _RenderPinnedSliver();
}
class _RenderPinnedSliver extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
_RenderPinnedSliver({ RenderBox? child }) {
this.child = child;
}
_PinnedSliverElement? _element;
double get childExtent {
if (child == null) {
return 0.0;
}
assert(child!.hasSize);
return switch (constraints.axis) {
Axis.vertical => child!.size.height,
Axis.horizontal => child!.size.width,
};
}
bool _needsUpdateChild = true;
bool _lastOverlapsContent = false;
@override
void markNeedsLayout() {
_needsUpdateChild = true;
super.markNeedsLayout();
}
@override
bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) {
assert(geometry!.hitTestExtent > 0.0);
if (child != null) {
return hitTestBoxChild(BoxHitTestResult.wrap(result), child!, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
}
return false;
}
@override
double childMainAxisPosition(covariant RenderObject child) => 0;
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child == this.child);
applyPaintTransformForBoxChild(child as RenderBox, transform);
}
@override
void paint(PaintingContext context, Offset offset) {
if (child != null && geometry!.visible) {
context.paintChild(child!, offset);
}
}
@override
void performLayout() {
final SliverConstraints constraints = this.constraints;
final bool overlapsContent = constraints.scrollOffset > 0.0;
if (_needsUpdateChild ||_lastOverlapsContent != overlapsContent) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
_element!._build(overlapsContent);
});
_lastOverlapsContent = overlapsContent;
_needsUpdateChild = false;
}
child?.layout(constraints.asBoxConstraints(), parentUsesSize: true);
final double layoutExtent = clampDouble(childExtent - constraints.scrollOffset, 0, constraints.remainingPaintExtent);
final double paintExtent = math.min(childExtent, constraints.remainingPaintExtent - constraints.overlap);
geometry = SliverGeometry(
scrollExtent: childExtent,
paintOrigin: constraints.overlap,
paintExtent: paintExtent,
layoutExtent: layoutExtent,
maxPaintExtent: childExtent,
maxScrollObstructionExtent: childExtent,
cacheExtent: calculateCacheOffset(constraints, from: 0.0, to: childExtent),
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
}
@override
void showOnScreen({
RenderObject? descendant,
Rect? rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
final Rect? localBounds = descendant != null
? MatrixUtils.transformRect(descendant.getTransformTo(this), rect ?? descendant.paintBounds)
: rect;
Rect? trim({
double top = -double.infinity,
double right = double.infinity,
double bottom = double.infinity,
double left = -double.infinity,
}) {
return localBounds?.intersect(Rect.fromLTRB(left, top, right, bottom));
}
super.showOnScreen(
descendant: this,
duration: duration,
curve: curve,
rect: switch(applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
AxisDirection.up => trim(bottom: childExtent),
AxisDirection.right => trim(left: 0),
AxisDirection.down => trim(top: 0),
AxisDirection.left => trim(right: childExtent),
}
);
}
}
class _PinnedSliverElement extends RenderObjectElement {
_PinnedSliverElement(PinnedSliver super.widget);
@override
_RenderPinnedSliver get renderObject => super.renderObject as _RenderPinnedSliver;
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
renderObject._element = this;
}
@override
void unmount() {
renderObject._element = null;
super.unmount();
}
@override
void update(PinnedSliver newWidget) {
final PinnedSliver oldWidget = widget as PinnedSliver;
super.update(newWidget);
if (newWidget.builder != oldWidget.builder) {
renderObject.markNeedsLayout();
}
}
@override
void performRebuild() {
super.performRebuild();
renderObject.markNeedsLayout();
}
Element? child;
void _build(bool overlapsContent) {
owner!.buildScope(this, () {
final PinnedSliver sliver = widget as PinnedSliver;
child = updateChild(child, sliver.builder(this, overlapsContent), null);
});
}
@override
void forgetChild(Element child) {
assert(child == this.child);
this.child = null;
super.forgetChild(child);
}
@override
void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
assert(renderObject.debugValidateChild(child));
renderObject.child = child;
}
@override
void moveRenderObjectChild(covariant RenderObject child, Object? oldSlot, Object? newSlot) {
assert(false);
}
@override
void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
renderObject.child = null;
}
@override
void visitChildren(ElementVisitor visitor) {
if (child != null) {
visitor(child!);
}
}
}
/// --- Demo Follows ---
class ItemListView extends StatelessWidget {
const ItemListView({
super.key,
required this.startColor,
required this.endColor,
this.itemCount = 50,
});
final Color startColor;
final Color endColor;
final int itemCount;
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(
color: Color.lerp(startColor, endColor, index / itemCount)!,
child: ListTile(
textColor: Colors.white,
title: Text('Page $index'),
),
);
},
childCount: itemCount,
),
),
);
}
}
class TitleBar extends StatelessWidget {
const TitleBar({
required this.topPadding,
required this.overlapsContent,
});
final double topPadding;
final bool overlapsContent;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
height: 200,
padding: EdgeInsets.only(top: topPadding),
alignment: Alignment.center,
child: AnimatedOpacity(
opacity: overlapsContent ? 1 : 0,
duration: const Duration(milliseconds: 1000),
child: const Text('Settings', style: TextStyle(fontSize: 24)),
),
);
}
}
class PinnedSliverDemo extends StatefulWidget {
const PinnedSliverDemo({ super.key });
@override
State<PinnedSliverDemo> createState() => _PinnedSliverDemoState();
}
class _PinnedSliverDemoState extends State<PinnedSliverDemo> with SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
const ItemListView(
startColor: Colors.blue,
endColor: Colors.red,
itemCount: 3,
),
PinnedSliver(
builder: (BuildContext context, bool overlapsContent) {
return TitleBar(
topPadding: MediaQuery.paddingOf(context).top,
overlapsContent: overlapsContent,
);
},
),
const ItemListView(
startColor: Colors.yellow,
endColor: Colors.green,
),
],
),
);
}
}
class PinnedSliverDemoApp extends StatelessWidget {
const PinnedSliverDemoApp({ super.key });
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
),
home: const PinnedSliverDemo(),
);
}
}
void main() {
runApp(const PinnedSliverDemoApp());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment