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