Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add animations to the controller #1757

Merged
merged 21 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 258 additions & 21 deletions lib/src/map/controller/map_controller_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,25 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>

late final MapInteractiveViewerState _interactiveViewerState;

MapControllerImpl([MapOptions? options])
Animation<LatLng>? _moveAnimation;
Animation<double>? _zoomAnimation;
Animation<double>? _rotationAnimation;
Animation<Offset>? _flingAnimation;
late bool _animationHasGesture;
late Offset _animationOffset;
late Point _flingMapCenterStartPoint;

MapControllerImpl({MapOptions? options, TickerProvider? vsync})
: super(
_MapControllerState(
options: options,
camera: options == null ? null : MapCamera.initialCamera(options),
animationController:
vsync == null ? null : AnimationController(vsync: vsync),
),
);
) {
value.animationController?.addListener(_handleAnimation);
}

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

AnimationController get _animationController {
return value.animationController ??
(throw Exception('You need to have the FlutterMap widget rendered at '
'least once before using the MapController.'));
}

/// This setter should only be called in this class or within tests. Changes
/// to the [_MapControllerState] should be done via methods in this class.
@visibleForTesting
Expand Down Expand Up @@ -179,29 +197,27 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
required MapEventSource source,
String? id,
}) {
if (newRotation != camera.rotation) {
final newCamera = options.cameraConstraint.constrain(
camera.withRotation(newRotation),
);
if (newCamera == null) return false;
if (newRotation == camera.rotation) return false;

final oldCamera = camera;
final newCamera = options.cameraConstraint.constrain(
camera.withRotation(newRotation),
);
if (newCamera == null) return false;

// Update camera then emit events and callbacks
value = value.withMapCamera(newCamera);
final oldCamera = camera;

_emitMapEvent(
MapEventRotate(
id: id,
source: source,
oldCamera: oldCamera,
camera: camera,
),
);
return true;
}
// Update camera then emit events and callbacks
value = value.withMapCamera(newCamera);

return false;
_emitMapEvent(
MapEventRotate(
id: id,
source: source,
oldCamera: oldCamera,
camera: camera,
),
);
return true;
}

MoveAndRotateResult rotateAroundPointRaw(
Expand Down Expand Up @@ -340,9 +356,23 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
value = _MapControllerState(
options: newOptions,
camera: newCamera,
animationController: value.animationController,
);
}

set vsync(TickerProvider tickerProvider) {
if (value.animationController == null) {
value = _MapControllerState(
options: value.options,
camera: value.camera,
animationController: AnimationController(vsync: tickerProvider)
..addListener(_handleAnimation),
);
} else {
_animationController.resync(tickerProvider);
}
}

/// To be called when a gesture that causes movement starts.
void moveStarted(MapEventSource source) {
_emitMapEvent(
Expand Down Expand Up @@ -508,6 +538,161 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
);
}

void moveAndRotateAnimatedRaw(
LatLng newCenter,
double newZoom,
double newRotation, {
required Offset offset,
required Duration duration,
required Curve curve,
required bool hasGesture,
required MapEventSource source,
}) {
if (newRotation == camera.rotation) {
moveAnimatedRaw(
newCenter,
newZoom,
duration: duration,
curve: curve,
hasGesture: hasGesture,
source: source,
);
return;
}
// cancel all ongoing animation
_animationController.stop();
_resetAnimations();

if (newCenter == camera.center && newZoom == camera.zoom) return;

// create the new animation
_moveAnimation = LatLngTween(begin: camera.center, end: newCenter)
.chain(CurveTween(curve: curve))
.animate(_animationController);
_zoomAnimation = Tween<double>(begin: camera.zoom, end: newZoom)
.chain(CurveTween(curve: curve))
.animate(_animationController);
_rotationAnimation = Tween<double>(begin: camera.rotation, end: newRotation)
.chain(CurveTween(curve: curve))
.animate(_animationController);

_animationController.duration = duration;
_animationHasGesture = hasGesture;
_animationOffset = offset;

// start the animation from its start
_animationController.forward(from: 0);
}

void rotateAnimatedRaw(
double newRotation, {
required Offset offset,
required Duration duration,
required Curve curve,
required bool hasGesture,
required MapEventSource source,
}) {
// cancel all ongoing animation
_animationController.stop();
_resetAnimations();

if (newRotation == camera.rotation) return;

// create the new animation
_rotationAnimation = Tween<double>(begin: camera.rotation, end: newRotation)
.chain(CurveTween(curve: curve))
.animate(_animationController);

_animationController.duration = duration;
_animationHasGesture = hasGesture;
_animationOffset = offset;

// start the animation from its start
_animationController.forward(from: 0);
}

void stopAnimationRaw({bool canceled = true}) {
if (isAnimating) _animationController.stop(canceled: canceled);
}

