diff --git a/packages/flet/lib/src/controls/interactive_viewer.dart b/packages/flet/lib/src/controls/interactive_viewer.dart index 9ea1734b0..446a6caa9 100644 --- a/packages/flet/lib/src/controls/interactive_viewer.dart +++ b/packages/flet/lib/src/controls/interactive_viewer.dart @@ -6,11 +6,13 @@ import '../flet_control_backend.dart'; import '../models/control.dart'; import '../utils/alignment.dart'; import '../utils/edge_insets.dart'; +import '../utils/numbers.dart'; import '../utils/others.dart'; +import '../utils/time.dart'; import 'create_control.dart'; import 'error.dart'; -class InteractiveViewerControl extends StatelessWidget { +class InteractiveViewerControl extends StatefulWidget { final Control? parent; final Control control; final List children; @@ -27,35 +29,110 @@ class InteractiveViewerControl extends StatelessWidget { required this.parentAdaptive, required this.backend}); + @override + State createState() => + _InteractiveViewerControlState(); +} + +class _InteractiveViewerControlState extends State + with SingleTickerProviderStateMixin { + final TransformationController _transformationController = + TransformationController(); + late AnimationController _animationController; + Animation? _animation; + Matrix4? _savedMatrix; + + @override + void initState() { + super.initState(); + _animationController = + AnimationController(vsync: this, duration: Duration.zero); + + widget.backend.subscribeMethods(widget.control.id, + (methodName, args) async { + switch (methodName) { + case "zoom": + var factor = parseDouble(args["factor"]); + if (factor != null) { + _transformationController.value = + _transformationController.value.scaled(factor, factor); + } + break; + case "pan": + var dx = parseDouble(args["dx"]); + var dy = parseDouble(args["dy"]); + if (dx != null && dy != null) { + _transformationController.value = + _transformationController.value.clone()..translate(dx, dy); + } + break; + case "reset": + var duration = durationFromString(args["duration"]); + if (duration == null) { + _transformationController.value = Matrix4.identity(); + } else { + _animationController.duration = duration; + _animation = Matrix4Tween( + begin: _transformationController.value, + end: Matrix4.identity(), + ).animate(_animationController) + ..addListener(() { + _transformationController.value = _animation!.value; + }); + _animationController.forward(from: 0); + } + break; + case "save_state": + _savedMatrix = _transformationController.value.clone(); + break; + case "restore_state": + if (_savedMatrix != null) { + _transformationController.value = _savedMatrix!; + } + break; + } + return null; + }); + } + + @override + void dispose() { + _transformationController.dispose(); + _animationController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - debugPrint("InteractiveViewer build: ${control.id}"); + debugPrint("InteractiveViewer build: ${widget.control.id}"); - var contentCtrls = children.where((c) => c.isVisible); - bool? adaptive = control.isAdaptive ?? parentAdaptive; - bool disabled = control.isDisabled || parentDisabled; + var contentCtrls = widget.children.where((c) => c.isVisible); + bool? adaptive = widget.control.isAdaptive ?? widget.parentAdaptive; + bool disabled = widget.control.isDisabled || widget.parentDisabled; var interactiveViewer = InteractiveViewer( - panEnabled: control.attrBool("panEnabled", true)!, - scaleEnabled: control.attrBool("scaleEnabled", true)!, + transformationController: _transformationController, + panEnabled: widget.control.attrBool("panEnabled", true)!, + scaleEnabled: widget.control.attrBool("scaleEnabled", true)!, trackpadScrollCausesScale: - control.attrBool("trackpadScrollCausesScale", false)!, - constrained: control.attrBool("constrained", true)!, - maxScale: control.attrDouble("maxScale", 2.5)!, - minScale: control.attrDouble("minScale", 0.8)!, - interactionEndFrictionCoefficient: - control.attrDouble("interactionEndFrictionCoefficient", 0.0000135)!, - scaleFactor: control.attrDouble("scaleFactor", 200)!, + widget.control.attrBool("trackpadScrollCausesScale", false)!, + constrained: widget.control.attrBool("constrained", true)!, + maxScale: widget.control.attrDouble("maxScale", 2.5)!, + minScale: widget.control.attrDouble("minScale", 0.8)!, + interactionEndFrictionCoefficient: widget.control + .attrDouble("interactionEndFrictionCoefficient", 0.0000135)!, + scaleFactor: widget.control.attrDouble("scaleFactor", 200)!, clipBehavior: - parseClip(control.attrString("clipBehavior"), Clip.hardEdge)!, - alignment: parseAlignment(control, "alignment"), + parseClip(widget.control.attrString("clipBehavior"), Clip.hardEdge)!, + alignment: parseAlignment(widget.control, "alignment"), boundaryMargin: - parseEdgeInsets(control, "boundaryMargin", EdgeInsets.zero)!, + parseEdgeInsets(widget.control, "boundaryMargin", EdgeInsets.zero)!, onInteractionStart: !disabled ? (ScaleStartDetails details) { - debugPrint("InteractiveViewer ${control.id} onInteractionStart"); - backend.triggerControlEvent( - control.id, + debugPrint( + "InteractiveViewer ${widget.control.id} onInteractionStart"); + widget.backend.triggerControlEvent( + widget.control.id, "interaction_start", jsonEncode({ "pc": details.pointerCount, @@ -68,9 +145,10 @@ class InteractiveViewerControl extends StatelessWidget { : null, onInteractionEnd: !disabled ? (ScaleEndDetails details) { - debugPrint("InteractiveViewer ${control.id} onInteractionEnd"); - backend.triggerControlEvent( - control.id, + debugPrint( + "InteractiveViewer ${widget.control.id} onInteractionEnd"); + widget.backend.triggerControlEvent( + widget.control.id, "interaction_end", jsonEncode({ "pc": details.pointerCount, @@ -80,9 +158,10 @@ class InteractiveViewerControl extends StatelessWidget { : null, onInteractionUpdate: !disabled ? (ScaleUpdateDetails details) { - debugPrint("InteractiveViewer ${control.id} onInteractionUpdate"); - backend.triggerControlEvent( - control.id, + debugPrint( + "InteractiveViewer ${widget.control.id} onInteractionUpdate"); + widget.backend.triggerControlEvent( + widget.control.id, "interaction_update", jsonEncode({ "pc": details.pointerCount, @@ -98,11 +177,13 @@ class InteractiveViewerControl extends StatelessWidget { } : null, child: contentCtrls.isNotEmpty - ? createControl(control, contentCtrls.first.id, disabled, + ? createControl(widget.control, contentCtrls.first.id, disabled, parentAdaptive: adaptive) : const ErrorControl( "InteractiveViewer.content must be provided and visible"), ); - return constrainedControl(context, interactiveViewer, parent, control); + + return constrainedControl( + context, interactiveViewer, widget.parent, widget.control); } } diff --git a/packages/flet/lib/src/utils/time.dart b/packages/flet/lib/src/utils/time.dart index 5d34a81d5..57e69d23c 100644 --- a/packages/flet/lib/src/utils/time.dart +++ b/packages/flet/lib/src/utils/time.dart @@ -29,3 +29,9 @@ Duration? durationFromJSON(dynamic json, [Duration? defaultValue]) { milliseconds: parseInt(json["milliseconds"], 0)!, microseconds: parseInt(json["microseconds"], 0)!); } + +Duration? durationFromString(String? duration, [Duration? defaultValue]) { + return duration != null + ? durationFromJSON(json.decode(duration), defaultValue) + : defaultValue; +} \ No newline at end of file diff --git a/packages/flet_map/lib/src/map.dart b/packages/flet_map/lib/src/map.dart index 19a40b5e2..f82db9b75 100644 --- a/packages/flet_map/lib/src/map.dart +++ b/packages/flet_map/lib/src/map.dart @@ -37,12 +37,6 @@ class _MapControlState extends State super.dispose(); } - Duration? durationFromString(String? duration, [Duration? defaultValue]) { - return duration != null - ? durationFromJSON(json.decode(duration), defaultValue) - : defaultValue; - } - @override Widget build(BuildContext context) { debugPrint("Map build: ${widget.control.id} (${widget.control.hashCode})"); diff --git a/sdk/python/packages/flet/src/flet/core/interactive_viewer.py b/sdk/python/packages/flet/src/flet/core/interactive_viewer.py index 4f806dc88..c56cb682e 100644 --- a/sdk/python/packages/flet/src/flet/core/interactive_viewer.py +++ b/sdk/python/packages/flet/src/flet/core/interactive_viewer.py @@ -11,10 +11,19 @@ from flet.core.event_handler import EventHandler from flet.core.ref import Ref from flet.core.tooltip import TooltipValue -from flet.core.types import (ClipBehavior, MarginValue, Offset, OffsetValue, - OptionalControlEventCallable, - OptionalEventCallable, ResponsiveNumber, - RotateValue, ScaleValue) +from flet.core.types import ( + ClipBehavior, + DurationValue, + MarginValue, + Number, + Offset, + OffsetValue, + OptionalControlEventCallable, + OptionalEventCallable, + ResponsiveNumber, + RotateValue, + ScaleValue, +) class InteractiveViewerInteractionStartEvent(ControlEvent): @@ -193,6 +202,23 @@ def before_update(self): def _get_children(self): return [self.__content] + def reset(self, animation_duration: Optional[DurationValue] = None): + self.invoke_method( + "reset", arguments={"duration": self._convert_attr_json(animation_duration)} + ) + + def save_state(self): + self.invoke_method("save_state") + + def restore_state(self): + self.invoke_method("restore_state") + + def zoom(self, factor: Number): + self.invoke_method("zoom", arguments={"factor": str(factor)}) + + def pan(self, dx: Number, dy: Number): + self.invoke_method("pan", arguments={"dx": str(dx), "dy": str(dy)}) + # min_scale @property def min_scale(self) -> float: diff --git a/sdk/python/packages/flet/src/flet/core/page.py b/sdk/python/packages/flet/src/flet/core/page.py index 3714b7d02..0714f07e2 100644 --- a/sdk/python/packages/flet/src/flet/core/page.py +++ b/sdk/python/packages/flet/src/flet/core/page.py @@ -650,7 +650,7 @@ def convert_page_media_change_event(e): _session_page.set(self) - def get_control(self, id: int) -> Control: + def get_control(self, id: str) -> Control: return self._index.get(id) def before_update(self) -> None: diff --git a/sdk/python/poetry.lock b/sdk/python/poetry.lock index 2e72bbc0d..1ec134ca0 100644 --- a/sdk/python/poetry.lock +++ b/sdk/python/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "altgraph"