Last active
January 14, 2023 08:32
-
-
Save CoderNamedHendrick/c3d4dcfa8f3b7357a44e12ec8a610c53 to your computer and use it in GitHub Desktop.
Detect visiblity of widget(s) in scrollables
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
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, | |
), | |
), | |
), | |
); | |
} | |
} |
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
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