Last active
August 18, 2023 00:25
-
-
Save HansMuller/829c0ede0b28e305aa1545edd8aa37f9 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 hack | |
// https://gist.github.com/HansMuller/829c0ede0b28e305aa1545edd8aa37f9 | |
// dartpad.dev/?id=829c0ede0b28e305aa1545edd8aa37f9 | |
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 _SliverInfo { | |
const _SliverInfo(this.constraints, this.geometry, this.boxConstraints); | |
final SliverConstraints constraints; | |
final SliverGeometry geometry; | |
final BoxConstraints boxConstraints; | |
} | |
class SliverLayoutInfo { | |
final Map<Object, _SliverInfo> _idToInfo = <Object, _SliverInfo>{}; | |
static void _save(RenderObject sliver, SliverConstraints constraints, SliverGeometry geometry, BoxConstraints boxConstraints) { | |
RenderObject? coordinatedSliver = sliver; | |
while (coordinatedSliver != null) { | |
if (coordinatedSliver is RenderCoordinatedSliver) { | |
coordinatedSliver.info._idToInfo[coordinatedSliver.id] = _SliverInfo(constraints, geometry, boxConstraints); | |
break; | |
} | |
coordinatedSliver = coordinatedSliver.parent; | |
} | |
assert(coordinatedSliver != null); | |
} | |
void _clear() => _idToInfo.clear(); | |
bool get isEmpty => _idToInfo.isEmpty; | |
bool get isNotEmpty => _idToInfo.isNotEmpty; | |
bool isVisible(id) => _idToInfo.containsKey(id); | |
SliverConstraints constraints(id) => _idToInfo[id]!.constraints; | |
SliverGeometry geometry(id) => _idToInfo[id]!.geometry; | |
double scrollOffset(id) => constraints(id).scrollOffset; | |
double scrollExtent(id) => geometry(id).scrollExtent; | |
double extent(id) => geometry(id).paintExtent; | |
double minExtent(id) { | |
final BoxConstraints boxConstraints = _idToInfo[id]!.boxConstraints; | |
return switch (constraints(id).axis) { | |
Axis.vertical => boxConstraints.minHeight, | |
Axis.horizontal => boxConstraints.minWidth, | |
}; | |
} | |
double maxExtent(id) { | |
final BoxConstraints boxConstraints = _idToInfo[id]!.boxConstraints; | |
return switch (constraints(id).axis) { | |
Axis.vertical => boxConstraints.maxHeight, | |
Axis.horizontal => boxConstraints.maxWidth, | |
}; | |
} | |
} | |
typedef SliverCoordinatorCallback = void Function(ScrollNotification n, SliverLayoutInfo i); | |
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 SliverLayoutInfo of(BuildContext context) { | |
final _SliverCoordinatorScope? scope = context.dependOnInheritedWidgetOfExactType<_SliverCoordinatorScope>(); | |
return scope!.info; | |
} | |
} | |
class _SliverCoordinatorState extends State<SliverCoordinator> { | |
SliverLayoutInfo info = SliverLayoutInfo(); | |
bool handleScrollNotification(ScrollNotification notification) { | |
if (notification is ScrollUpdateNotification) { | |
info._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, info); | |
}); | |
return true; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return NotificationListener<ScrollNotification>( | |
onNotification: handleScrollNotification, | |
child: _SliverCoordinatorScope( | |
info: info, | |
child: widget.child, | |
), | |
); | |
} | |
} | |
class _SliverCoordinatorScope extends InheritedWidget { | |
const _SliverCoordinatorScope({ required this.info, required super.child }); | |
final SliverLayoutInfo info; | |
@override | |
bool updateShouldNotify(_SliverCoordinatorScope oldWidget) => false; | |
} | |
class CoordinatedSliver extends SingleChildRenderObjectWidget { | |
const CoordinatedSliver({ | |
super.key, | |
required this.id, | |
super.child, | |
}); | |
final Object id; | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return RenderCoordinatedSliver( | |
id: id, | |
info: SliverCoordinator.of(context), | |
); | |
} | |
@override | |
void updateRenderObject(BuildContext context, RenderCoordinatedSliver renderObject) { | |
renderObject.id = id; | |
renderObject.info = SliverCoordinator.of(context); | |
} | |
} | |
class RenderCoordinatedSliver extends RenderProxySliver { | |
RenderCoordinatedSliver({ | |
required this.id, | |
required this.info, | |
RenderSliver? child, | |
}) : super(child); | |
Object id; | |
SliverLayoutInfo info; | |
} | |
// 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. | |
); | |
SliverLayoutInfo._save(this, constraints, geometry!, constraints.asBoxConstraints( | |
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 double height; | |
static const String appBar = 'appBar'; // Used as a CoordinatedSliver id | |
@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, SliverLayoutInfo i) { | |
if (i.isVisible(appBar) && notification is ScrollEndNotification) { | |
maybeAutoScroll(i.extent(appBar), i.minExtent(appBar), i.maxExtent(appBar)); | |
} | |
}, | |
child: CustomScrollView( | |
controller: scrollController, | |
slivers: <Widget>[ | |
CoordinatedSliver( | |
id: 'appBar', | |
child: 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