Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Created August 17, 2023 18:57
Show Gist options
  • Save HansMuller/cade8375566dca2ad7f5c115d930578c to your computer and use it in GitHub Desktop.
Save HansMuller/cade8375566dca2ad7f5c115d930578c to your computer and use it in GitHub Desktop.
// ReszingHeaderSliver AppBar Parts example: contacts version (no autoscroll)
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.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,
),
);
}
}
// 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 StatefulWidget {
const AppBarParts({ super.key });
@override
State<AppBarParts> createState() => _AppBarPartsState();
}
class _AppBarPartsState extends State<AppBarParts> {
@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: CustomScrollView(
slivers: <Widget>[
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