diff --git a/example/lib/app.dart b/example/lib/app.dart index 1d338a6..129869b 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -54,7 +54,7 @@ class _AppState extends State { body: Stack( children: [ CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)), - Positioned(bottom: 16, left: 16, child: Fps()), + Positioned(bottom: 64, left: 16, child: Fps()), ], ), ); diff --git a/lib/core/controller/controller.dart b/lib/core/controller/controller.dart index 444462f..de730e6 100644 --- a/lib/core/controller/controller.dart +++ b/lib/core/controller/controller.dart @@ -26,6 +26,7 @@ class CanvasController with ChangeNotifier { bool _init = false; late final SpatialHashing _spatialHash; List _renderedChildren = []; + TickerProvider? _ticker; bool debug; @@ -46,7 +47,7 @@ class CanvasController with ChangeNotifier { Offset get _gsCenter => ssToGs(_ssCenter, _gsTopLeftOffset, _scale); bool get _cleanRenderState => _init && _lastProcessedOffset == _gsTopLeftOffset && _lastProcessedScale == _scale; - // ==================== Public Functions ==================== + // ==================== Callback Functions ==================== /// Update the canvas size when the widget size changes. void onCanvasSizeChange(Size size) { @@ -67,6 +68,10 @@ class CanvasController with ChangeNotifier { _children[id]!.lastRenderedSize = size; } + void setTickerProvider(TickerProvider ticker) { + _ticker = ticker; + } + // ==================== Child Management ==================== /// Add a child at a given position with a builder. Returns the child ID. @@ -176,15 +181,20 @@ class CanvasController with ChangeNotifier { // ==================== 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)); + void centerOnScreenOffset(Offset ssOffset, {bool animate = true}) { + centerOnGridOffset(ssToGs(ssOffset, _gsTopLeftOffset, _scale), animate: animate); } /// Center the canvas so that the given grid-space offset is at the center of the viewport. - void centerOnGridOffset(Offset gsOffset) { + void centerOnGridOffset(Offset gsOffset, {bool animate = true}) { // if 2x scale you need to adjust lesser - _gsTopLeftOffset = gsOffset + (canvasSize * (2 * scale)).toOffset(); - notifyListeners(); + final newGsTopLeft = gsOffset + (canvasSize * (2 * scale)).toOffset(); + if (animate) { + animateToOffsetAndScale(offset: newGsTopLeft, scale: _scale); + } else { + _gsTopLeftOffset = newGsTopLeft; + notifyListeners(); + } } /// Focus the viewport on a child by its ID, with a margin in screen-space. @@ -195,6 +205,7 @@ class CanvasController with ChangeNotifier { BuildContext context, int id, { ScalingMode scalingMode = ScalingMode.keepScale, + bool animate = true, double preferredHorizontalMargin = 16, Size? childSize, forceRedraw = false, @@ -218,22 +229,56 @@ class CanvasController with ChangeNotifier { where c is child size in screen space and m is margin */ + double newScale = _scale; + Offset newGsTopLeft = _gsTopLeftOffset; + switch (scalingMode) { case ScalingMode.keepScale: // do nothing break; case ScalingMode.resetScale: - _scale = 1; + newScale = 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; + newScale = (canvasSize.width - 2 * preferredHorizontalMargin) / childSize!.width; break; } final marginOffset = ((canvasSize.toOffset() - (childSize! * scale).toOffset()) / (2 * scale)).makeAtleast(0); - _gsTopLeftOffset = childInfo.gsPosition - marginOffset; + newGsTopLeft = childInfo.gsPosition - marginOffset; - notifyListeners(); + if (animate) { + animateToOffsetAndScale(offset: newGsTopLeft, scale: newScale); // anim will notifyListeners + } else { + _gsTopLeftOffset = newGsTopLeft; + _scale = newScale; + notifyListeners(); + } + } + + // ==================== Animation ==================== + + Future animateToOffsetAndScale({ + required Offset offset, + required double scale, + Duration duration = const Duration(milliseconds: 300), + Curve curve = Curves.easeInOut, + }) async { + final anim = AnimationController(vsync: _ticker!, duration: duration); + final offsetTween = Tween(begin: _gsTopLeftOffset, end: offset); + final scaleTween = Tween(begin: _scale, end: scale); + + final offsetAnimation = offsetTween.animate(CurvedAnimation(parent: anim, curve: curve)); + final scaleAnimation = scaleTween.animate(CurvedAnimation(parent: anim, curve: curve)); + + anim.addListener(() { + _gsTopLeftOffset = offsetAnimation.value; + _scale = scaleAnimation.value; + notifyListeners(); + }); + + await anim.forward(); + anim.dispose(); } } diff --git a/lib/core/render.dart b/lib/core/render.dart index dadc7f0..0781bf4 100644 --- a/lib/core/render.dart +++ b/lib/core/render.dart @@ -7,36 +7,53 @@ import 'package:infinite_lazy_2d_grid/utils/styles.dart'; import 'background.dart'; /// An infinite canvas that places all the children at the specified positions. -class CanvasView extends StatelessWidget { +class CanvasView extends StatefulWidget { final CanvasBackground canvasBackground; final CanvasController controller; const CanvasView({required this.controller, required this.canvasBackground, super.key}); + @override + State createState() => _CanvasViewState(); +} + +class _CanvasViewState extends State with TickerProviderStateMixin { + @override + void initState() { + super.initState(); + widget.controller.setTickerProvider(this); + } + + @override + void dispose() { + widget.controller.dispose(); // for the change notifier + super.dispose(); + } + @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.translucent, - onScaleUpdate: controller.onScaleUpdate, - onScaleStart: controller.onScaleStart, + onScaleUpdate: widget.controller.onScaleUpdate, + onScaleStart: widget.controller.onScaleStart, child: ListenableBuilder( - listenable: controller, + listenable: widget.controller, builder: (_, _) { - final childrenWithPositions = controller.widgetsWithScreenPositions(context); + final childrenWithPositions = widget.controller.widgetsWithScreenPositions(context); 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 canvas = _CanvasRenderObject( childrenIds: childrenIds, - canvasBackground: canvasBackground, + canvasBackground: widget.canvasBackground, ssPositions: ssPositions, - scale: controller.scale, - onCanvasSizeChange: controller.onCanvasSizeChange, - onChildSizeChange: controller.onChildSizeChange, + scale: widget.controller.scale, + onCanvasSizeChange: widget.controller.onCanvasSizeChange, + onChildSizeChange: widget.controller.onChildSizeChange, children: children, ); - if (controller.debug) { + if (widget.controller.debug) { return Stack( children: [ canvas, @@ -44,7 +61,7 @@ class CanvasView extends StatelessWidget { top: 16, left: 16, child: Text( - 'Offset: ${controller.offset.coord()}\nScale: ${controller.scale.toStringAsFixed(1)}', + 'Offset: ${widget.controller.offset.coord()}\nScale: ${widget.controller.scale.toStringAsFixed(1)}', style: monospaceStyle, ), ),