add animation on focus and center

This commit is contained in:
2025-07-07 21:04:31 +00:00
parent 10bb69a15f
commit 5d7152ee05
3 changed files with 84 additions and 22 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ class _AppState extends State<App> {
body: Stack( body: Stack(
children: [ children: [
CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)), CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)),
Positioned(bottom: 16, left: 16, child: Fps()), Positioned(bottom: 64, left: 16, child: Fps()),
], ],
), ),
); );
+55 -10
View File
@@ -26,6 +26,7 @@ class CanvasController with ChangeNotifier {
bool _init = false; bool _init = false;
late final SpatialHashing<int> _spatialHash; late final SpatialHashing<int> _spatialHash;
List<ChildInfo> _renderedChildren = []; List<ChildInfo> _renderedChildren = [];
TickerProvider? _ticker;
bool debug; bool debug;
@@ -46,7 +47,7 @@ class CanvasController with ChangeNotifier {
Offset get _gsCenter => ssToGs(_ssCenter, _gsTopLeftOffset, _scale); Offset get _gsCenter => ssToGs(_ssCenter, _gsTopLeftOffset, _scale);
bool get _cleanRenderState => _init && _lastProcessedOffset == _gsTopLeftOffset && _lastProcessedScale == _scale; bool get _cleanRenderState => _init && _lastProcessedOffset == _gsTopLeftOffset && _lastProcessedScale == _scale;
// ==================== Public Functions ==================== // ==================== Callback Functions ====================
/// Update the canvas size when the widget size changes. /// Update the canvas size when the widget size changes.
void onCanvasSizeChange(Size size) { void onCanvasSizeChange(Size size) {
@@ -67,6 +68,10 @@ class CanvasController with ChangeNotifier {
_children[id]!.lastRenderedSize = size; _children[id]!.lastRenderedSize = size;
} }
void setTickerProvider(TickerProvider ticker) {
_ticker = ticker;
}
// ==================== 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.
@@ -176,15 +181,20 @@ class CanvasController with ChangeNotifier {
// ==================== Centering & Focus Functions ==================== // ==================== Centering & Focus Functions ====================
/// Center the canvas so that the given screen-space offset is at the center of the viewport. /// Center the canvas so that the given screen-space offset is at the center of the viewport.
void centerOnScreenOffset(Offset ssOffset) { void centerOnScreenOffset(Offset ssOffset, {bool animate = true}) {
centerOnGridOffset(ssToGs(ssOffset, _gsTopLeftOffset, _scale)); centerOnGridOffset(ssToGs(ssOffset, _gsTopLeftOffset, _scale), animate: animate);
} }
/// Center the canvas so that the given grid-space offset is at the center of the viewport. /// 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 // if 2x scale you need to adjust lesser
_gsTopLeftOffset = gsOffset + (canvasSize * (2 * scale)).toOffset(); final newGsTopLeft = gsOffset + (canvasSize * (2 * scale)).toOffset();
notifyListeners(); 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. /// 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, BuildContext context,
int id, { int id, {
ScalingMode scalingMode = ScalingMode.keepScale, ScalingMode scalingMode = ScalingMode.keepScale,
bool animate = true,
double preferredHorizontalMargin = 16, double preferredHorizontalMargin = 16,
Size? childSize, Size? childSize,
forceRedraw = false, forceRedraw = false,
@@ -218,22 +229,56 @@ class CanvasController with ChangeNotifier {
where c is child size in screen space and m is margin where c is child size in screen space and m is margin
*/ */
double newScale = _scale;
Offset newGsTopLeft = _gsTopLeftOffset;
switch (scalingMode) { switch (scalingMode) {
case ScalingMode.keepScale: case ScalingMode.keepScale:
// do nothing // do nothing
break; break;
case ScalingMode.resetScale: case ScalingMode.resetScale:
_scale = 1; newScale = 1;
case ScalingMode.fitInViewport: case ScalingMode.fitInViewport:
// the scale needs to be determined in this case // 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 // 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; break;
} }
final marginOffset = ((canvasSize.toOffset() - (childSize! * scale).toOffset()) / (2 * scale)).makeAtleast(0); 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<void> 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<Offset>(begin: _gsTopLeftOffset, end: offset);
final scaleTween = Tween<double>(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();
} }
} }
+28 -11
View File
@@ -7,36 +7,53 @@ import 'package:infinite_lazy_2d_grid/utils/styles.dart';
import 'background.dart'; import 'background.dart';
/// An infinite canvas that places all the children at the specified positions. /// An infinite canvas that places all the children at the specified positions.
class CanvasView extends StatelessWidget { class CanvasView extends StatefulWidget {
final CanvasBackground canvasBackground; final CanvasBackground canvasBackground;
final CanvasController controller; final CanvasController controller;
const CanvasView({required this.controller, required this.canvasBackground, super.key}); const CanvasView({required this.controller, required this.canvasBackground, super.key});
@override
State<CanvasView> createState() => _CanvasViewState();
}
class _CanvasViewState extends State<CanvasView> with TickerProviderStateMixin<CanvasView> {
@override
void initState() {
super.initState();
widget.controller.setTickerProvider(this);
}
@override
void dispose() {
widget.controller.dispose(); // for the change notifier
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
onScaleUpdate: controller.onScaleUpdate, onScaleUpdate: widget.controller.onScaleUpdate,
onScaleStart: controller.onScaleStart, onScaleStart: widget.controller.onScaleStart,
child: ListenableBuilder( child: ListenableBuilder(
listenable: controller, listenable: widget.controller,
builder: (_, _) { builder: (_, _) {
final childrenWithPositions = controller.widgetsWithScreenPositions(context); final childrenWithPositions = widget.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 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, childrenIds: childrenIds,
canvasBackground: canvasBackground, canvasBackground: widget.canvasBackground,
ssPositions: ssPositions, ssPositions: ssPositions,
scale: controller.scale, scale: widget.controller.scale,
onCanvasSizeChange: controller.onCanvasSizeChange, onCanvasSizeChange: widget.controller.onCanvasSizeChange,
onChildSizeChange: controller.onChildSizeChange, onChildSizeChange: widget.controller.onChildSizeChange,
children: children, children: children,
); );
if (controller.debug) { if (widget.controller.debug) {
return Stack( return Stack(
children: [ children: [
canvas, canvas,
@@ -44,7 +61,7 @@ class CanvasView extends StatelessWidget {
top: 16, top: 16,
left: 16, left: 16,
child: Text( child: Text(
'Offset: ${controller.offset.coord()}\nScale: ${controller.scale.toStringAsFixed(1)}', 'Offset: ${widget.controller.offset.coord()}\nScale: ${widget.controller.scale.toStringAsFixed(1)}',
style: monospaceStyle, style: monospaceStyle,
), ),
), ),