Last active
May 18, 2023 18:46
-
-
Save HansMuller/7e0a38380d059058b71d6e7b03b9cd6d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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