Skip to content

Commit e7170da

Browse files
josxhaJaffaKetchup
andauthored
feat: add animations to the controller (#1757)
* add `TickerProvider` to controller * use central `AnimationController` * implement `moveAndRotateAnimated()` * add fling and stop function * register listeners only a single time * clean up * dont rotate if rotation is 0 * clean up * fix reset animation * clean up * add `rotateAnimatedRaw`, `isAnimating` --------- Co-authored-by: Luka S <[email protected]>
1 parent 757cba8 commit e7170da

File tree

2 files changed

+261
-23
lines changed

2 files changed

+261
-23
lines changed

lib/src/map/controller/map_controller_impl.dart

Lines changed: 258 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,25 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
1717

1818
late final MapInteractiveViewerState _interactiveViewerState;
1919

20-
MapControllerImpl([MapOptions? options])
20+
Animation<LatLng>? _moveAnimation;
21+
Animation<double>? _zoomAnimation;
22+
Animation<double>? _rotationAnimation;
23+
Animation<Offset>? _flingAnimation;
24+
late bool _animationHasGesture;
25+
late Offset _animationOffset;
26+
late Point _flingMapCenterStartPoint;
27+
28+
MapControllerImpl({MapOptions? options, TickerProvider? vsync})
2129
: super(
2230
_MapControllerState(
2331
options: options,
2432
camera: options == null ? null : MapCamera.initialCamera(options),
33+
animationController:
34+
vsync == null ? null : AnimationController(vsync: vsync),
2535
),
26-
);
36+
) {
37+
value.animationController?.addListener(_handleAnimation);
38+
}
2739

2840
/// Link the viewer state with the controller. This should be done once when
2941
/// the FlutterMapInteractiveViewerState is initialized.
@@ -50,6 +62,12 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
5062
'least once before using the MapController.'));
5163
}
5264

65+
AnimationController get _animationController {
66+
return value.animationController ??
67+
(throw Exception('You need to have the FlutterMap widget rendered at '
68+
'least once before using the MapController.'));
69+
}
70+
5371
/// This setter should only be called in this class or within tests. Changes
5472
/// to the [_MapControllerState] should be done via methods in this class.
5573
@visibleForTesting
@@ -179,29 +197,27 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
179197
required MapEventSource source,
180198
String? id,
181199
}) {
182-
if (newRotation != camera.rotation) {
183-
final newCamera = options.cameraConstraint.constrain(
184-
camera.withRotation(newRotation),
185-
);
186-
if (newCamera == null) return false;
200+
if (newRotation == camera.rotation) return false;
187201

188-
final oldCamera = camera;
202+
final newCamera = options.cameraConstraint.constrain(
203+
camera.withRotation(newRotation),
204+
);
205+
if (newCamera == null) return false;
189206

190-
// Update camera then emit events and callbacks
191-
value = value.withMapCamera(newCamera);
207+
final oldCamera = camera;
192208

193-
_emitMapEvent(
194-
MapEventRotate(
195-
id: id,
196-
source: source,
197-
oldCamera: oldCamera,
198-
camera: camera,
199-
),
200-
);
201-
return true;
202-
}
209+
// Update camera then emit events and callbacks
210+
value = value.withMapCamera(newCamera);
203211

204-
return false;
212+
_emitMapEvent(
213+
MapEventRotate(
214+
id: id,
215+
source: source,
216+
oldCamera: oldCamera,
217+
camera: camera,
218+
),
219+
);
220+
return true;
205221
}
206222

207223
MoveAndRotateResult rotateAroundPointRaw(
@@ -340,9 +356,23 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
340356
value = _MapControllerState(
341357
options: newOptions,
342358
camera: newCamera,
359+
animationController: value.animationController,
343360
);
344361
}
345362

363+
set vsync(TickerProvider tickerProvider) {
364+
if (value.animationController == null) {
365+
value = _MapControllerState(
366+
options: value.options,
367+
camera: value.camera,
368+
animationController: AnimationController(vsync: tickerProvider)
369+
..addListener(_handleAnimation),
370+
);
371+
} else {
372+
_animationController.resync(tickerProvider);
373+
}
374+
}
375+
346376
/// To be called when a gesture that causes movement starts.
347377
void moveStarted(MapEventSource source) {
348378
_emitMapEvent(
@@ -508,6 +538,161 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
508538
);
509539
}
510540

