From 70c2d5a4cabcd87bc4e6e564fcd6dfa10bd4c521 Mon Sep 17 00:00:00 2001 From: Chris Loer Date: Thu, 6 Dec 2018 13:31:42 +0200 Subject: [core, darwin, android] Add onDidEnterIdle to MapObserver. didEnterIdle fires whenever render completes and no repaint is scheduled. --- include/mbgl/map/map_observer.hpp | 1 + .../mapbox/mapboxsdk/maps/MapChangeReceiver.java | 25 ++++++++++++++++ .../java/com/mapbox/mapboxsdk/maps/MapView.java | 35 +++++++++++++++++++++- .../com/mapbox/mapboxsdk/maps/NativeMapView.java | 7 +++++ .../mapboxsdk/maps/MapChangeReceiverTest.java | 33 ++++++++++++++++++++ .../activity/maplayout/MapChangeActivity.java | 1 + platform/android/src/native_map_view.cpp | 9 ++++++ platform/android/src/native_map_view.hpp | 1 + platform/ios/src/MGLMapViewDelegate.h | 13 ++++++++ platform/macos/src/MGLMapView.mm | 14 +++++++++ platform/macos/src/MGLMapViewDelegate.h | 13 ++++++++ .../test/MGLMapViewDelegateIntegrationTests.swift | 2 ++ scripts/changelog_staging/idle-event.json | 6 ++++ src/mbgl/map/map.cpp | 2 ++ 14 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 scripts/changelog_staging/idle-event.json diff --git a/include/mbgl/map/map_observer.hpp b/include/mbgl/map/map_observer.hpp index f63e5f2af3..9c3345d614 100644 --- a/include/mbgl/map/map_observer.hpp +++ b/include/mbgl/map/map_observer.hpp @@ -39,6 +39,7 @@ public: virtual void onDidFinishRenderingMap(RenderMode) {} virtual void onDidFinishLoadingStyle() {} virtual void onSourceChanged(style::Source&) {} + virtual void onDidEnterIdle() {} }; } // namespace mbgl diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java index a12c6c7fb3..f569ae3af9 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapChangeReceiver.java @@ -26,6 +26,8 @@ class MapChangeReceiver implements NativeMapView.StateCallback { = new CopyOnWriteArrayList<>(); private final List onDidFinishRenderingMapListenerList = new CopyOnWriteArrayList<>(); + private final List onDidEnterIdleListenerList + = new CopyOnWriteArrayList<>(); private final List onDidFinishLoadingStyleListenerList = new CopyOnWriteArrayList<>(); private final List onSourceChangedListenerList = new CopyOnWriteArrayList<>(); @@ -170,6 +172,20 @@ class MapChangeReceiver implements NativeMapView.StateCallback { } } + @Override + public void onDidEnterIdle() { + try { + if (!onDidEnterIdleListenerList.isEmpty()) { + for (MapView.OnDidEnterIdleListener listener : onDidEnterIdleListenerList) { + listener.onDidEnterIdle(); + } + } + } catch (Throwable err) { + Logger.e(TAG, "Exception in onDidEnterIdle", err); + throw err; + } + } + @Override public void onDidFinishLoadingStyle() { try { @@ -278,6 +294,14 @@ class MapChangeReceiver implements NativeMapView.StateCallback { onDidFinishRenderingMapListenerList.remove(listener); } + void addOnDidEnterIdleListener(MapView.OnDidEnterIdleListener listener) { + onDidEnterIdleListenerList.add(listener); + } + + void removeOnDidEnterIdleListener(MapView.OnDidEnterIdleListener listener) { + onDidEnterIdleListenerList.remove(listener); + } + void addOnDidFinishLoadingStyleListener(MapView.OnDidFinishLoadingStyleListener listener) { onDidFinishLoadingStyleListenerList.add(listener); } @@ -305,6 +329,7 @@ class MapChangeReceiver implements NativeMapView.StateCallback { onDidFinishRenderingFrameList.clear(); onWillStartRenderingMapListenerList.clear(); onDidFinishRenderingMapListenerList.clear(); + onDidEnterIdleListenerList.clear(); onDidFinishLoadingStyleListenerList.clear(); onSourceChangedListenerList.clear(); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java index b4c977cf15..8a29ae8070 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/MapView.java @@ -691,12 +691,32 @@ public class MapView extends FrameLayout implements NativeMapView.ViewCallback { /** * Remove a callback that's invoked when the map has finished rendering. * - * @param listener The callback that's invoked when the map has finished rendering + * @param listener The callback that's invoked when the map has has finished rendering. */ public void removeOnDidFinishRenderingMapListener(OnDidFinishRenderingMapListener listener) { mapChangeReceiver.removeOnDidFinishRenderingMapListener(listener); } + /** + * Set a callback that's invoked when the map has entered the idle state. + * + * @param listener The callback that's invoked when the map has entered the idle state. + */ + public void addOnDidEnterIdleListener(OnDidEnterIdleListener listener) { + mapChangeReceiver.addOnDidEnterIdleListener(listener); + } + + /** + * Remove a callback that's invoked when the map has entered the idle state. + * + * @param listener The callback that's invoked when the map has entered the idle state. + */ + public void removeOnDidEnterIdleListener(OnDidEnterIdleListener listener) { + mapChangeReceiver.removeOnDidEnterIdleListener(listener); + } + + /** + /** * Set a callback that's invoked when the style has finished loading. * @@ -870,6 +890,19 @@ public class MapView extends FrameLayout implements NativeMapView.ViewCallback { void onDidFinishRenderingMap(boolean fully); } + /** + * Interface definition for a callback to be invoked when the map has entered the idle state. + *

