|
| 1 | +import "dart:math" as math; |
| 2 | + |
| 3 | +import "package:flutter/foundation.dart"; |
| 4 | +import "package:flutter/material.dart"; |
| 5 | +import "package:flutter/rendering.dart"; |
| 6 | + |
| 7 | +// Based on the Flutter issue workaround discussed here: |
| 8 | +// https://github.com/flutter/flutter/issues/44557#issuecomment-664416718 |
| 9 | +// |
| 10 | +// Original implementation: |
| 11 | +// https://gist.github.com/tomaszpolanski/cf0edb7961d2304c2f293da9971cd4c9 |
| 12 | + |
| 13 | +class SliverStickyHeader extends RenderObjectWidget { |
| 14 | + final Widget child; |
| 15 | + |
| 16 | + const SliverStickyHeader({required this.child}); |
| 17 | + |
| 18 | + @override |
| 19 | + RenderSliverStickyHeader createRenderObject(BuildContext context) { |
| 20 | + return RenderSliverStickyHeader(); |
| 21 | + } |
| 22 | + |
| 23 | + @override |
| 24 | + SliverStickyHeaderElement createElement() => SliverStickyHeaderElement(this); |
| 25 | +} |
| 26 | + |
| 27 | +class SliverStickyHeaderElement extends RenderObjectElement { |
| 28 | + SliverStickyHeaderElement(super.widget); |
| 29 | + |
| 30 | + @override |
| 31 | + RenderSliverStickyHeader get renderObject => super.renderObject as RenderSliverStickyHeader; |
| 32 | + |
| 33 | + @override |
| 34 | + void mount(Element? parent, Object? newSlot) { |
| 35 | + super.mount(parent, newSlot); |
| 36 | + renderObject._element = this; |
| 37 | + } |
| 38 | + |
| 39 | + @override |
| 40 | + void unmount() { |
| 41 | + renderObject._element = null; |
| 42 | + super.unmount(); |
| 43 | + } |
| 44 | + |
| 45 | + @override |
| 46 | + void update(SliverStickyHeader newWidget) { |
| 47 | + final oldWidget = widget as SliverStickyHeader; |
| 48 | + super.update(newWidget); |
| 49 | + final newChild = newWidget.child; |
| 50 | + final oldChild = oldWidget.child; |
| 51 | + if (newChild != oldChild && (newChild.runtimeType != oldChild.runtimeType)) { |
| 52 | + renderObject.triggerRebuild(); |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + @override |
| 57 | + void performRebuild() { |
| 58 | + super.performRebuild(); |
| 59 | + renderObject.triggerRebuild(); |
| 60 | + } |
| 61 | + |
| 62 | + Element? child; |
| 63 | + |
| 64 | + void _build() { |
| 65 | + owner!.buildScope(this, () { |
| 66 | + final headerWidget = widget as SliverStickyHeader; |
| 67 | + child = updateChild(child, headerWidget.child, null); |
| 68 | + }); |
| 69 | + } |
| 70 | + |
| 71 | + @override |
| 72 | + void forgetChild(Element child) { |
| 73 | + assert(child == this.child, "forgetChild"); |
| 74 | + this.child = null; |
| 75 | + super.forgetChild(child); |
| 76 | + } |
| 77 | + |
| 78 | + @override |
| 79 | + void insertRenderObjectChild(covariant RenderBox child, Object? slot) { |
| 80 | + assert(renderObject.debugValidateChild(child), "insertRenderObjectChild"); |
| 81 | + renderObject.child = child; |
| 82 | + } |
| 83 | + |
| 84 | + @override |
| 85 | + void moveRenderObjectChild(covariant RenderObject child, Object? oldSlot, Object? newSlot) { |
| 86 | + assert(false, "moveRenderObjectChild"); |
| 87 | + } |
| 88 | + |
| 89 | + @override |
| 90 | + void removeRenderObjectChild(covariant RenderObject child, Object? slot) { |
| 91 | + renderObject.child = null; |
| 92 | + } |
| 93 | + |
| 94 | + @override |
| 95 | + void visitChildren(ElementVisitor visitor) { |
| 96 | + if (child != null) { |
| 97 | + visitor(child!); |
| 98 | + } |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +// --------------------- renderer --------------------------- // |
| 103 | + |
| 104 | +Rect? _trim( |
| 105 | + Rect? original, { |
| 106 | + double top = -double.infinity, |
| 107 | + double right = double.infinity, |
| 108 | + double bottom = double.infinity, |
| 109 | + double left = -double.infinity, |
| 110 | +}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom)); |
| 111 | + |
| 112 | +class RenderSliverStickyHeader extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers { |
| 113 | + double? _lastActualScrollOffset; |
| 114 | + late double? _effectiveScrollOffset; |
| 115 | + |
| 116 | + ScrollDirection? _lastStartedScrollDirection; |
| 117 | + |
| 118 | + double? _childPosition; |
| 119 | + |
| 120 | + SliverStickyHeaderElement? _element; |
| 121 | + |
| 122 | + RenderSliverStickyHeader({RenderBox? child}) { |
| 123 | + this.child = child; |
| 124 | + } |
| 125 | + |
| 126 | + @protected |
| 127 | + double get childExtent { |
| 128 | + if (child == null) { |
| 129 | + return 0; |
| 130 | + } |
| 131 | + assert(child!.hasSize, "childExtent"); |
| 132 | + switch (constraints.axis) { |
| 133 | + case Axis.vertical: |
| 134 | + return child!.size.height; |
| 135 | + case Axis.horizontal: |
| 136 | + return child!.size.width; |
| 137 | + } |
| 138 | + } |
| 139 | + |
| 140 | + var _needsUpdateChild = true; |
| 141 | + |
| 142 | + @override |
| 143 | + void markNeedsLayout() { |
| 144 | + _needsUpdateChild = true; |
| 145 | + super.markNeedsLayout(); |
| 146 | + } |
| 147 | + |
| 148 | + @protected |
| 149 | + void layoutChild(double scrollOffset, double maxExtent, {bool overlapsContent = false}) { |
| 150 | + if (_needsUpdateChild) { |
| 151 | + invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { |
| 152 | + assert(constraints == this.constraints, "invokeLayoutCallback"); |
| 153 | + updateChild(); |
| 154 | + }); |
| 155 | + _needsUpdateChild = false; |
| 156 | + } |
| 157 | + child?.layout(constraints.asBoxConstraints(), parentUsesSize: true); |
| 158 | + } |
| 159 | + |
| 160 | + @override |
| 161 | + bool hitTestChildren( |
| 162 | + SliverHitTestResult result, { |
| 163 | + required double mainAxisPosition, |
| 164 | + required double crossAxisPosition, |
| 165 | + }) { |
| 166 | + assert(geometry!.hitTestExtent > 0.0, "hitTestChildren"); |
| 167 | + if (child != null) { |
| 168 | + return hitTestBoxChild( |
| 169 | + BoxHitTestResult.wrap(result), |
| 170 | + child!, |
| 171 | + mainAxisPosition: mainAxisPosition, |
| 172 | + crossAxisPosition: crossAxisPosition, |
| 173 | + ); |
| 174 | + } |
| 175 | + return false; |
| 176 | + } |
| 177 | + |
| 178 | + @override |
| 179 | + void applyPaintTransform(RenderObject child, Matrix4 transform) { |
| 180 | + assert(child == this.child, "applyPaintTransform"); |
| 181 | + applyPaintTransformForBoxChild(child as RenderBox, transform); |
| 182 | + } |
| 183 | + |
| 184 | + @override |
| 185 | + void paint(PaintingContext context, Offset offset) { |
| 186 | + var paintedOffset = offset; |
| 187 | + if (child != null && geometry!.visible) { |
| 188 | + switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
| 189 | + case AxisDirection.up: |
| 190 | + paintedOffset += Offset(0, geometry!.paintExtent - childMainAxisPosition(child!) - childExtent); |
| 191 | + case AxisDirection.down: |
| 192 | + paintedOffset += Offset(0, childMainAxisPosition(child!)); |
| 193 | + case AxisDirection.left: |
| 194 | + paintedOffset += Offset(geometry!.paintExtent - childMainAxisPosition(child!) - childExtent, 0); |
| 195 | + case AxisDirection.right: |
| 196 | + paintedOffset += Offset(childMainAxisPosition(child!), 0); |
| 197 | + } |
| 198 | + context.paintChild(child!, paintedOffset); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + @override |
| 203 | + void describeSemanticsConfiguration(SemanticsConfiguration config) { |
| 204 | + super.describeSemanticsConfiguration(config); |
| 205 | + config.addTagForChildren(RenderViewport.excludeFromScrolling); |
| 206 | + } |
| 207 | + |
| 208 | + // pinned floating |
| 209 | + |
| 210 | + @protected |
| 211 | + double updateGeometry() { |
| 212 | + final double minExtent = childExtent; |
| 213 | + final double minAllowedExtent = constraints.remainingPaintExtent > minExtent |
| 214 | + ? minExtent |
| 215 | + : constraints.remainingPaintExtent; |
| 216 | + final double maxExtent = childExtent; |
| 217 | + final double paintExtent = maxExtent - _effectiveScrollOffset!; |
| 218 | + final double clampedPaintExtent = clampDouble(paintExtent, minAllowedExtent, constraints.remainingPaintExtent); |
| 219 | + final double layoutExtent = maxExtent - constraints.scrollOffset; |
| 220 | + geometry = SliverGeometry( |
| 221 | + scrollExtent: maxExtent, |
| 222 | + paintOrigin: math.min(constraints.overlap, 0), |
| 223 | + paintExtent: clampedPaintExtent, |
| 224 | + layoutExtent: clampDouble(layoutExtent, 0, clampedPaintExtent), |
| 225 | + maxPaintExtent: maxExtent, |
| 226 | + maxScrollObstructionExtent: minExtent, |
| 227 | + hasVisualOverflow: true, |
| 228 | + ); |
| 229 | + return 0; |
| 230 | + } |
| 231 | + |
| 232 | + @override |
| 233 | + void performLayout() { |
| 234 | + final SliverConstraints constraints = this.constraints; |
| 235 | + final double maxExtent = childExtent; |
| 236 | + if (_lastActualScrollOffset != null && |
| 237 | + ((constraints.scrollOffset < _lastActualScrollOffset!) || (_effectiveScrollOffset! < maxExtent))) { |
| 238 | + double delta = _lastActualScrollOffset! - constraints.scrollOffset; |
| 239 | + |
| 240 | + final bool allowFloatingExpansion = |
| 241 | + constraints.userScrollDirection == ScrollDirection.forward || |
| 242 | + (_lastStartedScrollDirection != null && _lastStartedScrollDirection == ScrollDirection.forward); |
| 243 | + if (allowFloatingExpansion) { |
| 244 | + if (_effectiveScrollOffset! > maxExtent) { |
| 245 | + _effectiveScrollOffset = maxExtent; |
| 246 | + } |
| 247 | + } else { |
| 248 | + if (delta > 0.0) { |
| 249 | + delta = 0.0; |
| 250 | + } |
| 251 | + } |
| 252 | + _effectiveScrollOffset = clampDouble(_effectiveScrollOffset! - delta, 0, constraints.scrollOffset); |
| 253 | + } else { |
| 254 | + _effectiveScrollOffset = constraints.scrollOffset; |
| 255 | + } |
| 256 | + final bool overlapsContent = _effectiveScrollOffset! < constraints.scrollOffset; |
| 257 | + |
| 258 | + layoutChild(_effectiveScrollOffset!, maxExtent, overlapsContent: overlapsContent); |
| 259 | + _childPosition = updateGeometry(); |
| 260 | + _lastActualScrollOffset = constraints.scrollOffset; |
| 261 | + } |
| 262 | + |
| 263 | + @override |
| 264 | + void showOnScreen({ |
| 265 | + RenderObject? descendant, |
| 266 | + Rect? rect, |
| 267 | + Duration duration = Duration.zero, |
| 268 | + Curve curve = Curves.ease, |
| 269 | + }) { |
| 270 | + assert(child != null || descendant == null, "showOnScreen"); |
| 271 | + |
| 272 | + final Rect? childBounds = descendant != null |
| 273 | + ? MatrixUtils.transformRect(descendant.getTransformTo(child), rect ?? descendant.paintBounds) |
| 274 | + : rect; |
| 275 | + |
| 276 | + double targetExtent; |
| 277 | + Rect? targetRect; |
| 278 | + switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) { |
| 279 | + case AxisDirection.up: |
| 280 | + targetExtent = childExtent - (childBounds?.top ?? 0); |
| 281 | + targetRect = _trim(childBounds, bottom: childExtent); |
| 282 | + case AxisDirection.right: |
| 283 | + targetExtent = childBounds?.right ?? childExtent; |
| 284 | + targetRect = _trim(childBounds, left: 0); |
| 285 | + case AxisDirection.down: |
| 286 | + targetExtent = childBounds?.bottom ?? childExtent; |
| 287 | + targetRect = _trim(childBounds, top: 0); |
| 288 | + case AxisDirection.left: |
| 289 | + targetExtent = childExtent - (childBounds?.left ?? 0); |
| 290 | + targetRect = _trim(childBounds, right: childExtent); |
| 291 | + } |
| 292 | + |
| 293 | + final double effectiveMaxExtent = math.max(childExtent, childExtent); |
| 294 | + |
| 295 | + targetExtent = clampDouble( |
| 296 | + clampDouble(targetExtent, double.negativeInfinity, double.infinity), |
| 297 | + childExtent, |
| 298 | + effectiveMaxExtent, |
| 299 | + ); |
| 300 | + |
| 301 | + super.showOnScreen( |
| 302 | + descendant: descendant == null ? this : child, |
| 303 | + rect: targetRect, |
| 304 | + duration: duration, |
| 305 | + curve: curve, |
| 306 | + ); |
| 307 | + } |
| 308 | + |
| 309 | + @override |
| 310 | + double childMainAxisPosition(RenderBox child) { |
| 311 | + assert(child == this.child, "childMainAxisPosition"); |
| 312 | + return _childPosition ?? 0.0; |
| 313 | + } |
| 314 | + |
| 315 | + void updateChild() { |
| 316 | + assert(_element != null, "updateChild"); |
| 317 | + _element!._build(); |
| 318 | + } |
| 319 | + |
| 320 | + @protected |
| 321 | + void triggerRebuild() { |
| 322 | + markNeedsLayout(); |
| 323 | + } |
| 324 | +} |
0 commit comments