I'm Sharing for visibility on how to approach detecting position of widget in general.
I was curious as to how you access positional data of widgets, and also wanted to be able to control the animated state of a ListView child element.
Looks like the main point of access to a widgets, size and position is via the BuildContext's
context.findRenderObject()
However, this is only usable after the component has been built and the widget is mounted.
This is addressed by using context.findRenderObject() in a function called using WidgetsBinding.instance.addPostFrameCallback((_) => calculatePosition(context));
Here's a wrapper component you can use in your ListView.itemBuilder()
code
import 'package:flutter/cupertino.dart';
import 'dart:developer' as developer;
enum POCInViewDirection { up, down, static }
class POCInView extends StatefulWidget {
final Widget child;
final double scrollHeight;
const POCInView({super.key, required this.child, required this.scrollHeight});
@override
POCInState createState() => POCInState();
}
class POCInState extends State<POCInView> {
bool inView = false; // are you in view or not.
double lastPositionY = 0; // used to determine which direction your widget is moving.
POCInViewDirection direction = POCInViewDirection.static; // Set based on direction your moving.
RenderBox? renderBoxRef;
bool skip = true;
@override
void initState() {
super.initState();
developer.log('InitState', name: 'POCInView');
lastPositionY = 0;
renderBoxRef = null;
direction = POCInViewDirection.static;
skip = true;
}
/// Calculate if this widget is in view.
/// uses BuildContext.findRenderObject() to get the RenderBox.
/// RenderBox has localToGlobal which will give you the objects offset(position)
/// Do some math to workout if you object is in view.
/// i.e. take into account widget height and position.
///
/// I only do Y coordinates.
///
void calculatePosition(BuildContext context) {
// findRenderObject() will fail if the widget has been unmounted. so leave if not mounted.
if (!mounted) {
renderBoxRef = null;
return;
}
// It says this can be quite expensive as it will hunt through the view tree to find a RenderBox.
// probably worth timing or seeing if its too much for you view.
// I've put a rough cache in, deleting the ref when its unmounted. mmmmm.
renderBoxRef ??= context.findRenderObject() as RenderBox;
//
inView = false;
if (renderBoxRef is RenderBox) {
Offset childOffset = renderBoxRef!.localToGlobal(Offset.zero);
final double y = childOffset.dy;
final double componentHeight = context.size!.height;
final double screenHeight = widget.scrollHeight;
if (y < screenHeight) {
if (y + componentHeight < -20) {
inView = false;
} else {
inView = true;
}
} else {
inView = false;
}
// work out which direction we're moving. Not quite working right yet.
direction = y > lastPositionY ? POCInViewDirection.down : POCInViewDirection.up;
lastPositionY = y;
//developer.log('In View: $inView, childOffset: ${childOffset.dy.toString()}', name: 'POCInView');
}
skip = false;
}
@override
Widget build(BuildContext context) {
// calculate position after build is complete. this is required to use context.findRenderObject().
WidgetsBinding.instance.addPostFrameCallback((_) => calculatePosition(context));
// fade in when in view.
final oChild = AnimatedOpacity(opacity: inView ? 1 : 0, duration: const Duration(seconds: 1), child: widget.child);
// slide in when in view, and adjust slide direction based on scroll direction.
return AnimatedSlide(
duration: Duration(seconds: inView ? 1 : 0),
offset: Offset(0, inView ? 0.0 : 0.25 * (skip == true ? 0 : (direction == POCInViewDirection.up ? 1 : -1))),
child: oChild,
);
}
}
ListView.builder
witch you can pass aitemBuilder
, that will build the 'widget' of a line in yourlistView
. So the flutter will manage to you witch line is visible and render it, and clear from memory the invisible ones... – Bath