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
+55 -10
View File
@@ -26,6 +26,7 @@ class CanvasController with ChangeNotifier {
bool _init = false;
late final SpatialHashing<int> _spatialHash;
List<ChildInfo> _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<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';
/// 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<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
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,
),
),