+ * {@link MapView#addOnDidEnterIdleListener(OnDidEnterIdleListener)} + *

+ */ + public interface OnDidEnterIdleListener { + /** + * Called when the map has entered the idle state. + */ + void onDidEnterIdle(); + } + /** * Interface definition for a callback to be invoked when the map has loaded the style. *

diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java index bdbca4fbca..59840f5bf3 100755 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/NativeMapView.java @@ -1009,6 +1009,11 @@ final class NativeMapView { } } + @Keep + private void onDidEnterIdle() { + stateCallback.onDidEnterIdle(); + } + @Keep private void onDidFinishLoadingStyle() { if (stateCallback != null) { @@ -1472,6 +1477,8 @@ final class NativeMapView { void onDidFinishRenderingMap(boolean fully); + void onDidEnterIdle(); + void onSourceChanged(String sourceId); } } diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java index 061fa7819a..848cf4a804 100644 --- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java +++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/maps/MapChangeReceiverTest.java @@ -55,6 +55,9 @@ public class MapChangeReceiverTest { @Mock private MapView.OnDidFinishRenderingMapListener onDidFinishRenderingMapListener; + @Mock + private MapView.OnDidEnterIdleListener onDidEnterIdleListener; + @Mock private MapView.OnDidFinishLoadingStyleListener onDidFinishLoadingStyleListener; @@ -491,6 +494,36 @@ public class MapChangeReceiverTest { } } + @Test + public void testOnDidEnterIdleListener() { + mapChangeEventManager.addOnDidEnterIdleListener(onDidEnterIdleListener); + mapChangeEventManager.onDidEnterIdle(); + verify(onDidEnterIdleListener).onDidEnterIdle(); + mapChangeEventManager.removeOnDidEnterIdleListener(onDidEnterIdleListener); + mapChangeEventManager.onDidEnterIdle(); + verify(onDidEnterIdleListener).onDidEnterIdle(); + + mapChangeEventManager.addOnDidEnterIdleListener(onDidEnterIdleListener); + Logger.setLoggerDefinition(loggerDefinition); + Exception exc = new RuntimeException(); + doThrow(exc).when(onDidEnterIdleListener).onDidEnterIdle(); + try { + mapChangeEventManager.onDidEnterIdle(); + Assert.fail("The exception should've been re-thrown."); + } catch (RuntimeException throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(exc)); + } + + Error err = new ExecutionError("", new Error()); + doThrow(err).when(onDidEnterIdleListener).onDidEnterIdle(); + try { + mapChangeEventManager.onDidEnterIdle(); + Assert.fail("The exception should've been re-thrown."); + } catch (ExecutionError throwable) { + verify(loggerDefinition).e(anyString(), anyString(), eq(err)); + } + } + @Test public void testOnDidFinishLoadingStyleListener() { mapChangeEventManager.addOnDidFinishLoadingStyleListener(onDidFinishLoadingStyleListener); diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/MapChangeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/MapChangeActivity.java index 75a21157e9..7fe4a47217 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/MapChangeActivity.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/maplayout/MapChangeActivity.java @@ -32,6 +32,7 @@ public class MapChangeActivity extends AppCompatActivity { mapView.addOnDidFinishLoadingStyleListener(() -> Timber.v("OnDidFinishLoadingStyle")); mapView.addOnDidFinishRenderingFrameListener(fully -> Timber.v("OnDidFinishRenderingFrame: fully: %s", fully)); mapView.addOnDidFinishRenderingMapListener(fully -> Timber.v("OnDidFinishRenderingMap: fully: %s", fully)); + mapView.addOnDidEnterIdleListener(() -> Timber.v("OnDidEnterIdle")); mapView.addOnSourceChangedListener(sourceId -> Timber.v("OnSourceChangedListener: source with id: %s", sourceId)); mapView.addOnWillStartLoadingMapListener(() -> Timber.v("OnWillStartLoadingMap")); mapView.addOnWillStartRenderingFrameListener(() -> Timber.v("OnWillStartRenderingFrame")); diff --git a/platform/android/src/native_map_view.cpp b/platform/android/src/native_map_view.cpp index a750baa62c..d724afa923 100755 --- a/platform/android/src/native_map_view.cpp +++ b/platform/android/src/native_map_view.cpp @@ -186,6 +186,15 @@ void NativeMapView::onDidFinishRenderingMap(MapObserver::RenderMode mode) { javaPeer.get(*_env).Call(*_env, onDidFinishRenderingMap, (jboolean) (mode != MapObserver::RenderMode::Partial)); } +void NativeMapView::onDidEnterIdle() { + assert(vm != nullptr); + + android::UniqueEnv _env = android::AttachEnv(); + static auto& javaClass = jni::Class::Singleton(*_env); + static auto onDidEnterIdle = javaClass.GetMethod(*_env, "onDidEnterIdle"); + javaPeer.get(*_env).Call(*_env, onDidEnterIdle); +} + void NativeMapView::onDidFinishLoadingStyle() { assert(vm != nullptr); diff --git a/platform/android/src/native_map_view.hpp b/platform/android/src/native_map_view.hpp index b1f8354d46..44e9259b2a 100755 --- a/platform/android/src/native_map_view.hpp +++ b/platform/android/src/native_map_view.hpp @@ -67,6 +67,7 @@ public: void onDidFinishRenderingFrame(MapObserver::RenderMode) override; void onWillStartRenderingMap() override; void onDidFinishRenderingMap(MapObserver::RenderMode) override; + void onDidEnterIdle() override; void onDidFinishLoadingStyle() override; void onSourceChanged(mbgl::style::Source&) override; diff --git a/platform/ios/src/MGLMapViewDelegate.h b/platform/ios/src/MGLMapViewDelegate.h index 9175669dc9..8c1d3d98c0 100644 --- a/platform/ios/src/MGLMapViewDelegate.h +++ b/platform/ios/src/MGLMapViewDelegate.h @@ -239,6 +239,19 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)mapViewDidFinishRenderingFrame:(MGLMapView *)mapView fullyRendered:(BOOL)fullyRendered; +/** + Tells the delegate that the map view is entering an idle state, and no more + drawing will be necessary until new data is loaded or there is some interaction + with the map. + + - No camera transitions are in progress + - All currently requested tiles have loaded + - All fade/transition animations have completed + + @param mapView The map view that has just entered the idle state. + */ +- (void)mapViewDidEnterIdle:(MGLMapView *)mapView; + /** Tells the delegate that the map has just finished loading a style. diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm index 2f379ab406..eb877216d2 100644 --- a/platform/macos/src/MGLMapView.mm +++ b/platform/macos/src/MGLMapView.mm @@ -934,6 +934,16 @@ public: } } +- (void)mapViewDidEnterIdle { + if (!_mbglMap) { + return; + } + + if ([self.delegate respondsToSelector:@selector(mapViewDidEnterIdle)]) { + [self.delegate mapViewDidEnterIdle:self]; + } +} + - (void)mapViewDidFinishLoadingStyle { if (!_mbglMap) { return; @@ -3050,6 +3060,10 @@ public: bool fullyRendered = mode == mbgl::MapObserver::RenderMode::Full; [nativeView mapViewDidFinishRenderingMapFullyRendered:fullyRendered]; } + + void onDidEnterIdle() override { + [nativeView mapViewDidEnterIdle]; + } void onDidFinishLoadingStyle() override { [nativeView mapViewDidFinishLoadingStyle]; diff --git a/platform/macos/src/MGLMapViewDelegate.h b/platform/macos/src/MGLMapViewDelegate.h index 2a8b28c1b4..ad59f5bd39 100644 --- a/platform/macos/src/MGLMapViewDelegate.h +++ b/platform/macos/src/MGLMapViewDelegate.h @@ -151,6 +151,19 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)mapViewDidFinishRenderingFrame:(MGLMapView *)mapView fullyRendered:(BOOL)fullyRendered; +/** + Tells the delegate that the map view is entering an idle state, and no more + drawing will be necessary until new data is loaded or there is some interaction + with the map. + + - No camera transitions are in progress + - All currently requested tiles have loaded + - All fade/transition animations have completed + + @param mapView The map view that has just entered the idle state. + */ +- (void)mapViewDidEnterIdle:(MGLMapView *)mapView; + /** Tells the delegate that the map has just finished loading a style. diff --git a/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift b/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift index 00635d97eb..109c279a09 100644 --- a/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift +++ b/platform/macos/test/MGLMapViewDelegateIntegrationTests.swift @@ -24,6 +24,8 @@ extension MGLMapViewDelegateIntegrationTests: MGLMapViewDelegate { func mapViewDidFinishRenderingFrame(_ mapView: MGLMapView, fullyRendered: Bool) {} func mapViewDidFinishRenderingMap(_ mapView: MGLMapView, fullyRendered: Bool) {} + + func mapViewDidEnterIdle(_ mapView: MGLMapView) {} func mapViewDidFailLoadingMap(_ mapView: MGLMapView, withError error: Error) {} diff --git a/scripts/changelog_staging/idle-event.json b/scripts/changelog_staging/idle-event.json new file mode 100644 index 0000000000..96db06eb99 --- /dev/null +++ b/scripts/changelog_staging/idle-event.json @@ -0,0 +1,6 @@ +{ + "core": "Add onDidEnterIdle to MapObserver, which fires whenever render completes and no repaint is scheduled.", + "darwin": "Add mapViewDidEnterIdle to MGLMapViewDelegate, which fires whenever render completes and no repaint is scheduled.", + "android": "Add onDidEnterIdle listener to MapView, which fires whenever render completes and no repaint is scheduled.", + "issue": 13469 +} diff --git a/src/mbgl/map/map.cpp b/src/mbgl/map/map.cpp index c3bebae837..5e80f1512b 100644 --- a/src/mbgl/map/map.cpp +++ b/src/mbgl/map/map.cpp @@ -209,6 +209,8 @@ void Map::Impl::onDidFinishRenderingFrame(RenderMode renderMode, bool needsRepai if (needsRepaint || transform.inTransition()) { onUpdate(); + } else if (rendererFullyLoaded) { + observer.onDidEnterIdle(); } } else if (stillImageRequest && rendererFullyLoaded) { auto request = std::move(stillImageRequest); -- cgit v1.2.1