add animation on focus and center
This commit is contained in:
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user