add feature to allow listeners on build extent enter/exit
This commit is contained in:
+10
-1
@@ -1,6 +1,7 @@
|
|||||||
import 'simple_example.dart';
|
import 'simple_example.dart';
|
||||||
import './caching_test.dart';
|
import './caching_test.dart';
|
||||||
import './dynamic_widget_example.dart';
|
import './dynamic_widget_example.dart';
|
||||||
|
import './render_callbacks_example.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -45,6 +46,13 @@ class ExampleChooser extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: const Text('Dynamic Widget Inputs'),
|
child: const Text('Dynamic Widget Inputs'),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.push(context, MaterialPageRoute(builder: (context) => const RenderCallbacksExample()));
|
||||||
|
},
|
||||||
|
child: const Text('Render Callbacks Demo'),
|
||||||
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
@@ -52,7 +60,8 @@ class ExampleChooser extends StatelessWidget {
|
|||||||
'Examples:\n'
|
'Examples:\n'
|
||||||
'• Simple: Basic usage demo\n'
|
'• Simple: Basic usage demo\n'
|
||||||
'• Caching Test: To sanity check build counts\n'
|
'• Caching Test: To sanity check build counts\n'
|
||||||
'• Dynamic Inputs: How to handle changing widget data',
|
'• Dynamic Inputs: How to handle changing widget data\n'
|
||||||
|
'• Render Callbacks: See widgets enter/exit render area',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(fontSize: 14, color: Colors.grey),
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:infinite_lazy_grid/infinite_lazy_grid.dart';
|
||||||
|
import 'widgets/fps.dart';
|
||||||
|
|
||||||
|
class RenderCallbacksExample extends StatefulWidget {
|
||||||
|
const RenderCallbacksExample({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RenderCallbacksExample> createState() => _RenderCallbacksExampleState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenderCallbacksExampleState extends State<RenderCallbacksExample> {
|
||||||
|
late final LazyCanvasController controller;
|
||||||
|
final List<CanvasChildId> childIds = [];
|
||||||
|
final List<String> logs = [];
|
||||||
|
final ScrollController logScrollController = ScrollController();
|
||||||
|
|
||||||
|
void onWidgetEnteredRender(CanvasChildId id) {
|
||||||
|
_addLog('Widget $id entered render area', Colors.green);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onWidgetExitedRender(CanvasChildId id) {
|
||||||
|
_addLog('Widget $id exited render area', Colors.red);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// Initialize controller with render callbacks
|
||||||
|
controller = LazyCanvasController(
|
||||||
|
debug: true,
|
||||||
|
onWidgetEnteredRender: onWidgetEnteredRender,
|
||||||
|
onWidgetExitedRender: onWidgetExitedRender,
|
||||||
|
);
|
||||||
|
|
||||||
|
_createGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createGrid() {
|
||||||
|
// Create a 10x10 grid of widgets
|
||||||
|
for (int row = 0; row < 4; row++) {
|
||||||
|
for (int col = 0; col < 4; col++) {
|
||||||
|
final id = controller.addChild(
|
||||||
|
Offset(col * 180.0, row * 180.0),
|
||||||
|
GridItem(row: row, col: col, color: Colors.primaries[(row * 10 + col) % Colors.primaries.length]),
|
||||||
|
);
|
||||||
|
childIds.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addLog(String message, Color color) {
|
||||||
|
setState(() {
|
||||||
|
logs.insert(0, message);
|
||||||
|
// Keep only the last 50 logs
|
||||||
|
if (logs.length > 50) {
|
||||||
|
logs.removeRange(50, logs.length);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-scroll to top of logs
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (logScrollController.hasClients) {
|
||||||
|
logScrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearLogs() {
|
||||||
|
setState(() {
|
||||||
|
logs.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Render Callbacks Example')),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
LazyCanvas(controller: controller),
|
||||||
|
|
||||||
|
// FPS counter
|
||||||
|
const Positioned(bottom: 16, left: 16, child: Fps()),
|
||||||
|
|
||||||
|
// Logs panel
|
||||||
|
Positioned(
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Container(
|
||||||
|
width: 350,
|
||||||
|
height: 400,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.8),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.blue,
|
||||||
|
borderRadius: BorderRadius.only(topLeft: Radius.circular(8), topRight: Radius.circular(8)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Render Callbacks Log',
|
||||||
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: _clearLogs,
|
||||||
|
icon: const Icon(Icons.clear, color: Colors.white),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Logs list
|
||||||
|
Expanded(
|
||||||
|
child: logs.isEmpty
|
||||||
|
? const Center(
|
||||||
|
child: Text(
|
||||||
|
'Pan around to see render callbacks',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
controller: logScrollController,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
itemCount: logs.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final log = logs[index];
|
||||||
|
final isEntered = log.contains('entered');
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isEntered ? Icons.add_circle : Icons.remove_circle,
|
||||||
|
color: isEntered ? Colors.green : Colors.red,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
log,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isEntered ? Colors.green : Colors.red,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
logScrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GridItem extends StatelessWidget {
|
||||||
|
final int row;
|
||||||
|
final int col;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const GridItem({super.key, required this.row, required this.col, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('Tapped grid item ($row, $col)'), duration: const Duration(seconds: 1)));
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
border: Border.all(color: Colors.black, width: 1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'$row',
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$col',
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,9 @@ class LazyCanvasController with ChangeNotifier {
|
|||||||
double? _lastProcessedScale;
|
double? _lastProcessedScale;
|
||||||
bool _markDirty = false; // do any of the non scale or offset changes require a rebuild?
|
bool _markDirty = false; // do any of the non scale or offset changes require a rebuild?
|
||||||
final bool useIdsFromArgs;
|
final bool useIdsFromArgs;
|
||||||
|
OnWidgetEnteredRender? onWidgetEnteredRender;
|
||||||
|
OnWidgetExitedRender? onWidgetExitedRender;
|
||||||
|
Set<CanvasChildId> _renderedWidgets = {}; // to track which widgets are currently rendered
|
||||||
|
|
||||||
bool debug;
|
bool debug;
|
||||||
final Duration defaultAnimationDuration;
|
final Duration defaultAnimationDuration;
|
||||||
@@ -48,6 +51,8 @@ class LazyCanvasController with ChangeNotifier {
|
|||||||
this.defaultAnimationDuration = const Duration(milliseconds: 300),
|
this.defaultAnimationDuration = const Duration(milliseconds: 300),
|
||||||
this.background = const DotGridBackground(),
|
this.background = const DotGridBackground(),
|
||||||
this.useIdsFromArgs = false,
|
this.useIdsFromArgs = false,
|
||||||
|
this.onWidgetEnteredRender,
|
||||||
|
this.onWidgetExitedRender,
|
||||||
}) : _hashCellSize = hashCellSize,
|
}) : _hashCellSize = hashCellSize,
|
||||||
_buildCacheExtent = buildCacheExtent != null ? buildCacheExtent + Offset(50, 50) : null
|
_buildCacheExtent = buildCacheExtent != null ? buildCacheExtent + Offset(50, 50) : null
|
||||||
// only top left is considered so if a widget has long width, it'll not be rendered
|
// only top left is considered so if a widget has long width, it'll not be rendered
|
||||||
@@ -117,7 +122,7 @@ class LazyCanvasController with ChangeNotifier {
|
|||||||
Size? childSize,
|
Size? childSize,
|
||||||
CanvasChildId? id,
|
CanvasChildId? id,
|
||||||
}) {
|
}) {
|
||||||
assert(!useIdsFromArgs && useIdsFromArgs && id == null);
|
assert(!useIdsFromArgs || useIdsFromArgs && id != null);
|
||||||
id ??= _uuid.v4();
|
id ??= _uuid.v4();
|
||||||
if (focusOnInit) {
|
if (focusOnInit) {
|
||||||
assert(
|
assert(
|
||||||
@@ -174,17 +179,6 @@ class LazyCanvasController with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the position of a child by its ID.
|
|
||||||
Offset getPosition(CanvasChildId id) {
|
|
||||||
if (_children.containsKey(id)) {
|
|
||||||
return _children[id]!.gsPosition;
|
|
||||||
} else {
|
|
||||||
throw _ChildNotFoundException;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Gesture Handling ====================
|
|
||||||
|
|
||||||
/// Called when a scale gesture starts.
|
/// Called when a scale gesture starts.
|
||||||
void onScaleStart(ScaleStartDetails details) {
|
void onScaleStart(ScaleStartDetails details) {
|
||||||
_baseScale = _scale;
|
_baseScale = _scale;
|
||||||
@@ -245,6 +239,24 @@ class LazyCanvasController with ChangeNotifier {
|
|||||||
|
|
||||||
final idsToBuild = _childrenWithinBuildArea(_gsCenter, _buildExtent);
|
final idsToBuild = _childrenWithinBuildArea(_gsCenter, _buildExtent);
|
||||||
|
|
||||||
|
// do the needed callbacks
|
||||||
|
if (onWidgetEnteredRender != null || onWidgetExitedRender != null) {
|
||||||
|
final newRenderedWidgets = idsToBuild.toSet();
|
||||||
|
final exitedWidgets = _renderedWidgets.difference(newRenderedWidgets);
|
||||||
|
final enteredWidgets = newRenderedWidgets.difference(_renderedWidgets);
|
||||||
|
|
||||||
|
Future.microtask(() {
|
||||||
|
// so that the build is not blocked
|
||||||
|
for (final id in exitedWidgets) {
|
||||||
|
onWidgetExitedRender?.call(id);
|
||||||
|
}
|
||||||
|
for (final id in enteredWidgets) {
|
||||||
|
onWidgetEnteredRender?.call(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_renderedWidgets = idsToBuild.toSet();
|
||||||
|
|
||||||
return _lastRenderedWidgets = idsToBuild.map((id) {
|
return _lastRenderedWidgets = idsToBuild.map((id) {
|
||||||
final item = _children[id]!;
|
final item = _children[id]!;
|
||||||
final ssPosition = gsToSs(item.gsPosition, _gsTopLeftOffset, _scale);
|
final ssPosition = gsToSs(item.gsPosition, _gsTopLeftOffset, _scale);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class _Debug extends StatelessWidget {
|
|||||||
left: 0,
|
left: 0,
|
||||||
bottom: -60,
|
bottom: -60,
|
||||||
child: Text(
|
child: Text(
|
||||||
'ID: $id\nGS:(${gs.dx.toInt()},${gs.dy.toInt()})\nSS:(${ss.dx.toInt()},${ss.dy.toInt()})',
|
'ID: ${id.substring(0, 4)}\nGS:(${gs.dx.toInt()},${gs.dy.toInt()})\nSS:(${ss.dx.toInt()},${ss.dy.toInt()})',
|
||||||
style: monospaceStyle,
|
style: monospaceStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,3 +25,7 @@ class ChildInfo {
|
|||||||
enum ScalingMode { resetScale, keepScale, fitInViewport }
|
enum ScalingMode { resetScale, keepScale, fitInViewport }
|
||||||
|
|
||||||
typedef CanvasChildId = String;
|
typedef CanvasChildId = String;
|
||||||
|
|
||||||
|
// listener callbacks
|
||||||
|
typedef OnWidgetEnteredRender = void Function(CanvasChildId id);
|
||||||
|
typedef OnWidgetExitedRender = void Function(CanvasChildId id);
|
||||||
|
|||||||
Reference in New Issue
Block a user