Skip to content

Instantly share code, notes, and snippets.

@HansMuller
Created August 30, 2023 00:35
Show Gist options
  • Save HansMuller/ae789e5a01406c3d46e412c40e1537f4 to your computer and use it in GitHub Desktop.
Save HansMuller/ae789e5a01406c3d46e412c40e1537f4 to your computer and use it in GitHub Desktop.
// A CoordinatedSliver that auto-scrolls to align itself with the
// top of the viewport when a scroll gesture leaves it partially visible.
//
// This demo must be run in a simulator or on a mobile device
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
class Item extends StatelessWidget {
const Item({ super.key, required this.title, required this.color });
final String title;
final Color color;
@override
Widget build(BuildContext context) {
return Card(
color: color,
child: ListTile(
textColor: Colors.white,
title: Text(title),
),
);
}
}
// 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 Item(
title: 'Item $index',
color: Color.lerp(startColor, endColor, index / itemCount)!
);
},
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 CoordinatedSliver extends SingleChildRenderObjectWidget {
const CoordinatedSliver({
super.key,
super.child,
});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCoordinatedSliver(
id: this,
data: SliverCoordinator.of(context),
);
}
@override
void updateRenderObject(BuildContext context, RenderCoordinatedSliver renderObject) {
renderObject.id = this;
renderObject.data = SliverCoordinator.of(context);
}
SliverLayoutInfo? getLayoutInfo(SliverCoordinatorData data) {
return data.get<SliverLayoutInfo>(this);
}
}
class RenderCoordinatedSliver extends RenderProxySliver {
RenderCoordinatedSliver({
required this.id,
required this.data,
RenderSliver? child,
}) : super(child);
Object id;
SliverCoordinatorData data;
@override
void performLayout() {
super.performLayout();
data.put<SliverLayoutInfo>(id, SliverLayoutInfo(
constraints: constraints,
geometry: geometry!,
));
}
}
class AppBarParts extends StatefulWidget {
const AppBarParts({ super.key });
@override
State<AppBarParts> createState() => _AppBarPartsState();
}
class _AppBarPartsState extends State<AppBarParts> {
late final ScrollController scrollController;
late CoordinatedSliver alignedItem;
@override
void initState() {
super.initState();
scrollController = ScrollController();
}
@override
void dispose() {
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
const EdgeInsets horizontalPadding = EdgeInsets.symmetric(horizontal: 8);
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: SliverCoordinator(
callback: (ScrollNotification notification, SliverCoordinatorData data) {
SliverLayoutInfo? info = alignedItem.getLayoutInfo(data);
if (info == null) {
return;
}
final double scrollOffset = info.constraints.scrollOffset;
final double itemExtent = info.geometry.scrollExtent;
if (notification is ScrollEndNotification) {
if (scrollOffset > 0 && scrollOffset < itemExtent) {
scrollController.position.animateTo(
info.constraints.precedingScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut
);
}
}
},
child: CustomScrollView(
controller: scrollController,
slivers: <Widget>[
const SliverPadding(
padding: horizontalPadding,
sliver: ItemList(
startColor: Colors.blue,
endColor: Colors.red,
itemCount: 5,
),
),
SliverPadding(
padding: horizontalPadding,
sliver: alignedItem = const CoordinatedSliver(
child: SliverToBoxAdapter(
child: Item(
title: 'AlignedItem',
color: Colors.orange
),
),
),
),
const SliverPadding(
padding: horizontalPadding,
sliver: ItemList(
startColor: Colors.blue,
endColor: Colors.red,
),
),
],
),
),
),
),
);
}
}
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