flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.65k stars 27.6k forks source link

[google_maps_flutter] - onCameraIdle behaves differently on Android and iOS #116722

Open hellomentorplus opened 1 year ago

hellomentorplus commented 1 year ago

Steps to Reproduce

Create a simple GoogleMap widget

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class MapTest extends StatelessWidget {
  MapTest({Key? key}) : super(key: key);

  late final GoogleMapController mapController;

  final CameraPosition cameraPosition = const CameraPosition(
      target: LatLng(37.816829, 144.967318), zoom: 15);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
      GoogleMap(
      initialCameraPosition: cameraPosition,
    onMapCreated: (GoogleMapController controller) => mapController = controller,
    onCameraMoveStarted: () => print('onCameraMoveStarted'),
    onCameraIdle: () => print('onCamera idle'),
    myLocationEnabled: false,
    zoomControlsEnabled: false,
    mapToolbarEnabled: false,
    compassEnabled: false,
    myLocationButtonEnabled: false),
          ElevatedButton(onPressed: () {
            // move camera to the same camera position
            mapController.moveCamera(CameraUpdate.newCameraPosition(cameraPosition));
          },child: const Text('trigger move camera'),)
        ],
      ),
    );
  }
}
  1. run widget with code above
  2. tap on button 'trigger move camera' without moving the map to new camera position

Observation

On Android device, when tapping on 'trigger move camera', both onCameraMoveStarted and onCameraIdle will be triggered although camera has the same camera position.

On iOS device, only 1 line 'flutter: onCamera idle' is printed, tapping on 'trigger move camera' without actually move camera will not trigger moving camera or call onCameraIdle. When move map to different camera position, and tap on 'trigger move camera', then both onCameraMoveStarted and onCameraIdle are called respectively

Expectation

Behaviour to be the same on both Android and iOS.

darshankawar commented 1 year ago

Thanks for the report. Using code sample provided and running on iOS, observed that only onCameraIdle is printed first but not when tapping the button.

flutter: onCamera idle

stable, master flutter doctor -v ``` [✓] Flutter (Channel stable, 3.3.9, on macOS 12.2.1 21D62 darwin-x64, locale en-GB) • Flutter version 3.3.9 on channel stable at /Users/dhs/documents/fluttersdk/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision b8f7f1f986 (12 days ago), 2022-11-23 06:43:51 +0900 • Engine revision 8f2221fbef • Dart version 2.18.5 • DevTools version 2.15.0 [!] Xcode - develop for iOS and macOS (Xcode 12.3) • Xcode at /Applications/Xcode.app/Contents/Developer ! Flutter recommends a minimum Xcode version of 13. Download the latest version or update via the Mac App Store. • CocoaPods version 1.11.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] VS Code (version 1.62.0) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.21.0 [✓] Connected device (5 available) • SM G975F (mobile) • RZ8M802WY0X • android-arm64 • Android 11 (API 30) • Darshan's iphone (mobile) • 21150b119064aecc249dfcfe05e259197461ce23 • ios • iOS 14.4.1 18D61 • iPhone 12 Pro Max (mobile) • A5473606-0213-4FD8-BA16-553433949729 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-3 (simulator) • macOS (desktop) • macos • darwin-x64 • Mac OS X 10.15.4 19E2269 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 98.0.4758.80 [✓] HTTP Host Availability • All required HTTP hosts are available ! Doctor found issues in 1 category. [!] Flutter (Channel master, 3.7.0-4.0.pre.48, on macOS 12.2.1 21D62 darwin-x64, locale en-GB) • Flutter version 3.7.0-4.0.pre.48 on channel master at /Users/dhs/documents/fluttersdk/flutter ! Warning: `flutter` on your path resolves to /Users/dhs/Documents/Fluttersdk/flutter/bin/flutter, which is not inside your current Flutter SDK checkout at /Users/dhs/documents/fluttersdk/flutter. Consider adding /Users/dhs/documents/fluttersdk/flutter/bin to the front of your path. ! Warning: `dart` on your path resolves to /Users/dhs/Documents/Fluttersdk/flutter/bin/dart, which is not inside your current Flutter SDK checkout at /Users/dhs/documents/fluttersdk/flutter. Consider adding /Users/dhs/documents/fluttersdk/flutter/bin to the front of your path. • Upstream repository https://github.com/flutter/flutter.git • Framework revision eefbe85c8b (19 minutes ago), 2022-12-08 22:32:07 -0600 • Engine revision 8d83b98c55 • Dart version 3.0.0 (build 3.0.0-0.0.dev) • DevTools version 2.20.0 • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades. [!] Xcode - develop for iOS and macOS (Xcode 12.3) • Xcode at /Applications/Xcode.app/Contents/Developer ! Flutter recommends a minimum Xcode version of 13. Download the latest version or update via the Mac App Store. • CocoaPods version 1.11.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] VS Code (version 1.62.0) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.21.0 [✓] Connected device (5 available) • SM G975F (mobile) • RZ8M802WY0X • android-arm64 • Android 11 (API 30) • Darshan's iphone (mobile) • 21150b119064aecc249dfcfe05e259197461ce23 • ios • iOS 14.4.1 18D61 • iPhone 12 Pro Max (mobile) • A5473606-0213-4FD8-BA16-553433949729 • ios • com.apple.CoreSimulator.SimRuntime.iOS-14-3 (simulator) • macOS (desktop) • macos • darwin-x64 • Mac OS X 10.15.4 19E2269 darwin-x64 • Chrome (web) • chrome • web-javascript • Google Chrome 98.0.4758.80 [✓] HTTP Host Availability • All required HTTP hosts are available ! Doctor found issues in 1 category. ```
Colman commented 3 months ago

Problem The Google Maps Flutter library simply calls the onCameraIdle callback whenever the associated onCameraIdle callback is called within the platform specific SDK. In the Google Maps SDK for Android, the map doesn't have to move for onCameraIdle to be called. In the Google Maps SDK for iOS the map does have to move for onCameraIdle to be called.

Best fix I believe this bug should be fixed in the Google Maps iOS SDK. Google built both SDKs so they should have a consistent definition of what "idle" means. If the map moves from its current position to its current position, did it idle? I don't see why this should be platform dependent.

Package fix If it's determined that the SDKs should not be updated, then the Flutter plugin should check if the camera update being given to the iOS SDK is "significant". If it's not a "significant" change, the Flutter plugin should call onCameraIdle itself since it knows that the iOS SDK won't.

Workaround Extend or create a wrapper around your GoogleMapController. Any time, the moveCamera or animateCamera method is called, check if the camera change is "significant" by comparing it to the last camera position given by the onCameraMoved callback. If it is, then the onCameraIdle callback will be called properly so you can call the method on the actual controller as usual. Else, call the onCameraIdle callback yourself since you know that the package won't.

What is "significant"? You can't just check if the change to the camera is zero. Instead, you must check if it's "significant". This is because the iOS maps plugin uses a float32 for latitude, longitude, and zoom (bearing and tilt are float64). Whereas, Dart uses a double. This means that if the change you request is small enough, but not zero, the iOS maps plugin may consider it as no change. For example, if the current zoom of the map is 13 and you request 13.0000001, the Flutter plugin will cast it to a float32, which gives: 13.000000. Since, this is equivalent to the last zoom, it will not move and not call onCameraIdle. Therefore, you can't just check:

if (previousZoom != newZoom || previousLatitude != newLatitude || previousLongitude != newLongitude)

You need to cast all values to Float32 first, then compare them:

Future<bool?> _isSignificantChange(LatLng target, double zoom) async {
   final currentRoundedLatitude = _getFloat32(
     currentCameraPosition.target.latitude,
   );
   final currentRoundedLongitude = _getFloat32(
     currentCameraPosition.target.longitude,
   );
   final currentRoundedZoom = _getFloat32(
     currentCameraPosition.zoom,
   );
   final currentRoundedZoom = _getFloat32(
     currentCameraPosition.zoom,
   );

   final targetRoundedLatitude = _getFloat32(target.latitude);
   final targetRoundedLongitude = _getFloat32(target.longitude);
   final targetRoundedZoom = _getFloat32(zoom);

   return currentRoundedLatitude != targetRoundedLatitude ||
       currentRoundedLongitude != targetRoundedLongitude ||
       currentRoundedZoom != targetRoundedZoom;
}

double _getFloat32(double value) {
   final bytes = ByteData(4);
   bytes.setFloat32(0, value);
   return bytes.getFloat32(0);
}