bool get isAnimating => _animationController.isAnimating;

void _resetAnimations() {
_moveAnimation = null;
_rotationAnimation = null;
_zoomAnimation = null;
_flingAnimation = null;
}

void flingAnimatedRaw({
required double velocity,
required Offset direction,
required Offset begin,
Offset offset = Offset.zero,
double mass = 1,
double stiffness = 1000,
double ratio = 5,
required bool hasGesture,
}) {
// cancel all ongoing animation
_animationController.stop();
_resetAnimations();

_animationHasGesture = hasGesture;
_animationOffset = offset;
_flingMapCenterStartPoint = camera.project(camera.center);

final distance =
(Offset.zero & Size(camera.nonRotatedSize.x, camera.nonRotatedSize.y))
.shortestSide;

_flingAnimation = Tween<Offset>(
begin: begin,
end: begin - direction * distance,
).animate(_animationController);

_animationController.value = 0;
_animationController.fling(
velocity: velocity,
springDescription: SpringDescription.withDampingRatio(
mass: mass,
stiffness: stiffness,
ratio: ratio,
),
);
}

void moveAnimatedRaw(
LatLng newCenter,
double newZoom, {
Offset offset = Offset.zero,
required Duration duration,
required Curve curve,
required bool hasGesture,
required MapEventSource source,
}) {
// cancel all ongoing animation
_animationController.stop();
_resetAnimations();

if (newCenter == camera.center && newZoom == camera.zoom) return;

// create the new animation
_moveAnimation = LatLngTween(begin: camera.center, end: newCenter)
.chain(CurveTween(curve: curve))
.animate(_animationController);
_zoomAnimation = Tween<double>(begin: camera.zoom, end: newZoom)
.chain(CurveTween(curve: curve))
.animate(_animationController);

_animationController.duration = duration;
_animationHasGesture = hasGesture;
_animationOffset = offset;

// start the animation from its start
_animationController.forward(from: 0);
}

void _emitMapEvent(MapEvent event) {
if (event.source == MapEventSource.mapController && event is MapEventMove) {
_interactiveViewerState.interruptAnimatedMovement(event);
Expand All @@ -518,9 +703,58 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
_mapEventSink.add(event);
}

void _handleAnimation() {
// fling animation
if (_flingAnimation != null) {
final newCenterPoint = _flingMapCenterStartPoint +
_flingAnimation!.value.toPoint().rotate(camera.rotationRad);
moveRaw(
camera.unproject(newCenterPoint),
camera.zoom,
hasGesture: _animationHasGesture,
source: MapEventSource.flingAnimationController,
offset: _animationOffset,
);
return;
}

// animated movement
if (_moveAnimation != null) {
if (_rotationAnimation != null) {
moveAndRotateRaw(
_moveAnimation?.value ?? camera.center,
_zoomAnimation?.value ?? camera.zoom,
_rotationAnimation!.value,
hasGesture: _animationHasGesture,
source: MapEventSource.mapController,
offset: _animationOffset,
);
} else {
moveRaw(
_moveAnimation!.value,
_zoomAnimation?.value ?? camera.zoom,
hasGesture: _animationHasGesture,
source: MapEventSource.mapController,
offset: _animationOffset,
);
}
return;
}

// animated rotation
if (_rotationAnimation != null) {
rotateRaw(
_rotationAnimation!.value,
hasGesture: _animationHasGesture,
source: MapEventSource.mapController,
);
}
}

@override
void dispose() {
_mapEventStreamController.close();
value.animationController?.dispose();
super.dispose();
}
}
Expand All @@ -529,14 +763,17 @@ class MapControllerImpl extends ValueNotifier<_MapControllerState>
class _MapControllerState {
final MapCamera? camera;
final MapOptions? options;
final AnimationController? animationController;

const _MapControllerState({
required this.options,
required this.camera,
required this.animationController,
});

_MapControllerState withMapCamera(MapCamera camera) => _MapControllerState(
options: options,
camera: camera,
animationController: animationController,
);
}
5 changes: 3 additions & 2 deletions lib/src/map/widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class FlutterMap extends StatefulWidget {
}

class _FlutterMapStateContainer extends State<FlutterMap>
with AutomaticKeepAliveClientMixin {
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
bool _initialCameraFitApplied = false;

late MapControllerImpl _mapController;
Expand Down Expand Up @@ -184,9 +184,10 @@ class _FlutterMapStateContainer extends State<FlutterMap>

void _setMapController() {
if (_controllerCreatedInternally) {
_mapController = MapControllerImpl(widget.options);
_mapController = MapControllerImpl(options: widget.options, vsync: this);
} else {
_mapController = widget.mapController! as MapControllerImpl;
_mapController.vsync = this;
_mapController.options = widget.options;
}
}
Expand Down
Loading