To get the size/position of a widget on screen, you can use GlobalKey
to get its BuildContext
to then find the RenderBox
of that specific widget, which will contain its global position and rendered size.
There is just one thing to be careful of: That context may not exist if the widget is not rendered. Which can cause a problem with ListView
as widgets are rendered only if they are potentially visible.
Another problem is that you can't get a widget's RenderBox
during the build
call as the widget hasn't been rendered yet.
But what if I need to get the size during the build! What can I do?
There's one cool widget that can help: Overlay
and its OverlayEntry
. They are used to display widgets on top of everything else (similar to the stack).
But the coolest thing is that they are on a different build
flow; they are built after regular widgets.
That have one super cool implication: OverlayEntry
can have a size that depends on widgets of the actual widget tree.
Okay. But don't OverlayEntry requires to be rebuilt manually?
Yes, they do. But there's another thing to be aware of: ScrollController
, passed to a Scrollable
, is a listenable similar to AnimationController
.
Which means you could combine an AnimatedBuilder
with a ScrollController
. It would have the lovely effect to rebuild your widget automatically on a scroll. Perfect for this situation, right?
Combining everything into an example:
In the following example, you'll see an overlay that follows a widget inside a ListView
and shares the same height.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, this.title}) : super(key: key);
final String? title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final controller = ScrollController();
OverlayEntry? sticky;
GlobalKey stickyKey = GlobalKey();
@override
void initState() {
sticky?.remove();
sticky = OverlayEntry(
builder: (context) => stickyBuilder(context),
);
SchedulerBinding.instance.addPostFrameCallback((_) {
if (sticky != null) {
Overlay.of(context).insert(sticky!);
}
});
super.initState();
}
@override
void dispose() {
sticky?.remove();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
if (index == 6) {
return Container(
key: stickyKey,
height: 100.0,
color: Colors.green,
child: const Text("I'm fat"),
);
}
return ListTile(
title: Text(
'Hello $index',
style: const TextStyle(color: Colors.white),
),
);
},
),
);
}
Widget stickyBuilder(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, child) {
final keyContext = stickyKey.currentContext;
if (keyContext != null) {
// widget is visible
final box = keyContext.findRenderObject() as RenderBox;
final pos = box.localToGlobal(Offset.zero);
return Positioned(
top: pos.dy + box.size.height,
left: 50.0,
right: 50.0,
height: box.size.height,
child: Material(
child: Container(
alignment: Alignment.center,
color: Colors.purple,
child: const Text("^ Nah I think you're okay"),
),
),
);
}
return Container();
},
);
}
}
Note:
When navigating to a different screen, call the following. Otherwise, sticky would stay visible.
sticky.remove();
new Text("hello")
to more complex ones. I lay these widgets into ListView, and I need their height to compute some scroll effects. I am OK with getting the height at the layout time, just like what SingleChildLayoutDelegate is doing.