Skip to content

Instantly share code, notes, and snippets.

@CoderNamedHendrick
Last active January 14, 2023 08:32
Show Gist options
  • Save CoderNamedHendrick/c3d4dcfa8f3b7357a44e12ec8a610c53 to your computer and use it in GitHub Desktop.
Save CoderNamedHendrick/c3d4dcfa8f3b7357a44e12ec8a610c53 to your computer and use it in GitHub Desktop.
Detect visiblity of widget(s) in scrollables
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
static const itemCount = 10;
late final List<GlobalKey> _keys;
late final List<ScrollObjectListener> _listeners;
late final List<int> _counters;
late final List<Ticker> _tickers;
late ScrollController _controller;
@override
void initState() {
super.initState();
_keys = List.generate(itemCount, (index) => GlobalKey());
_listeners = List.generate(itemCount, (index) => ScrollObjectListener());
_counters = List.generate(itemCount, (index) => 0);
_tickers = List.generate(itemCount,
(index) => Ticker((_) => setState(() => _counters[index]++)));
// scroll controller impl
_controller = ScrollController();
// Scroll controller impl
_controller.addListener(() {
ScrollObjectListener.init(_controller.position);
_fireListeners(_controller.position);
});
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) {
// first listener call before using scroll notification
// _controller impl
_initListeners(_controller.position.axis);
// for notification impl you have to hardcode the axis
// _initListeners(Axis.vertical);
});
}
void _initListeners(Axis axis) {
ScrollObjectListener.direction = ScrollDirection.reverse;
ScrollObjectListener.axis = axis;
for (int i = 0; i < itemCount; i++) {
final show = _keys.elementAt(i).detectMe(
_listeners.elementAt(i),
axis == Axis.horizontal
? MediaQuery.of(context).size.width
: MediaQuery.of(context).size.height);
if (show == null) continue;
if (!show.isNegative) {
_startTicker(i);
}
}
}
void _startTicker(int index) {
if (_tickers.elementAt(index).isActive) {
return;
}
_tickers.elementAt(index).start();
debugPrint('Ticker $index Started');
}
void _stopTicker(int index, [bool canceled = false]) {
if (_tickers.elementAt(index).isActive) {
_tickers.elementAt(index).stop(canceled: canceled);
debugPrint('Ticker $index stopped');
}
}
void _fireListeners(ScrollMetrics metrics) {
for (int i = 0; i < itemCount; i++) {
final showPercent = _keys.elementAt(i).detectMe(
_listeners.elementAt(i), //
metrics.viewportDimension);
if (showPercent == null) continue;
if (showPercent.isNegative) {
_stopTicker(i);
}
if (showPercent >= 27 && showPercent <= 33) {
_startTicker(i);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
// notification listener impl
child: NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
ScrollObjectListener.init(scrollNotification.metrics);
_fireListeners(scrollNotification.metrics);
return false;
},
child: SingleChildScrollView(
// using a controller impl
controller: _controller,
physics: const BouncingScrollPhysics(),
// scrollDirection: Axis.horizontal,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...List.generate(
itemCount,
(index) => Column(
mainAxisSize: MainAxisSize.min,
children: [
HoldingContainer(
index: _counters.elementAt(index),
key: _keys.elementAt(index),
color: [Colors.red, Colors.blue, Colors.green]
.elementAt(index % 3),
// horizontalSense: true,
),
const SizedBox(height: 10),
],
),
),
],
),
),
),
),
);
}
}
class HoldingContainer extends StatelessWidget {
const HoldingContainer(
{Key? key, required this.index, this.color, this.horizontalSense = false})
: super(key: key);
final int index;
final Color? color;
final bool horizontalSense;
static Color get _color {
final rand = Random();
return Color.fromRGBO(
rand.nextInt(256),
rand.nextInt(256),
rand.nextInt(256),
1,
);
}
static const dim = 500.0;
@override
Widget build(BuildContext context) {
return Container(
height: horizontalSense ? double.infinity : dim,
width: horizontalSense ? dim : double.infinity,
color: color ?? _color,
child: Center(
child: Text(
index.toString(),
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.w600,
),
),
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
extension RenderObjectVisibleProps on GlobalKey {
int? detectMe(ScrollObjectListener listener, double viewPortDim) {
final RenderObject? box = currentContext?.findRenderObject();
if (box != null) {
return listener.detectBox(box as RenderBox, viewPortDim);
}
return null;
}
}
class ScrollObjectListener {
static ScrollDirection direction = ScrollDirection.idle;
static Axis _axis = Axis.vertical;
static set axis(Axis axis) => _axis = axis;
static double _position = 0;
int _lastKnownPercentage = -1;
set percentageCovered(int percentage) => _lastKnownPercentage = percentage;
ScrollObjectListener();
static void init(ScrollMetrics metrics) {
_axis = metrics.axis;
if (metrics.pixels > _position && _position < metrics.maxScrollExtent) {
direction = ScrollDirection.reverse;
} else if (metrics.pixels < _position &&
_position > metrics.minScrollExtent) {
direction = ScrollDirection.forward;
} else {
direction = ScrollDirection.idle;
}
_position = metrics.pixels;
}
int detectBox(RenderBox box, double viewPortDimension) {
final boxSize = box.size;
// Early return for direction is idle.
if (direction == ScrollDirection.idle) {
return _lastKnownPercentage;
}
if (_axis == Axis.horizontal) {
// Listen for box when orientation is horizontal.
return _horizontalDetectBox(box, viewPortDimension, boxSize);
}
// Listen for box when orientation is vertical.
return _verticalDetectBox(box, viewPortDimension, boxSize);
}
int _verticalDetectBox(RenderBox box, double viewPortHeight, Size boxSize) {
late final double offsetDy;
late final double viewPortCovered;
// Scrolling from the bottom
if (direction == ScrollDirection.forward) {
offsetDy =
box.localToGlobal(Offset(0, viewPortHeight + boxSize.height)).dy;
viewPortCovered = offsetDy - viewPortHeight;
return _calculateBoxPercentageCovered(
viewPortCovered, viewPortHeight, boxSize.height);
}
// Scrolling from the top
offsetDy = box.localToGlobal(Offset.zero).dy;
viewPortCovered = viewPortHeight - offsetDy;
return _calculateBoxPercentageCovered(
viewPortCovered, viewPortHeight, boxSize.height);
}
int _horizontalDetectBox(RenderBox box, double viewPortWidth, Size boxSize) {
late final double offsetDx;
late final double viewPortCovered;
// Scrolling from the right
if (direction == ScrollDirection.forward) {
offsetDx = box.localToGlobal(Offset(viewPortWidth + boxSize.width, 0)).dx;
viewPortCovered = offsetDx - viewPortWidth;
return _calculateBoxPercentageCovered(
viewPortCovered, viewPortWidth, boxSize.width);
}
// Scrolling from the left
offsetDx = box.localToGlobal(Offset.zero).dx;
viewPortCovered = viewPortWidth - offsetDx;
return _calculateBoxPercentageCovered(
viewPortCovered, viewPortWidth, boxSize.width);
}
int _calculateBoxPercentageCovered(
double viewPortCovered, double viewPortDimension, double boxDimension) {
if (viewPortCovered.isNegative) {
percentageCovered = -1;
return _lastKnownPercentage;
}
if (viewPortCovered > (viewPortDimension + boxDimension)) {
percentageCovered = -1;
return _lastKnownPercentage;
}
percentageCovered = ((viewPortCovered / boxDimension) * 100).ceil();
return _lastKnownPercentage;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment