mapbox / mapbox-maps-flutter

Interactive, thoroughly customizable maps for Flutter powered by Mapbox Maps SDK
https://www.mapbox.com/mobile-maps-sdk
Other
292 stars 120 forks source link

Add Interpolator and AnimatorListener to MapAnimationOptions #731

Open flikkr opened 1 month ago

flikkr commented 1 month ago

I am trying to recreate this rotating globe animation from the Android and iOS SDK in Flutter, but it seems like there are some missing parameters in the MapAnimationOptions class, notably Interpolator for indicating the animation curve, and AnimatorListener for listening to the animation onEnd event.

My current implementation has jittery animation due to the animation curve not being linear. I'm wondering if there is an easier way to do this without the missing animation parameters?

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:heritage_quest/util/env.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';

class MapPage extends StatefulWidget {
  const MapPage({super.key});

  @override
  State createState() => MapPageState();
}

class MapPageState extends State<MapPage> {
  late MapboxMap _map;
  late Timer _timer;
  bool _spinEnabled = true;
  bool _userInteracting = false;
  final double _secondsPerRevolution = 120;
  final double _maxSpinZoom = 5;
  final double _slowSpinZoom = 3;

  @override
  void initState() {
    super.initState();
    MapboxOptions.setAccessToken(Env.mapboxToken);
  }

  void _onMapCreated(MapboxMap mapboxMap) {
    _map = mapboxMap;
    _map.style.setProjection(StyleProjection(name: StyleProjectionName.globe));
    _startSpinning();
  }

  void _startSpinning() {
    _timer = Timer.periodic(Duration(milliseconds: 1000), (timer) {
      _spinGlobe();
    });
  }

  Future<void> _spinGlobe() async {
    if (_spinEnabled && !_userInteracting) {
      final position = await _map.getCameraState();
      double zoom = position.zoom;
      if (zoom < _maxSpinZoom) {
        double distancePerSecond = 360 / _secondsPerRevolution;
        if (zoom > _slowSpinZoom) {
          double zoomDif = (_maxSpinZoom - zoom) / (_maxSpinZoom - _slowSpinZoom);
          distancePerSecond *= zoomDif;
        }
        double newLng = position.center.coordinates.lng - distancePerSecond;
        _map.easeTo(
          CameraOptions(
            center: Point(
              coordinates: Position(newLng, position.center.coordinates.lat),
            ),
          ),
          MapAnimationOptions(duration: 1000),
        );
      }
    }
  }

  void _onScrollListener(MapContentGestureContext gesture) {
    _timer.cancel();
    print(gesture.touchPosition.toString());
  }

  void _onMapIdleListener(MapIdleEventData event) {
    print("I'm idle");
    _map.getCameraState().then((value) {
      if (canSpin(value.zoom)) {
        _startSpinning();
      }
    });
  }

  bool canSpin(double zoom) => _spinEnabled && !_userInteracting && zoom < _maxSpinZoom;

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          MapWidget(
            cameraOptions: CameraOptions(zoom: 3),
            onMapIdleListener: _onMapIdleListener,
            onScrollListener: _onScrollListener,
            key: const ValueKey("mapWidget"),
            styleUri: MapboxStyles.STANDARD_SATELLITE,
            onMapCreated: _onMapCreated,
          ),
        ],
      ),
    );
  }
}
evil159 commented 1 month ago

Hi @flikkr, thank you for reaching out! We do lack the timing curve parameter for our animations comparing to native platforms, I've created a ticket to add it on the Flutter side https://mapbox.atlassian.net/browse/MAPSFLT-255. Meanwhile, you can animate the globe with a series of setCamera() calls. Setup an animation with AnimationController/Ticker and then in the animation/ticket callback update the map camera with the position interpolated for the current animation progress. This is a bit more elaborate way than using high-level animations provided by us, but you can customize every aspect of the animation this way.

flikkr commented 1 month ago

Thanks for the reply @evil159! Nice to hear that it will be worked on.

Using the method you mentioned, wouldn't I need to call the setCamera method 60 times per second if I want 60fps animation? That's a lot of native calls, would it affect performance?

evil159 commented 1 month ago

Yes, you'd need to set the camera in sync with the current frame drawing frequency(60 is a good starting point), it should have more overhead than calling easeTo() once per second, but should still be manageable. If you'll experience a big degradation in performance or any other severe issues - don't hesitate to file another issue.