541+
void moveAndRotateAnimatedRaw(
542+
LatLng newCenter,
543+
double newZoom,
544+
double newRotation, {
545+
required Offset offset,
546+
required Duration duration,
547+
required Curve curve,
548+
required bool hasGesture,
549+
required MapEventSource source,
550+
}) {
551+
if (newRotation == camera.rotation) {
552+
moveAnimatedRaw(
553+
newCenter,
554+
newZoom,
555+
duration: duration,
556+
curve: curve,
557+
hasGesture: hasGesture,
558+
source: source,
559+
);
560+
return;
561+
}
562+
// cancel all ongoing animation
563+
_animationController.stop();
564+
_resetAnimations();
565+
566+
if (newCenter == camera.center && newZoom == camera.zoom) return;
567+
568+
// create the new animation
569+
_moveAnimation = LatLngTween(begin: camera.center, end: newCenter)
570+
.chain(CurveTween(curve: curve))
571+
.animate(_animationController);
572+
_zoomAnimation = Tween<double>(begin: camera.zoom, end: newZoom)
573+
.chain(CurveTween(curve: curve))
574+
.animate(_animationController);
575+
_rotationAnimation = Tween<double>(begin: camera.rotation, end: newRotation)
576+
.chain(CurveTween(curve: curve))
577+
.animate(_animationController);
578+
579+
_animationController.duration = duration;
580+
_animationHasGesture = hasGesture;
581+
_animationOffset = offset;
582+
583+
// start the animation from its start
584+
_animationController.forward(from: 0);
585+
}
586+
587+
void rotateAnimatedRaw(
588+
double newRotation, {
589+
required Offset offset,
590+
required Duration duration,
591+
required Curve curve,
592+
required bool hasGesture,
593+
required MapEventSource source,
594+
}) {
595+
// cancel all ongoing animation
596+
_animationController.stop();
597+
_resetAnimations();
598+
599+
if (newRotation == camera.rotation) return;
600+
601+
// create the new animation
602+
_rotationAnimation = Tween<double>(begin: camera.rotation, end: newRotation)
603+
.chain(CurveTween(curve: curve))
604+
.animate(_animationController);
605+
606+
_animationController.duration = duration;
607+
_animationHasGesture = hasGesture;
608+
_animationOffset = offset;
609+
610+
// start the animation from its start
611+
_animationController.forward(from: 0);
612+
}
613+
614+
void stopAnimationRaw({bool canceled = true}) {
615+
if (isAnimating) _animationController.stop(canceled: canceled);
616+
}
617+
618+
bool get isAnimating => _animationController.isAnimating;
619+
620+
void _resetAnimations() {
621+
_moveAnimation = null;
622+
_rotationAnimation = null;
623+
_zoomAnimation = null;
624+
_flingAnimation = null;
625+
}
626+
627+
void flingAnimatedRaw({
628+
required double velocity,
629+
required Offset direction,
630+
required Offset begin,
631+
Offset offset = Offset.zero,
632+
double mass = 1,
633+
double stiffness = 1000,
634+
double ratio = 5,
635+
required bool hasGesture,
636+
}) {
637+
// cancel all ongoing animation
638+
_animationController.stop();
639+
_resetAnimations();
640+
641+
_animationHasGesture = hasGesture;
642+
_animationOffset = offset;
643+
_flingMapCenterStartPoint = camera.project(camera.center);
644+
645+
final distance =
646+
(Offset.zero & Size(camera.nonRotatedSize.x, camera.nonRotatedSize.y))
647+
.shortestSide;
648+
649+
_flingAnimation = Tween<Offset>(
650+
begin: begin,
651+
end: begin - direction * distance,
652+
).animate(_animationController);
653+
654+
_animationController.value = 0;
655+
_animationController.fling(
656+
velocity: velocity,
657+
springDescription: SpringDescription.withDampingRatio(
658+
mass: mass,
659+
stiffness: stiffness,
660+
ratio: ratio,
661+
),
662+
);
663+
}
664+
665+
void moveAnimatedRaw(
666+
LatLng newCenter,
667+
double newZoom, {
668+
Offset offset = Offset.zero,
669+
required Duration duration,
670+
required Curve curve,
671+
required bool hasGesture,
672+
required MapEventSource source,
673+
}) {
674+
// cancel all ongoing animation
675+
_animationController.stop();
676+
_resetAnimations();
677+
678+
if (newCenter == camera.center && newZoom == camera.zoom) return;
679+
680+
// create the new animation
681+
_moveAnimation = LatLngTween(begin: camera.center, end: newCenter)
682+
.chain(CurveTween(curve: curve))
683+
.animate(_animationController);
684+
_zoomAnimation = Tween<double>(begin: camera.zoom, end: newZoom)
685+
.chain(CurveTween(curve: curve))
686+
.animate(_animationController);
687+
688+
_animationController.duration = duration;
689+
_animationHasGesture = hasGesture;
690+
_animationOffset = offset;
691+
692+
// start the animation from its start
693+
_animationController.forward(from: 0);
694+
}
695+
511696
void _emitMapEvent(MapEvent event) {
512697
if (event.source == MapEventSource.mapController && event is MapEventMove) {
513698
_interactiveViewerState.interruptAnimatedMovement(event);
@@ -518,9 +703,58 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
518703
_mapEventSink.add(event);
519704
}
520705

706+
void _handleAnimation() {
707+
// fling animation
708+
if (_flingAnimation != null) {
709+
final newCenterPoint = _flingMapCenterStartPoint +
710+
_flingAnimation!.value.toPoint().rotate(camera.rotationRad);
711+
moveRaw(
712+
camera.unproject(newCenterPoint),
713+
camera.zoom,
714+
hasGesture: _animationHasGesture,
715+
source: MapEventSource.flingAnimationController,
716+
offset: _animationOffset,
717+
);
718+
return;
719+
}
720+
721+
// animated movement
722+
if (_moveAnimation != null) {
723+
if (_rotationAnimation != null) {
724+
moveAndRotateRaw(
725+
_moveAnimation?.value ?? camera.center,
726+
_zoomAnimation?.value ?? camera.zoom,
727+
_rotationAnimation!.value,
728+
hasGesture: _animationHasGesture,
729+
source: MapEventSource.mapController,
730+
offset: _animationOffset,
731+
);
732+
} else {
733+
moveRaw(
734+
_moveAnimation!.value,
735+
_zoomAnimation?.value ?? camera.zoom,
736+
hasGesture: _animationHasGesture,
737+
source: MapEventSource.mapController,
738+
offset: _animationOffset,
739+
);
740+
}
741+
return;
742+
}
743+
744+
// animated rotation
745+
if (_rotationAnimation != null) {
746+
rotateRaw(
747+
_rotationAnimation!.value,
748+
hasGesture: _animationHasGesture,
749+
source: MapEventSource.mapController,
750+
);
751+
}
752+
}
753+
521754
@override
522755
void dispose() {
523756
_mapEventStreamController.close();
757+
value.animationController?.dispose();
524758
super.dispose();
525759
}
526760
}
@@ -529,14 +763,17 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
529763
class _MapControllerState {
530764
final MapCamera? camera;
531765
final MapOptions? options;
766+
final AnimationController? animationController;
532767

533768
const _MapControllerState({
534769
required this.options,
535770
required this.camera,
771+
required this.animationController,
536772
});
537773

538774
_MapControllerState withMapCamera(MapCamera camera) => _MapControllerState(
539775
options: options,
540776
camera: camera,
777+
animationController: animationController,
541778
);
542779
}

lib/src/map/widget.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class FlutterMap extends StatefulWidget {
5151
}
5252

5353
class _FlutterMapStateContainer extends State<FlutterMap>
54-
with AutomaticKeepAliveClientMixin {
54+
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
5555
bool _initialCameraFitApplied = false;
5656

5757
late MapControllerImpl _mapController;
@@ -184,9 +184,10 @@ class _FlutterMapStateContainer extends State<FlutterMap>
184184

185185
void _setMapController() {
186186
if (_controllerCreatedInternally) {
187-
_mapController = MapControllerImpl(widget.options);
187+
_mapController = MapControllerImpl(options: widget.options, vsync: this);
188188
} else {
189189
_mapController = widget.mapController! as MapControllerImpl;
190+
_mapController.vsync = this;
190191
_mapController.options = widget.options;
191192
}
192193
}

0 commit comments

Comments
 (0)