add centering and focus capabilities
This commit is contained in:
+10
-9
@@ -11,15 +11,21 @@ class App extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AppState extends State<App> {
|
class _AppState extends State<App> {
|
||||||
final CanvasController controller = CanvasController(debug: true, initialScale: 1);
|
final CanvasController controller = CanvasController(debug: true);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
for (int i = 0; i < 10000; i++) {
|
for (int i = 0; i < 10; i++) {
|
||||||
controller.addChild(
|
controller.addChild(
|
||||||
Offset((i % 80) * 100.0, (i ~/ 80) * 100.0),
|
Offset((i % 80) * 100.0, (i ~/ 80) * 100.0),
|
||||||
() => InfoContainer(color: Colors.primaries[i % Colors.primaries.length]),
|
(_) => GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Tapped a container')));
|
||||||
|
controller.focusOnChild(context, i);
|
||||||
|
},
|
||||||
|
child: InfoContainer(color: Colors.primaries[i % Colors.primaries.length]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,11 +67,6 @@ class InfoContainer extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return Container(color: color, width: 50, height: 50);
|
||||||
onTap: () {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Tapped a container')));
|
|
||||||
},
|
|
||||||
child: Container(color: color, width: 50, height: 50),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:infinite_lazy_2d_grid/core/spatial_hashing.dart';
|
import 'package:infinite_lazy_2d_grid/utils/measure_size.dart';
|
||||||
import 'package:infinite_lazy_2d_grid/utils/offset_extensions.dart';
|
import '../spatial_hashing.dart';
|
||||||
|
import '../../utils/offset_extensions.dart';
|
||||||
|
import '../../utils/size_extensions.dart';
|
||||||
import '../../utils/conversions.dart';
|
import '../../utils/conversions.dart';
|
||||||
import '../../utils/styles.dart';
|
import '../../utils/styles.dart';
|
||||||
|
|
||||||
@@ -11,29 +15,26 @@ part 'types.dart';
|
|||||||
class CanvasController with ChangeNotifier {
|
class CanvasController with ChangeNotifier {
|
||||||
int _nextId = 0; // surely we won't run out of IDs, Clueless
|
int _nextId = 0; // surely we won't run out of IDs, Clueless
|
||||||
Offset _gsTopLeftOffset = Offset.zero;
|
Offset _gsTopLeftOffset = Offset.zero;
|
||||||
double _baseScale, _scale;
|
Offset? _lastProcessedOffset;
|
||||||
|
double _baseScale = 1, _scale = 1;
|
||||||
|
double? _lastProcessedScale;
|
||||||
late Size _canvasSize;
|
late Size _canvasSize;
|
||||||
final Map<int, _ChildInfo> _children = {}; // int for IDs
|
final HashMap<int, _ChildInfo> _children = HashMap<int, _ChildInfo>(); // int for IDs
|
||||||
final Offset? _buildCacheExtent;
|
final Offset? _buildCacheExtent;
|
||||||
late Offset _buildExtent;
|
late Offset _buildExtent;
|
||||||
final Size _hashCellSize;
|
final Size _hashCellSize;
|
||||||
bool _init = false;
|
bool _init = false;
|
||||||
late final SpatialHashing<int> _spatialHash;
|
late final SpatialHashing<int> _spatialHash;
|
||||||
|
List<ChildInfo> _renderedChildren = [];
|
||||||
|
|
||||||
bool debug;
|
bool debug;
|
||||||
|
|
||||||
CanvasController({
|
CanvasController({this.debug = false, Offset? buildCacheExtent, Size hashCellSize = const Size(100, 100)})
|
||||||
double initialScale = 1,
|
: _hashCellSize = hashCellSize,
|
||||||
this.debug = false,
|
_buildCacheExtent = buildCacheExtent != null ? buildCacheExtent + Offset(50, 50) : null
|
||||||
Offset? buildCacheExtent,
|
// only top left is considered so if a widget has long width, it'll not be rendered
|
||||||
Size hashCellSize = const Size(100, 100),
|
// unless the cache extent is sufficient
|
||||||
}) : _scale = initialScale,
|
{
|
||||||
_baseScale = initialScale,
|
|
||||||
_hashCellSize = hashCellSize,
|
|
||||||
_buildCacheExtent = buildCacheExtent != null ? buildCacheExtent + Offset(50, 50) : null,
|
|
||||||
// only top left is considered to if a widget has long width, it'll not be rendered
|
|
||||||
// unless the cache extent is sufficient
|
|
||||||
assert(initialScale > 0, 'Initial scale must be greater than 0') {
|
|
||||||
_spatialHash = SpatialHashing<int>(cellSize: _hashCellSize);
|
_spatialHash = SpatialHashing<int>(cellSize: _hashCellSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ class CanvasController with ChangeNotifier {
|
|||||||
Size get canvasSize => _canvasSize;
|
Size get canvasSize => _canvasSize;
|
||||||
Offset get _ssCenter => Offset(_canvasSize.width / 2, _canvasSize.height / 2);
|
Offset get _ssCenter => Offset(_canvasSize.width / 2, _canvasSize.height / 2);
|
||||||
Offset get _gsCenter => ssToGs(_ssCenter, _gsTopLeftOffset, _scale);
|
Offset get _gsCenter => ssToGs(_ssCenter, _gsTopLeftOffset, _scale);
|
||||||
|
bool get _cleanRenderState => _init && _lastProcessedOffset == _gsTopLeftOffset && _lastProcessedScale == _scale;
|
||||||
|
|
||||||
// ==================== Public Functions ====================
|
// ==================== Public Functions ====================
|
||||||
|
|
||||||
@@ -61,6 +63,10 @@ class CanvasController with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onChildSizeChange(int id, Size size) {
|
||||||
|
_children[id]!.lastRenderedSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Child Management ====================
|
// ==================== Child Management ====================
|
||||||
|
|
||||||
/// Add a child at a given position with a builder. Returns the child ID.
|
/// Add a child at a given position with a builder. Returns the child ID.
|
||||||
@@ -140,17 +146,24 @@ class CanvasController with ChangeNotifier {
|
|||||||
// ==================== Positioning Logic ====================
|
// ==================== Positioning Logic ====================
|
||||||
|
|
||||||
/// Currently rendered widgets with their position info
|
/// Currently rendered widgets with their position info
|
||||||
List<ChildInfo> widgetsWithScreenPositions() {
|
List<ChildInfo> widgetsWithScreenPositions(BuildContext context, {bool forceRebuild = false}) {
|
||||||
if (!_init) return [];
|
if (!_init) return [];
|
||||||
|
|
||||||
|
if (_cleanRenderState && !forceRebuild) {
|
||||||
|
return _renderedChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastProcessedOffset = _gsTopLeftOffset;
|
||||||
|
_lastProcessedScale = _scale;
|
||||||
|
|
||||||
final idsToBuild = _childrenWithinBuildArea(_gsCenter, _buildExtent);
|
final idsToBuild = _childrenWithinBuildArea(_gsCenter, _buildExtent);
|
||||||
|
|
||||||
return idsToBuild.map((id) {
|
return _renderedChildren = 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);
|
||||||
var child = item.builder();
|
var child = item.builder(context);
|
||||||
if (debug) child = _Debug(id: id, gs: item.gsPosition, ss: ssPosition, child: child);
|
if (debug) child = _Debug(id: id, gs: item.gsPosition, ss: ssPosition, child: child);
|
||||||
return ChildInfo(gsPosition: item.gsPosition, ssPosition: ssPosition, child: child);
|
return ChildInfo(id: id, gsPosition: item.gsPosition, ssPosition: ssPosition, child: child);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,4 +172,68 @@ class CanvasController with ChangeNotifier {
|
|||||||
final items = _spatialHash.getPointsAround(center.toPoint(), halfExtent);
|
final items = _spatialHash.getPointsAround(center.toPoint(), halfExtent);
|
||||||
return items.map((item) => item.data).toList(); // data is the child id here
|
return items.map((item) => item.data).toList(); // data is the child id here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Centering & Focus Functions ====================
|
||||||
|
|
||||||
|
/// Center the canvas so that the given screen-space offset is at the center of the viewport.
|
||||||
|
void centerOnScreenOffset(Offset ssOffset) {
|
||||||
|
centerOnGridOffset(ssToGs(ssOffset, _gsTopLeftOffset, _scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Center the canvas so that the given grid-space offset is at the center of the viewport.
|
||||||
|
void centerOnGridOffset(Offset gsOffset) {
|
||||||
|
// if 2x scale you need to adjust lesser
|
||||||
|
_gsTopLeftOffset = gsOffset + (canvasSize * (2 * scale)).toOffset();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Focus the viewport on a child by its ID, with a margin in screen-space.
|
||||||
|
/// If it's already rendered, size will be picked up from the child widget. If not
|
||||||
|
/// an offstage rendering will be used ( double render )
|
||||||
|
/// Preferred horizontal margin used for [ScalingMode.fitInViewport].
|
||||||
|
void focusOnChild(
|
||||||
|
BuildContext context,
|
||||||
|
int id, {
|
||||||
|
ScalingMode scalingMode = ScalingMode.keepScale,
|
||||||
|
double preferredHorizontalMargin = 16,
|
||||||
|
Size? childSize,
|
||||||
|
forceRedraw = false,
|
||||||
|
}) {
|
||||||
|
if (!_children.containsKey(id)) {
|
||||||
|
throw _ChildNotFoundException;
|
||||||
|
}
|
||||||
|
|
||||||
|
final childInfo = _children[id]!;
|
||||||
|
|
||||||
|
// try to figure out the size, take from render cache if available
|
||||||
|
// else do an offstage render
|
||||||
|
childSize ??= childInfo.lastRenderedSize != null && !forceRedraw
|
||||||
|
? childInfo.lastRenderedSize
|
||||||
|
: measureWidgetSize(context, childInfo.builder);
|
||||||
|
|
||||||
|
/*
|
||||||
|
margin is symmatric on ltrb so
|
||||||
|
2mx + cx = screenWidth
|
||||||
|
2my + cy = screenHeight
|
||||||
|
where c is child size in screen space and m is margin
|
||||||
|
*/
|
||||||
|
|
||||||
|
switch (scalingMode) {
|
||||||
|
case ScalingMode.keepScale:
|
||||||
|
// do nothing
|
||||||
|
break;
|
||||||
|
case ScalingMode.resetScale:
|
||||||
|
_scale = 1;
|
||||||
|
case ScalingMode.fitInViewport:
|
||||||
|
// the scale needs to be determined in this case
|
||||||
|
// and hence a margin is needed to constrain on x, to get the scale, we then center it along y
|
||||||
|
_scale = (canvasSize.width - 2 * preferredHorizontalMargin) / childSize!.width;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final marginOffset = ((canvasSize.toOffset() - (childSize! * scale).toOffset()) / (2 * scale)).makeAtleast(0);
|
||||||
|
_gsTopLeftOffset = childInfo.gsPosition - marginOffset;
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
part of 'controller.dart';
|
part of 'controller.dart';
|
||||||
|
|
||||||
typedef WidgetBuilder = Widget Function();
|
|
||||||
|
|
||||||
// ------------------------------ Private Types ------------------------------
|
// ------------------------------ Private Types ------------------------------
|
||||||
|
|
||||||
// some simple exceptions
|
// some simple exceptions
|
||||||
@@ -10,14 +8,18 @@ final _ChildNotFoundException = Exception('Child with the given ID does not exis
|
|||||||
|
|
||||||
class _ChildInfo {
|
class _ChildInfo {
|
||||||
Offset gsPosition;
|
Offset gsPosition;
|
||||||
|
Size? lastRenderedSize;
|
||||||
final WidgetBuilder builder;
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
_ChildInfo({required this.gsPosition, required this.builder});
|
_ChildInfo({required this.gsPosition, required this.builder});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChildInfo {
|
class ChildInfo {
|
||||||
|
int id;
|
||||||
Offset gsPosition;
|
Offset gsPosition;
|
||||||
Offset ssPosition;
|
Offset ssPosition;
|
||||||
Widget child;
|
Widget child;
|
||||||
ChildInfo({required this.gsPosition, required this.ssPosition, required this.child});
|
ChildInfo({required this.id, required this.gsPosition, required this.ssPosition, required this.child});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ScalingMode { resetScale, keepScale, fitInViewport }
|
||||||
|
|||||||
+40
-8
@@ -22,14 +22,17 @@ class CanvasView extends StatelessWidget {
|
|||||||
child: ListenableBuilder(
|
child: ListenableBuilder(
|
||||||
listenable: controller,
|
listenable: controller,
|
||||||
builder: (_, _) {
|
builder: (_, _) {
|
||||||
final childrenWithPositions = controller.widgetsWithScreenPositions();
|
final childrenWithPositions = controller.widgetsWithScreenPositions(context);
|
||||||
final ssPositions = childrenWithPositions.map((e) => e.ssPosition).toList();
|
final ssPositions = childrenWithPositions.map((e) => e.ssPosition).toList();
|
||||||
|
final childrenIds = childrenWithPositions.map((e) => e.id).toList();
|
||||||
final children = childrenWithPositions.map((e) => e.child).toList();
|
final children = childrenWithPositions.map((e) => e.child).toList();
|
||||||
final canvas = _CanvasRenderObject(
|
final canvas = _CanvasRenderObject(
|
||||||
|
childrenIds: childrenIds,
|
||||||
canvasBackground: canvasBackground,
|
canvasBackground: canvasBackground,
|
||||||
ssPositions: ssPositions,
|
ssPositions: ssPositions,
|
||||||
scale: controller.scale,
|
scale: controller.scale,
|
||||||
onCanvasSizeChange: controller.onCanvasSizeChange,
|
onCanvasSizeChange: controller.onCanvasSizeChange,
|
||||||
|
onChildSizeChange: controller.onChildSizeChange,
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -59,16 +62,20 @@ class CanvasView extends StatelessWidget {
|
|||||||
/// A combined widget for all the render object of the children + background.
|
/// A combined widget for all the render object of the children + background.
|
||||||
/// Everything is in screen space here
|
/// Everything is in screen space here
|
||||||
class _CanvasRenderObject extends MultiChildRenderObjectWidget {
|
class _CanvasRenderObject extends MultiChildRenderObjectWidget {
|
||||||
|
final List<int> childrenIds;
|
||||||
final List<Offset> ssPositions;
|
final List<Offset> ssPositions;
|
||||||
final double scale;
|
final double scale;
|
||||||
final CanvasBackground canvasBackground;
|
final CanvasBackground canvasBackground;
|
||||||
final Function onCanvasSizeChange;
|
final Function onCanvasSizeChange;
|
||||||
|
final Function onChildSizeChange;
|
||||||
|
|
||||||
const _CanvasRenderObject({
|
const _CanvasRenderObject({
|
||||||
|
required this.childrenIds,
|
||||||
required this.ssPositions,
|
required this.ssPositions,
|
||||||
required this.scale,
|
required this.scale,
|
||||||
required this.canvasBackground,
|
required this.canvasBackground,
|
||||||
required this.onCanvasSizeChange,
|
required this.onCanvasSizeChange,
|
||||||
|
required this.onChildSizeChange,
|
||||||
required super.children, // children go to the MultiChildRenderObjectWidget
|
required super.children, // children go to the MultiChildRenderObjectWidget
|
||||||
}) : assert(ssPositions.length == children.length, 'Children and positions must have the same length'),
|
}) : assert(ssPositions.length == children.length, 'Children and positions must have the same length'),
|
||||||
assert(scale != 0);
|
assert(scale != 0);
|
||||||
@@ -76,16 +83,19 @@ class _CanvasRenderObject extends MultiChildRenderObjectWidget {
|
|||||||
@override
|
@override
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
return _CanvasRenderBox(
|
return _CanvasRenderBox(
|
||||||
|
childrenIds: childrenIds,
|
||||||
ssPositions: ssPositions,
|
ssPositions: ssPositions,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
canvasBackground: canvasBackground,
|
canvasBackground: canvasBackground,
|
||||||
onCanvasSizeChange: onCanvasSizeChange,
|
onCanvasSizeChange: onCanvasSizeChange,
|
||||||
|
onChildSizeChange: onChildSizeChange,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) {
|
void updateRenderObject(BuildContext context, _CanvasRenderBox renderObject) {
|
||||||
renderObject
|
renderObject
|
||||||
|
..childrenIds = childrenIds
|
||||||
..ssPositions = ssPositions
|
..ssPositions = ssPositions
|
||||||
..canvasBackground = canvasBackground
|
..canvasBackground = canvasBackground
|
||||||
..scale = scale;
|
..scale = scale;
|
||||||
@@ -94,6 +104,7 @@ class _CanvasRenderObject extends MultiChildRenderObjectWidget {
|
|||||||
|
|
||||||
class _CanvasWidgetParentData extends ContainerBoxParentData<RenderBox> {
|
class _CanvasWidgetParentData extends ContainerBoxParentData<RenderBox> {
|
||||||
// there is already an "offset" defined in BoxParentdata that is exactly what I want
|
// there is already an "offset" defined in BoxParentdata that is exactly what I want
|
||||||
|
late int id;
|
||||||
late double scale; // scale is same for all rn but this makes it trivial to expand to children with diff scales
|
late double scale; // scale is same for all rn but this makes it trivial to expand to children with diff scales
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,14 +113,24 @@ class _CanvasRenderBox extends RenderBox
|
|||||||
ContainerRenderObjectMixin<RenderBox, _CanvasWidgetParentData>,
|
ContainerRenderObjectMixin<RenderBox, _CanvasWidgetParentData>,
|
||||||
RenderBoxContainerDefaultsMixin<RenderBox, _CanvasWidgetParentData> {
|
RenderBoxContainerDefaultsMixin<RenderBox, _CanvasWidgetParentData> {
|
||||||
CanvasBackground _canvasBackground;
|
CanvasBackground _canvasBackground;
|
||||||
|
List<int> _childrenIds;
|
||||||
List<Offset> _ssPositions;
|
List<Offset> _ssPositions;
|
||||||
double _scale;
|
double _scale;
|
||||||
Function onCanvasSizeChange;
|
Function onCanvasSizeChange;
|
||||||
|
Function onChildSizeChange;
|
||||||
|
|
||||||
_CanvasRenderBox({required ssPositions, required scale, required canvasBackground, required this.onCanvasSizeChange})
|
_CanvasRenderBox({
|
||||||
: _ssPositions = ssPositions,
|
required childrenIds,
|
||||||
_scale = scale,
|
required ssPositions,
|
||||||
_canvasBackground = canvasBackground;
|
required scale,
|
||||||
|
required canvasBackground,
|
||||||
|
required this.onCanvasSizeChange,
|
||||||
|
required this.onChildSizeChange,
|
||||||
|
}) : assert(childrenIds.length == ssPositions.length),
|
||||||
|
_childrenIds = childrenIds,
|
||||||
|
_ssPositions = ssPositions,
|
||||||
|
_scale = scale,
|
||||||
|
_canvasBackground = canvasBackground;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void setupParentData(RenderBox child) {
|
void setupParentData(RenderBox child) {
|
||||||
@@ -128,6 +149,12 @@ class _CanvasRenderBox extends RenderBox
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set childrenIds(List<int> childrenIds) {
|
||||||
|
if (_childrenIds != childrenIds) {
|
||||||
|
_childrenIds = childrenIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set scale(double scale) {
|
set scale(double scale) {
|
||||||
if (_scale != scale) {
|
if (_scale != scale) {
|
||||||
_scale = scale;
|
_scale = scale;
|
||||||
@@ -153,11 +180,16 @@ class _CanvasRenderBox extends RenderBox
|
|||||||
int index = 0;
|
int index = 0;
|
||||||
while (child != null) {
|
while (child != null) {
|
||||||
final _CanvasWidgetParentData childParentData = child.parentData! as _CanvasWidgetParentData;
|
final _CanvasWidgetParentData childParentData = child.parentData! as _CanvasWidgetParentData;
|
||||||
childParentData.offset = _ssPositions[index++];
|
childParentData.offset = _ssPositions[index];
|
||||||
|
childParentData.id = _childrenIds[index];
|
||||||
childParentData.scale = _scale;
|
childParentData.scale = _scale;
|
||||||
|
index++;
|
||||||
// loosen so like a stack can take it's own size inside parent
|
// loosen so like a stack can take it's own size inside parent
|
||||||
// like a stack parent is not using size and is always expanded hence the second arg
|
child.layout(constraints.loosen(), parentUsesSize: true);
|
||||||
child.layout(constraints.loosen(), parentUsesSize: false);
|
|
||||||
|
// notify the controller about the size of the child
|
||||||
|
onChildSizeChange(childParentData.id, child.size);
|
||||||
|
|
||||||
child = childParentData.nextSibling;
|
child = childParentData.nextSibling;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Utility to synchronously measure the size of a widget by building it offscreen in an Overlay.
|
||||||
|
/// Only works for widgets with deterministic, constraint-based sizing (no async/layout dependencies).
|
||||||
|
Size measureWidgetSize(BuildContext context, WidgetBuilder builder, {BoxConstraints? constraints}) {
|
||||||
|
final key = GlobalKey();
|
||||||
|
final overlay = Overlay.of(context);
|
||||||
|
Size? measuredSize;
|
||||||
|
|
||||||
|
final widget = Offstage(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: constraints ?? const BoxConstraints(),
|
||||||
|
child: Container(key: key, child: builder(context)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final entry = OverlayEntry(builder: (_) => widget);
|
||||||
|
overlay.insert(entry);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.handleDrawFrame();
|
||||||
|
|
||||||
|
final ctx = key.currentContext;
|
||||||
|
if (ctx != null) {
|
||||||
|
measuredSize = (ctx.findRenderObject() as RenderBox).size;
|
||||||
|
}
|
||||||
|
entry.remove();
|
||||||
|
return measuredSize ?? Size.zero;
|
||||||
|
}
|
||||||
@@ -8,10 +8,29 @@ extension OffsetCartesionOps on Offset {
|
|||||||
return dx * dx + dy * dy;
|
return dx * dx + dy * dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Offset operator +(Offset other) {
|
||||||
|
return Offset(dx + other.dx, dy + other.dy);
|
||||||
|
}
|
||||||
|
|
||||||
Offset operator -(Offset other) {
|
Offset operator -(Offset other) {
|
||||||
return Offset(dx - other.dx, dy - other.dy);
|
return Offset(dx - other.dx, dy - other.dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Offset operator *(double scalar) {
|
||||||
|
return Offset(dx * scalar, dy * scalar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset makeAtleast(double value) {
|
||||||
|
return Offset(dx < value ? value : dx, dy < value ? value : dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset operator /(double scalar) {
|
||||||
|
if (scalar == 0) {
|
||||||
|
throw ArgumentError('Division by zero is not allowed.');
|
||||||
|
}
|
||||||
|
return Offset(dx / scalar, dy / scalar);
|
||||||
|
}
|
||||||
|
|
||||||
Point toPoint() {
|
Point toPoint() {
|
||||||
return Point(dx, dy);
|
return Point(dx, dy);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension SizeOps on Size {
|
||||||
|
Size operator /(double scalar) {
|
||||||
|
if (scalar == 0) {
|
||||||
|
throw ArgumentError('Division by zero is not allowed.');
|
||||||
|
}
|
||||||
|
return Size(width / scalar, height / scalar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Size operator *(double scalar) {
|
||||||
|
return Size(width * scalar, height * scalar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset toOffset() {
|
||||||
|
return Offset(width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,9 +15,9 @@ class TestChild extends StatelessWidget {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('CanvasView renders only visible children and reduces count on zoom out', (WidgetTester tester) async {
|
testWidgets('CanvasView renders only visible children and reduces count on zoom out', (WidgetTester tester) async {
|
||||||
final controller = CanvasController(debug: true, initialScale: 1);
|
final controller = CanvasController(debug: true);
|
||||||
for (int i = 0; i < 10000; i++) {
|
for (int i = 0; i < 10000; i++) {
|
||||||
controller.addChild(Offset((i % 80) * 100.0, (i ~/ 80) * 100.0), () => TestChild(index: i));
|
controller.addChild(Offset((i % 80) * 100.0, (i ~/ 80) * 100.0), (_) => TestChild(index: i));
|
||||||
}
|
}
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
@@ -46,9 +46,9 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('CanvasView scales as expected', (WidgetTester tester) async {
|
testWidgets('CanvasView scales as expected', (WidgetTester tester) async {
|
||||||
final controller = CanvasController(debug: true, initialScale: 1);
|
final controller = CanvasController(debug: true);
|
||||||
controller.addChild(const Offset(0, 0), () => TestChild(index: 0));
|
controller.addChild(const Offset(0, 0), (_) => TestChild(index: 0));
|
||||||
controller.addChild(const Offset(100, 100), () => TestChild(index: 1));
|
controller.addChild(const Offset(100, 100), (_) => TestChild(index: 1));
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
@@ -57,6 +57,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
final context = tester.element(find.byType(CanvasView));
|
||||||
|
|
||||||
// Initial size check
|
// Initial size check
|
||||||
final finder = find.byType(TestChild);
|
final finder = find.byType(TestChild);
|
||||||
@@ -67,7 +68,7 @@ void main() {
|
|||||||
controller.onScaleUpdate(ScaleUpdateDetails(focalPoint: const Offset(0, 0), scale: 2.0));
|
controller.onScaleUpdate(ScaleUpdateDetails(focalPoint: const Offset(0, 0), scale: 2.0));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(controller.scale, 2.0);
|
expect(controller.scale, 2.0);
|
||||||
final ssPositions = controller.widgetsWithScreenPositions().map((e) => e.ssPosition).toList();
|
final ssPositions = controller.widgetsWithScreenPositions(context).map((e) => e.ssPosition).toList();
|
||||||
expect(ssPositions, [Offset.zero, const Offset(200, 200)]);
|
expect(ssPositions, [Offset.zero, const Offset(200, 200)]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user