Last active
June 7, 2024 08:00
-
-
Save CoderNamedHendrick/e94238c8203a27c2ecd934cc2fe12fcd to your computer and use it in GitHub Desktop.
Detect widget on screen
This file contains 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'; | |
// Records | |
typedef Position = ({double? x, double? y}); | |
typedef VisibleArea = ({Position topLeft, Position bottomRight}); | |
// Callbacks | |
typedef SizeCallback = void Function(({double? height, double? width})); | |
typedef PositionCallback = void Function(Position); | |
typedef VisibleAreaCallback = void Function(VisibleArea area); | |
const _kDefaultVisibleThreshold = 0.1; | |
class DetectableWidget extends StatefulWidget { | |
const DetectableWidget({ | |
super.key, | |
required this.child, | |
this.ancestorToDetectIn, | |
this.size, | |
this.position, | |
this.visibleArea, | |
this.scrollOffsetNotifier, | |
this.onDetect, | |
this.visibleProgress, | |
this.visibilityThreshold = _kDefaultVisibleThreshold, | |
}) : assert( | |
visibilityThreshold >= 0 || visibilityThreshold <= 1, | |
'Visible threshold must be between 0 and 1', | |
); | |
final Widget child; | |
final RenderObject? ancestorToDetectIn; | |
final SizeCallback? size; | |
final PositionCallback? position; | |
final VisibleAreaCallback? visibleArea; | |
final ValueNotifier<double>? scrollOffsetNotifier; | |
final ValueChanged<bool>? onDetect; | |
final ValueChanged<double>? visibleProgress; | |
final double | |
visibilityThreshold; // by how much do you want the progress gone before calling onDetect? | |
@override | |
State<DetectableWidget> createState() => _DetectableWidgetState(); | |
} | |
class _DetectableWidgetState extends State<DetectableWidget> { | |
VisibleArea? visibleArea; | |
bool? prevVisibleStatus; | |
void _scrollOffsetListener() { | |
if (widget.scrollOffsetNotifier == null || visibleArea == null) { | |
return; | |
} | |
final scrollOffset = widget.scrollOffsetNotifier!.value; | |
final topYPosition = visibleArea!.topLeft.y!; | |
final bottomYPosition = visibleArea!.bottomRight.y!; | |
final visibilityProgress = (bottomYPosition - scrollOffset) / // | |
// basically height | |
(bottomYPosition - topYPosition); | |
widget.visibleProgress | |
?.call(double.parse(visibilityProgress.clamp(0, 1).toStringAsFixed(2))); | |
final isVisible = scrollOffset <= bottomYPosition; | |
if (prevVisibleStatus == isVisible) return; | |
if (prevVisibleStatus == false && | |
visibilityProgress < widget.visibilityThreshold) return; | |
widget.onDetect?.call(isVisible); | |
prevVisibleStatus = isVisible; | |
} | |
@override | |
void initState() { | |
super.initState(); | |
widget.scrollOffsetNotifier?.addListener(_scrollOffsetListener); | |
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) { | |
_initialiseDetection(); | |
_scrollOffsetListener(); | |
}); | |
} | |
@override | |
void didUpdateWidget(covariant DetectableWidget oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
oldWidget.scrollOffsetNotifier?.removeListener(_scrollOffsetListener); | |
widget.scrollOffsetNotifier?.addListener(_scrollOffsetListener); | |
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) { | |
_initialiseDetection(); | |
}); | |
} | |
void _initialiseDetection() { | |
final renderBox = context.findRenderObject() as RenderBox?; | |
final positionTranslation = context | |
.findRenderObject() | |
?.getTransformTo(widget.ancestorToDetectIn) | |
.getTranslation(); | |
widget.position | |
?.call((x: positionTranslation?.x, y: positionTranslation?.y)); | |
widget.size?.call( | |
(width: renderBox?.size.width, height: renderBox?.size.height), | |
); | |
visibleArea = ( | |
topLeft: (x: positionTranslation?.x, y: positionTranslation?.y), | |
bottomRight: ( | |
x: (NullableNum(positionTranslation?.x) + | |
NullableNum(renderBox?.size.width)) | |
?.toDouble(), | |
y: (NullableNum(positionTranslation?.y) + | |
NullableNum(renderBox?.size.height)) | |
?.toDouble(), | |
), | |
); | |
widget.visibleArea?.call(visibleArea!); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return widget.child; | |
} | |
} | |
extension type const NullableNum(num? i) { | |
num? operator +(NullableNum other) { | |
if (i == null) return null; | |
if (other.i == null) return null; | |
return i! + other.i!; | |
} | |
num? operator -(NullableNum other) { | |
if (i == null) return null; | |
if (other.i == null) return null; | |
return i! - other.i!; | |
} | |
} |
This file contains 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:detect_object_poc/detectable_widget.dart'; | |
import 'package:flutter/material.dart'; | |
import 'dart:math'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), | |
useMaterial3: true, | |
), | |
home: const MyHomePage(title: 'Flutter Demo Home Page'), | |
); | |
} | |
} | |
class MyHomePage extends StatefulWidget { | |
const MyHomePage({super.key, required this.title}); | |
final String title; | |
@override | |
State<MyHomePage> createState() => _MyHomePageState(); | |
} | |
class _MyHomePageState extends State<MyHomePage> { | |
@override | |
Widget build(BuildContext context) { | |
return const Scaffold(body: _ScaffoldBody()); | |
} | |
} | |
class _ScaffoldBody extends StatefulWidget { | |
const _ScaffoldBody(); | |
@override | |
State<_ScaffoldBody> createState() => _ScaffoldBodyState(); | |
} | |
class _ScaffoldBodyState extends State<_ScaffoldBody> { | |
final scrollKey = GlobalKey(); | |
RenderObject? scrollRenderObject; | |
final scrollController = ScrollController(); | |
final scrollOffset = ValueNotifier<double>(0); | |
double visibleProgress = 0; | |
@override | |
void initState() { | |
super.initState(); | |
scrollController.addListener(() { | |
if (!scrollController.hasClients) return; | |
scrollOffset.value = scrollController.offset; | |
}); | |
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) { | |
scrollRenderObject = (scrollKey.currentContext?.findRenderObject()); | |
}); | |
} | |
@override | |
void didUpdateWidget(covariant _ScaffoldBody oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) { | |
scrollRenderObject = (scrollKey.currentContext?.findRenderObject()); | |
}); | |
} | |
@override | |
void dispose() { | |
scrollController.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
children: [ | |
Align( | |
alignment: const Alignment(0.8, -0.8), | |
child: FloatingActionButton.large( | |
onPressed: () {}, | |
child: Text( | |
visibleProgress.toString(), | |
), | |
), | |
), | |
SingleChildScrollView( | |
controller: scrollController, | |
child: Column( | |
key: scrollKey, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
const _Dummy(), | |
DetectableWidget( | |
scrollOffsetNotifier: scrollOffset, | |
ancestorToDetectIn: scrollRenderObject, | |
onDetect: (isVisible) { | |
late final SnackBar snackBar; | |
if (isVisible) { | |
snackBar = const SnackBar( | |
content: Text('I\'m Visible'), | |
backgroundColor: Colors.green, | |
duration: Duration(milliseconds: 300), | |
); | |
} else { | |
snackBar = const SnackBar( | |
content: Text('I\'m not Visible'), | |
backgroundColor: Colors.red, | |
duration: Duration(milliseconds: 300), | |
); | |
} | |
ScaffoldMessenger.of(context).showSnackBar(snackBar); | |
}, | |
visibilityThreshold: 0.01, | |
visibleProgress: (progress) { | |
setState(() { | |
visibleProgress = progress; | |
}); | |
}, | |
child: Container( | |
height: 300, | |
width: 500, | |
color: Colors.indigo, | |
), | |
), | |
...List.generate(30, (_) => const _Dummy()), | |
], | |
), | |
), | |
], | |
); | |
} | |
} | |
class _Dummy extends StatelessWidget { | |
const _Dummy(); | |
double get notZeroToOne { | |
final random = Random().nextDouble(); | |
if (random < 0.5) return random + 0.2; | |
return random; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
height: notZeroToOne * 300, | |
width: notZeroToOne * 300, | |
color: Colors.amber.withAlpha((notZeroToOne * 255).toInt()), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment