Created
August 17, 2023 17:58
-
-
Save HansMuller/1656ac30e13775c81f3bde2233e4ffe1 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
// ReszingHeaderSliver AppBar Parts example: simple version | |
import 'dart:math' as math; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
// A placeholder SliverList of 50 items. | |
class ItemList extends StatelessWidget { | |
const ItemList({ | |
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 SliverList( | |
delegate: SliverChildBuilderDelegate( | |
(BuildContext context, int index) { | |
return Card( | |
color: Color.lerp(startColor, endColor, index / itemCount)!, | |
child: ListTile( | |
textColor: Colors.white, | |
title: Text('Item $index'), | |
), | |
); | |
}, | |
childCount: itemCount, | |
), | |
); | |
} | |
} | |
// This trivial wrapper is just to avoid the lint warning about public classes that | |
// depend on private types, i.e. _ResizingHeaderSliver depends on _Slot. | |
class ResizingHeaderSliver extends StatelessWidget { | |
const ResizingHeaderSliver({ | |
super.key, | |
this.minExtentPrototype, | |
this.maxExtentPrototype, | |
this.child, | |
}); | |
final Widget? minExtentPrototype; | |
final Widget? maxExtentPrototype; | |
final Widget? child; | |
@override | |
Widget build(BuildContext context) { | |
return _ResizingHeaderSliver( | |
minExtentPrototype: minExtentPrototype, | |
maxExtentPrototype: maxExtentPrototype, | |
child: child, | |
); | |
} | |
} | |
enum _Slot { | |
minExtent, | |
maxExtent, | |
child, | |
} | |
class _ResizingHeaderSliver extends SlottedMultiChildRenderObjectWidget<_Slot, RenderBox> { | |
const _ResizingHeaderSliver({ | |
this.minExtentPrototype, | |
this.maxExtentPrototype, | |
this.child, | |
}); | |
final Widget? minExtentPrototype; | |
final Widget? maxExtentPrototype; | |
final Widget? child; | |
@override | |
Iterable<_Slot> get slots => _Slot.values; | |
@override | |
Widget? childForSlot(_Slot slot) { | |
return switch (slot) { | |
_Slot.minExtent => minExtentPrototype, | |
_Slot.maxExtent => maxExtentPrototype, | |
_Slot.child => child, | |
}; | |
} | |
@override | |
_RenderResizingHeaderSliver createRenderObject(BuildContext context) { | |
return _RenderResizingHeaderSliver(); | |
} | |
} | |
class _RenderResizingHeaderSliver extends RenderSliver with SlottedContainerRenderObjectMixin<_Slot, RenderBox>, RenderSliverHelpers { | |
_RenderResizingHeaderSliver(); | |
RenderBox? get minExtentPrototype => childForSlot(_Slot.minExtent); | |
RenderBox? get maxExtentPrototype => childForSlot(_Slot.maxExtent); | |
RenderBox? get child => childForSlot(_Slot.child); | |
@override | |
Iterable<RenderBox> get children { | |
return <RenderBox>[ | |
if (minExtentPrototype != null) minExtentPrototype!, | |
if (maxExtentPrototype != null) maxExtentPrototype!, | |
if (child != null) child!, | |
]; | |
} | |
double boxExtent(RenderBox? box) { | |
if (box == null) { | |
return 0.0; | |
} | |
assert(box.hasSize); | |
return switch (constraints.axis) { | |
Axis.vertical => box.size.height, | |
Axis.horizontal => box.size.width, | |
}; | |
} | |
double get childExtent => boxExtent(child); | |
@override | |
void setupParentData(RenderObject child) { | |
if (child.parentData is! SliverPhysicalParentData) { | |
child.parentData = SliverPhysicalParentData(); | |
} | |
} | |
@protected | |
void setChildParentData(RenderObject child, SliverConstraints constraints, SliverGeometry geometry) { | |
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; | |
final AxisDirection direction = applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection); | |
childParentData.paintOffset = switch (direction) { | |
AxisDirection.up => Offset(0.0, -(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset))), | |
AxisDirection.right => Offset(-constraints.scrollOffset, 0.0), | |
AxisDirection.down => Offset(0.0, -constraints.scrollOffset), | |
AxisDirection.left => Offset(-(geometry.scrollExtent - (geometry.paintExtent + constraints.scrollOffset)), 0.0), | |
}; | |
} | |
@override | |
double childMainAxisPosition(covariant RenderObject child) => 0; | |
@override | |
void performLayout() { | |
final SliverConstraints constraints = this.constraints; | |
final BoxConstraints prototypeBoxConstraints = constraints.asBoxConstraints(); | |
double minExtent = 0; | |
if (minExtentPrototype != null) { | |
minExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true); | |
minExtent = boxExtent(minExtentPrototype); | |
} | |
double maxExtent = double.infinity; | |
if (maxExtentPrototype != null) { | |
maxExtentPrototype!.layout(prototypeBoxConstraints, parentUsesSize: true); | |
maxExtent = boxExtent(maxExtentPrototype); | |
} | |
final double scrollOffset = constraints.scrollOffset; | |
final double shrinkOffset = math.min(scrollOffset, maxExtent); | |
final BoxConstraints boxConstraints = constraints.asBoxConstraints( | |
minExtent: minExtent, | |
maxExtent: math.max(minExtent, maxExtent - shrinkOffset), | |
); | |
child?.layout(boxConstraints, parentUsesSize: true); | |
final double remainingPaintExtent = constraints.remainingPaintExtent; | |
final double layoutExtent = math.min(childExtent, maxExtent - scrollOffset); | |
geometry = SliverGeometry( | |
scrollExtent: maxExtent, | |
paintOrigin: constraints.overlap, | |
paintExtent: math.min(childExtent, remainingPaintExtent), | |
layoutExtent: clampDouble(layoutExtent, 0, remainingPaintExtent), | |
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 applyPaintTransform(RenderObject child, Matrix4 transform) { | |
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData; | |
childParentData.applyPaintTransform(transform); | |
} | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
if (child != null && geometry!.visible) { | |
final SliverPhysicalParentData childParentData = child!.parentData! as SliverPhysicalParentData; | |
context.paintChild(child!, offset + childParentData.paintOffset); | |
} | |
} | |
@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; | |
} | |
} | |
class AppBarParts extends StatelessWidget { | |
const AppBarParts({ super.key }); | |
@override | |
Widget build(BuildContext context) { | |
final ThemeData theme = Theme.of(context); | |
final ColorScheme colorScheme = theme.colorScheme; | |
final Widget header = Container( | |
color: colorScheme.background, | |
padding: const EdgeInsets.symmetric(horizontal: 4), | |
child: Material( | |
color: colorScheme.primary, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(8), | |
side: BorderSide( | |
width: 7, | |
color: colorScheme.outline, | |
), | |
), | |
child: Container( | |
alignment: Alignment.center, | |
padding: const EdgeInsets.symmetric(vertical: 8), | |
child: Text( | |
'ResizingHeaderSliver', | |
style: theme.textTheme.headlineMedium!.copyWith( | |
color: colorScheme.onPrimary, | |
), | |
), | |
), | |
), | |
); | |
return Scaffold( | |
body: SafeArea( | |
child: Padding( | |
padding: const EdgeInsets.all(4), | |
child: CustomScrollView( | |
slivers: <Widget>[ | |
ResizingHeaderSliver( | |
minExtentPrototype: header, | |
maxExtentPrototype: Padding( | |
padding: const EdgeInsets.symmetric(vertical: 100), | |
child: header, | |
), | |
child: header, | |
), | |
ItemList( | |
startColor: colorScheme.primary, | |
endColor: colorScheme.secondary, | |
), | |
], | |
), | |
), | |
), | |
); | |
} | |
} | |
class AppBarPartsApp extends StatelessWidget { | |
const AppBarPartsApp({ super.key }); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData(useMaterial3: true), | |
home: const AppBarParts(), | |
); | |
} | |
} | |
void main() { | |
runApp(const AppBarPartsApp()); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment