Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Created August 21, 2023 22:12
Show Gist options
  • Save HansMuller/fd7b154108a142a0ea2ef1be24d09e94 to your computer and use it in GitHub Desktop.
Save HansMuller/fd7b154108a142a0ea2ef1be24d09e94 to your computer and use it in GitHub Desktop.
// 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