Created
August 21, 2023 22:12
-
-
Save HansMuller/fd7b154108a142a0ea2ef1be24d09e94 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: contacts version with autoscroll | |
// New coordinator API; coordinated slivers report layout info directly. | |
import 'dart:math' as math; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/scheduler.dart'; | |
class _ContactBarButton extends StatelessWidget { | |
const _ContactBarButton(this.icon, this.label); | |
final String label; | |
final IconData icon; | |
@override | |
Widget build(BuildContext context) { | |
return ElevatedButton( | |
style: ElevatedButton.styleFrom( | |
padding: const EdgeInsets.all(4), | |
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), | |
), | |
onPressed: () { }, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Icon(icon), | |
Text(label), | |
], | |
), | |
); | |
} | |
} | |
class ContactBar extends StatelessWidget { | |
const ContactBar({ | |
super.key, | |
required this.name, | |
required this.initials, | |
this.fontSize = 16, | |
this.isPrototype = false, | |
}); | |
factory ContactBar.prototype({ required double fontSize }) { | |
return ContactBar( | |
name: 'John Appleseed', | |
initials: 'JA', | |
fontSize: fontSize, | |
isPrototype: true, | |
); | |
} | |
final String name; | |
final String initials; | |
final double fontSize; | |
final bool isPrototype; | |
@override | |
Widget build(BuildContext context) { | |
final ColorScheme colorScheme = Theme.of(context).colorScheme; | |
final Widget nameAndInitials = Column( | |
children: <Widget>[ | |
CircleAvatar( | |
backgroundColor: colorScheme.secondary, | |
child: Text( | |
initials, | |
style: TextStyle(fontSize: fontSize, color: colorScheme.onSecondary) | |
), | |
), | |
Text( | |
name, | |
style: TextStyle(fontSize: fontSize * 0.75), | |
softWrap: false | |
), | |
], | |
); | |
return ColoredBox( | |
color: colorScheme.background, | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
if (isPrototype) | |
nameAndInitials | |
else | |
Expanded(child: FittedBox(child: nameAndInitials)), | |
const SizedBox(height: 8), | |
Row( | |
mainAxisAlignment: MainAxisAlignment.spaceEvenly, | |
children: <Widget>[ | |
const _ContactBarButton(Icons.call, 'call'), | |
const _ContactBarButton(Icons.mms, 'message'), | |
const _ContactBarButton(Icons.mail, 'mail'), | |
].map((Widget child) { | |
return Expanded(child: Padding(padding: const EdgeInsets.all(8), child: child)); | |
}).toList(), | |
), | |
], | |
), | |
); | |
} | |
} | |
// 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, | |
), | |
); | |
} | |
} | |
class SliverLayoutInfo { | |
const SliverLayoutInfo({ | |
required this.constraints, | |
required this.geometry | |
}); | |
final SliverConstraints constraints; | |
final SliverGeometry geometry; | |
} | |
class SliverCoordinatorData { | |
final Map<Object, SliverLayoutInfo> _idToInfo = <Object, SliverLayoutInfo>{}; | |
void put<T extends SliverLayoutInfo>(Object id, T info) { | |
_idToInfo[id] = info; | |
} | |
T? get<T extends SliverLayoutInfo>(Object id) { | |
return _idToInfo[id] as T?; | |
} | |
void clear() => _idToInfo.clear(); | |
} | |
class _SliverCoordinatorScope extends InheritedWidget { | |
const _SliverCoordinatorScope({ required this.data, required super.child }); | |
final SliverCoordinatorData data; | |
@override | |
bool updateShouldNotify(_SliverCoordinatorScope oldWidget) => false; | |
} | |
typedef SliverCoordinatorCallback = void Function(ScrollNotification notification, SliverCoordinatorData data); | |
class SliverCoordinator extends StatefulWidget { | |
const SliverCoordinator({ super.key, required this.callback, required this.child }); | |
final Widget child; | |
final SliverCoordinatorCallback callback; | |
@override | |
State<SliverCoordinator> createState() => _SliverCoordinatorState(); | |
static SliverCoordinatorData of(BuildContext context) { | |
final _SliverCoordinatorScope? scope = context.dependOnInheritedWidgetOfExactType<_SliverCoordinatorScope>(); | |
return scope!.data; | |
} | |
} | |
class _SliverCoordinatorState extends State<SliverCoordinator> { | |
SliverCoordinatorData data = SliverCoordinatorData(); | |
bool handleScrollNotification(ScrollNotification notification) { | |
if (notification is ScrollUpdateNotification) { | |
data.clear(); | |
} | |
// The callback runs after the descendant CustomScollView's viewport | |
// has been laid out and the SliverLayoutInfo object has been updated. | |
SchedulerBinding.instance.addPostFrameCallback((Duration duration) { | |
widget.callback(notification, data); | |
}); | |
return true; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return NotificationListener<ScrollNotification>( | |
onNotification: handleScrollNotification, | |
child: _SliverCoordinatorScope( | |
data: data, | |
child: widget.child, | |
), | |
); | |
} | |
} | |
class ResizingHeaderSliverLayoutInfo extends SliverLayoutInfo{ | |
const ResizingHeaderSliverLayoutInfo({ | |
required super.constraints, | |
required super.geometry, | |
required this.minExtent, | |
required this.maxExtent, | |
}); | |
final double minExtent; | |
final double maxExtent; | |
} | |
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( | |
id: this, | |
minExtentPrototype: minExtentPrototype, | |
maxExtentPrototype: maxExtentPrototype, | |
child: child, | |
); | |
} | |
ResizingHeaderSliverLayoutInfo? getLayoutInfo(SliverCoordinatorData data) { | |
return data.get<ResizingHeaderSliverLayoutInfo>(this); | |
} | |
} | |
enum _Slot { | |
minExtent, | |
maxExtent, | |
child, | |
} | |
class _ResizingHeaderSliver extends SlottedMultiChildRenderObjectWidget<_Slot, RenderBox> { | |
const _ResizingHeaderSliver({ | |
required this.id, | |
this.minExtentPrototype, | |
this.maxExtentPrototype, | |
this.child, | |
}); | |
final Object id; | |
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( | |
id: id, | |
data: SliverCoordinator.of(context), | |
); | |
} | |
@override | |
void updateRenderObject(BuildContext context, _RenderResizingHeaderSliver renderObject) { | |
renderObject.id = id; | |
renderObject.data = SliverCoordinator.of(context); | |
} | |
} | |
class _RenderResizingHeaderSliver extends RenderSliver with SlottedContainerRenderObjectMixin<_Slot, RenderBox>, RenderSliverHelpers { | |
_RenderResizingHeaderSliver({ | |
required this.id, | |
required this.data | |
}); | |
Object id; | |
SliverCoordinatorData data; | |
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. | |
); | |
data.put<ResizingHeaderSliverLayoutInfo>(id, ResizingHeaderSliverLayoutInfo( | |
constraints: constraints, | |
geometry: geometry!, | |
minExtent: minExtent, | |
maxExtent: maxExtent, | |
)); | |
} | |
@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 StatefulWidget { | |
const AppBarParts({ super.key }); | |
@override | |
State<AppBarParts> createState() => _AppBarPartsState(); | |
} | |
class _AppBarPartsState extends State<AppBarParts> { | |
late final ScrollController scrollController; | |
late final ResizingHeaderSliver appBar; | |
@override | |
void initState() { | |
super.initState(); | |
scrollController = ScrollController(); | |
} | |
@override | |
void dispose() { | |
scrollController.dispose(); | |
super.dispose(); | |
} | |
void maybeAutoScroll( | |
double extent, // the header's current height | |
double minExtent, // the height of the header's minExtentPrototype | |
double maxExtent // the height of the header's maxExtentPrototype | |
) { | |
if (extent > minExtent && extent < maxExtent) { | |
scrollController.animateTo( | |
extent > (minExtent + maxExtent) / 2 ? 0 : maxExtent - minExtent, | |
duration: const Duration(milliseconds: 300), | |
curve: Curves.easeInOut | |
); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
final ColorScheme colorScheme = Theme.of(context).colorScheme; | |
return Scaffold( | |
body: SafeArea( | |
child: Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 4), | |
child: SliverCoordinator( | |
callback: (ScrollNotification notification, SliverCoordinatorData data) { | |
ResizingHeaderSliverLayoutInfo? info = appBar.getLayoutInfo(data); | |
if (notification is ScrollEndNotification && info != null) { | |
maybeAutoScroll(info.geometry.paintExtent, info.minExtent, info.maxExtent); | |
} | |
}, | |
child: CustomScrollView( | |
controller: scrollController, | |
slivers: <Widget>[ | |
appBar = ResizingHeaderSliver( | |
minExtentPrototype: ContactBar.prototype(fontSize: 16), | |
maxExtentPrototype: ContactBar.prototype(fontSize: 72), | |
child: const ContactBar( | |
name: 'John Appleseed', | |
initials: 'JA', | |
), | |
), | |
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