diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9559c1a3..479c191cce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Mapbox welcomes participation and contributions from everyone. ## Features ✨ and improvements 🏁 * Add accuracy radius support for LocationComponent. ([#1016](https://github.com/mapbox/mapbox-maps-android/pull/1016)) +* Add support for custom widgets rendered on top of the map. ([#1036](https://github.com/mapbox/mapbox-maps-android/pull/1036)) # 10.4.0-beta.1 diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt index 920efa62d3..9c4e79a768 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/CarCameraController.kt @@ -6,12 +6,13 @@ import com.mapbox.maps.MapSurface import com.mapbox.maps.dsl.cameraOptions import com.mapbox.maps.extension.androidauto.OnMapScrollListener import com.mapbox.maps.plugin.animation.camera +import com.mapbox.maps.plugin.locationcomponent.OnIndicatorBearingChangedListener import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener /** * Controller class to handle map camera changes. */ -class CarCameraController : OnIndicatorPositionChangedListener, OnMapScrollListener { +class CarCameraController : OnIndicatorPositionChangedListener, OnIndicatorBearingChangedListener, OnMapScrollListener { private var lastGpsLocation: Point = HELSINKI private var isTrackingPuck = true @@ -46,6 +47,16 @@ class CarCameraController : OnIndicatorPositionChangedListener, OnMapScrollListe } } + override fun onIndicatorBearingChanged(bearing: Double) { + if (isTrackingPuck) { + surface.getMapboxMap().setCamera( + cameraOptions { + bearing(bearing) + } + ) + } + } + override fun onMapScroll() { dismissTracking() } diff --git a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt index f93341809a..a24fa5b0cf 100644 --- a/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt +++ b/android-auto-app/src/main/java/com/mapbox/maps/testapp/auto/car/MapSession.kt @@ -4,12 +4,12 @@ import android.Manifest.permission.ACCESS_FINE_LOCATION import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.content.res.Configuration -import androidx.car.app.Screen -import androidx.car.app.ScreenManager -import androidx.car.app.Session +import androidx.car.app.* import com.mapbox.maps.EdgeInsets import com.mapbox.maps.MapSurface import com.mapbox.maps.Style +import com.mapbox.maps.extension.androidauto.CompassWidget +import com.mapbox.maps.extension.androidauto.LogoWidget import com.mapbox.maps.extension.androidauto.initMapSurface import com.mapbox.maps.extension.style.layers.generated.skyLayer import com.mapbox.maps.extension.style.layers.properties.generated.SkyType @@ -28,7 +28,16 @@ class MapSession : Session() { override fun onCreateScreen(intent: Intent): Screen { val mapScreen = MapScreen(carContext) - initMapSurface(scrollListener = carCameraController) { surface -> + initMapSurface( + scrollListener = carCameraController, + ) { surface -> + val logo = LogoWidget(carContext) + val compass = CompassWidget( + carContext, + marginX = 26f, + marginY = 120f, + ) + mapSurface = surface carCameraController.init( mapSurface, @@ -42,6 +51,15 @@ class MapSession : Session() { mapScreen.setMapCameraController(carCameraController) loadStyle(surface) initLocationComponent(surface) + + surface.addWidget(logo) + surface.addWidget(compass) + + surface.getMapboxMap().apply { + addOnCameraChangeListener { + compass.setRotation(this.cameraState.bearing.toFloat()) + } + } } return if (carContext.checkSelfPermission(ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) { carContext.getCarService(ScreenManager::class.java).push(mapScreen) @@ -73,6 +91,7 @@ class MapSession : Session() { locationPuck = CarLocationPuck.duckLocationPuckLowZoom enabled = true addOnIndicatorPositionChangedListener(carCameraController) + addOnIndicatorBearingChangedListener(carCameraController) } } diff --git a/extension-androidauto/api/extension-androidauto.api b/extension-androidauto/api/extension-androidauto.api index 61b9785e04..1b52d6918b 100644 --- a/extension-androidauto/api/extension-androidauto.api +++ b/extension-androidauto/api/extension-androidauto.api @@ -7,6 +7,16 @@ public final class com/mapbox/maps/extension/androidauto/BuildConfig { public fun ()V } +public final class com/mapbox/maps/extension/androidauto/CompassWidget : com/mapbox/maps/renderer/widget/BitmapWidget { + public fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FF)V + public synthetic fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class com/mapbox/maps/extension/androidauto/LogoWidget : com/mapbox/maps/renderer/widget/BitmapWidget { + public fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FF)V + public synthetic fun (Landroid/content/Context;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V +} + public abstract interface class com/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback { public abstract fun onMapSurfaceReady (Lcom/mapbox/maps/MapSurface;)V } @@ -14,9 +24,10 @@ public abstract interface class com/mapbox/maps/extension/androidauto/MapSurface public final class com/mapbox/maps/extension/androidauto/MapboxCarUtilsKt { public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V + public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Landroidx/car/app/SurfaceCallback;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V public static final fun initMapSurface (Landroidx/car/app/Session;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;)V - public static synthetic fun initMapSurface$default (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;ILjava/lang/Object;)V + public static synthetic fun initMapSurface$default (Landroidx/car/app/Session;Lcom/mapbox/maps/MapInitOptions;Lcom/mapbox/maps/extension/androidauto/OnMapScrollListener;Lcom/mapbox/maps/extension/androidauto/OnMapScaleListener;Landroidx/car/app/SurfaceCallback;Lcom/mapbox/maps/extension/androidauto/MapSurfaceReadyCallback;ILjava/lang/Object;)V } public abstract interface class com/mapbox/maps/extension/androidauto/OnMapScaleListener { diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt new file mode 100644 index 0000000000..25338337d0 --- /dev/null +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/CompassWidget.kt @@ -0,0 +1,30 @@ +package com.mapbox.maps.extension.androidauto + +import android.content.Context +import android.graphics.BitmapFactory +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.renderer.widget.BitmapWidget +import com.mapbox.maps.renderer.widget.WidgetPosition + +/** + * Widget shows compass. Positioned in the top right corner by default. + * + * @param position position of compass + * @param marginX horizontal margin in pixels + * @param marginY vertical margin in pixels + */ +@MapboxExperimental +class CompassWidget( + context: Context, + position: WidgetPosition = WidgetPosition( + horizontal = WidgetPosition.Horizontal.RIGHT, + vertical = WidgetPosition.Vertical.TOP, + ), + marginX: Float = 20f, + marginY: Float = 20f, +) : BitmapWidget( + bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_compass_icon), + position = position, + marginX = marginX, + marginY = marginY, +) \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt new file mode 100644 index 0000000000..f4bdbcf39b --- /dev/null +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/LogoWidget.kt @@ -0,0 +1,30 @@ +package com.mapbox.maps.extension.androidauto + +import android.content.Context +import android.graphics.BitmapFactory +import com.mapbox.maps.MapboxExperimental +import com.mapbox.maps.renderer.widget.BitmapWidget +import com.mapbox.maps.renderer.widget.WidgetPosition + +/** + * Widget shows compass. Positioned in the bottom left corner by default. + * + * @param position position of logo + * @param marginX horizontal margin in pixels + * @param marginY vertical margin in pixels + */ +@MapboxExperimental +class LogoWidget constructor( + context: Context, + position: WidgetPosition = WidgetPosition( + horizontal = WidgetPosition.Horizontal.LEFT, + vertical = WidgetPosition.Vertical.BOTTOM, + ), + marginX: Float = 20f, + marginY: Float = 20f, +) : BitmapWidget( + bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.mapbox_logo_icon), + position = position, + marginX = marginX, + marginY = marginY, +) \ No newline at end of file diff --git a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt index b1a1a02ad6..f19e9c3817 100644 --- a/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt +++ b/extension-androidauto/src/main/java/com/mapbox/maps/extension/androidauto/MapboxCarUtils.kt @@ -57,11 +57,13 @@ fun Session.initMapSurface( mapInitOptions: MapInitOptions = MapInitOptions(carContext), scrollListener: OnMapScrollListener? = null, scaleListener: OnMapScaleListener? = null, + callback: SurfaceCallback? = null, mapSurfaceReadyCallback: MapSurfaceReadyCallback ) { var mapSurface: MapSurface? = null val surfaceCallback: SurfaceCallback = object : SurfaceCallback { override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) { + callback?.onSurfaceAvailable(surfaceContainer) synchronized(this) { Logger.i(TAG, "Surface available $surfaceContainer") surfaceContainer.surface?.let { surface -> @@ -119,7 +121,6 @@ fun Session.initMapSurface( lifecycle.addObserver( object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { - Logger.i(TAG, "SurfaceRenderer created") synchronized(this) { carContext.getCarService(AppManager::class.java) .setSurfaceCallback(surfaceCallback) @@ -127,21 +128,18 @@ fun Session.initMapSurface( } override fun onStart(owner: LifecycleOwner) { - Logger.i(TAG, "onStart") synchronized(this) { mapSurface?.onStart() } } override fun onStop(owner: LifecycleOwner) { - Logger.i(TAG, "onStop") synchronized(this) { mapSurface?.onStop() } } override fun onDestroy(owner: LifecycleOwner) { - Logger.i(TAG, "onDestroy") synchronized(this) { mapSurface?.onDestroy() } @@ -151,7 +149,6 @@ fun Session.initMapSurface( } private fun MapSurface.onScroll(distanceX: Float, distanceY: Float) { - Logger.i(TAG, "handleScroll $distanceX, $distanceY") synchronized(this) { val centerScreen = ScreenCoordinate(0.0, 0.0) getMapboxMap().apply { @@ -170,7 +167,6 @@ private fun MapSurface.onScroll(distanceX: Float, distanceY: Float) { } private fun MapSurface.onScale(focusX: Float, focusY: Float, scaleFactor: Float) { - Logger.i(TAG, "handleScale $focusX, $focusY. $scaleFactor") synchronized(this) { val cameraState = getMapboxMap().cameraState Logger.i(TAG, "setting zoom ${cameraState.zoom * scaleFactor}") diff --git a/sdk/api/sdk.api b/sdk/api/sdk.api index 3cf2e6716f..ca4eae6783 100644 --- a/sdk/api/sdk.api +++ b/sdk/api/sdk.api @@ -9,11 +9,13 @@ public final class com/mapbox/maps/BuildConfig { } public abstract interface class com/mapbox/maps/MapControllable : com/mapbox/maps/MapboxLifecycleObserver { + public abstract fun addWidget (Lcom/mapbox/maps/renderer/widget/Widget;)V public abstract fun getMapboxMap ()Lcom/mapbox/maps/MapboxMap; public abstract fun onGenericMotionEvent (Landroid/view/MotionEvent;)Z public abstract fun onSizeChanged (II)V public abstract fun onTouchEvent (Landroid/view/MotionEvent;)Z public abstract fun queueEvent (Ljava/lang/Runnable;Z)V + public abstract fun removeWidget (Lcom/mapbox/maps/renderer/widget/Widget;)Z public abstract fun setMaximumFps (I)V public abstract fun setOnFpsChangedListener (Lcom/mapbox/maps/renderer/OnFpsChangedListener;)V public abstract fun snapshot ()Landroid/graphics/Bitmap; @@ -83,8 +85,10 @@ public final class com/mapbox/maps/MapSurface : com/mapbox/maps/MapControllable, public fun (Landroid/content/Context;Landroid/view/Surface;)V public fun (Landroid/content/Context;Landroid/view/Surface;Lcom/mapbox/maps/MapInitOptions;)V public synthetic fun (Landroid/content/Context;Landroid/view/Surface;Lcom/mapbox/maps/MapInitOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun addWidget (Lcom/mapbox/maps/renderer/widget/Widget;)V public fun getMapboxMap ()Lcom/mapbox/maps/MapboxMap; public fun getPlugin (Ljava/lang/String;)Lcom/mapbox/maps/plugin/MapPlugin; + public final fun getSurface ()Landroid/view/Surface; public fun onDestroy ()V public fun onGenericMotionEvent (Landroid/view/MotionEvent;)Z public fun onLowMemory ()V @@ -93,6 +97,7 @@ public final class com/mapbox/maps/MapSurface : com/mapbox/maps/MapControllable, public fun onStop ()V public fun onTouchEvent (Landroid/view/MotionEvent;)Z public fun queueEvent (Ljava/lang/Runnable;Z)V + public fun removeWidget (Lcom/mapbox/maps/renderer/widget/Widget;)Z public fun setMaximumFps (I)V public fun setOnFpsChangedListener (Lcom/mapbox/maps/renderer/OnFpsChangedListener;)V public fun snapshot ()Landroid/graphics/Bitmap; @@ -109,6 +114,7 @@ public class com/mapbox/maps/MapView : android/widget/FrameLayout, com/mapbox/ma public fun (Landroid/content/Context;Landroid/util/AttributeSet;I)V public fun (Landroid/content/Context;Lcom/mapbox/maps/MapInitOptions;)V public synthetic fun (Landroid/content/Context;Lcom/mapbox/maps/MapInitOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun addWidget (Lcom/mapbox/maps/renderer/widget/Widget;)V public final fun createPlugin (Lcom/mapbox/maps/plugin/Plugin;)V public fun getMapboxMap ()Lcom/mapbox/maps/MapboxMap; public fun getPlugin (Ljava/lang/String;)Lcom/mapbox/maps/plugin/MapPlugin; @@ -124,6 +130,7 @@ public class com/mapbox/maps/MapView : android/widget/FrameLayout, com/mapbox/ma public fun onStop ()V public fun onTouchEvent (Landroid/view/MotionEvent;)Z public fun queueEvent (Ljava/lang/Runnable;Z)V + public fun removeWidget (Lcom/mapbox/maps/renderer/widget/Widget;)Z public fun setMaximumFps (I)V public fun setOnFpsChangedListener (Lcom/mapbox/maps/renderer/OnFpsChangedListener;)V public fun snapshot ()Landroid/graphics/Bitmap; @@ -496,6 +503,46 @@ public final class com/mapbox/maps/renderer/RenderHandlerThread$sam$i$java_lang_ public final synthetic fun run ()V } +public class com/mapbox/maps/renderer/widget/BitmapWidget : com/mapbox/maps/renderer/widget/Widget { + public fun (Landroid/graphics/Bitmap;)V + public fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;)V + public fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;F)V + public fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FF)V + public synthetic fun (Landroid/graphics/Bitmap;Lcom/mapbox/maps/renderer/widget/WidgetPosition;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun getRenderer$sdk_release ()Lcom/mapbox/maps/renderer/widget/WidgetRenderer; + public fun setRotation (F)V + public fun setTranslation (FF)V + public final fun updateBitmap (Landroid/graphics/Bitmap;)V +} + +public abstract class com/mapbox/maps/renderer/widget/Widget { + public fun ()V + public abstract fun setRotation (F)V + public abstract fun setTranslation (FF)V +} + +public final class com/mapbox/maps/renderer/widget/WidgetPosition { + public fun (Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal;Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical;)V + public final fun getHorizontal ()Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public final fun getVertical ()Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; +} + +public final class com/mapbox/maps/renderer/widget/WidgetPosition$Horizontal : java/lang/Enum { + public static final field CENTER Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static final field LEFT Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static final field RIGHT Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static fun valueOf (Ljava/lang/String;)Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; + public static fun values ()[Lcom/mapbox/maps/renderer/widget/WidgetPosition$Horizontal; +} + +public final class com/mapbox/maps/renderer/widget/WidgetPosition$Vertical : java/lang/Enum { + public static final field BOTTOM Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static final field CENTER Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static final field TOP Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static fun valueOf (Ljava/lang/String;)Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; + public static fun values ()[Lcom/mapbox/maps/renderer/widget/WidgetPosition$Vertical; +} + public abstract interface class com/mapbox/maps/viewannotation/ViewAnnotationManager { public abstract fun addViewAnnotation (ILcom/mapbox/maps/ViewAnnotationOptions;)Landroid/view/View; public abstract fun addViewAnnotation (ILcom/mapbox/maps/ViewAnnotationOptions;Landroidx/asynclayoutinflater/view/AsyncLayoutInflater;Lkotlin/jvm/functions/Function1;)V diff --git a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt index ec5a74b4b1..ac1c8abc6e 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapControllable.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapControllable.kt @@ -3,6 +3,7 @@ package com.mapbox.maps import android.graphics.Bitmap import android.view.MotionEvent import com.mapbox.maps.renderer.OnFpsChangedListener +import com.mapbox.maps.renderer.widget.Widget /** * MapControllable interface is the gateway for public API to talk to the internal map controller. @@ -73,4 +74,18 @@ interface MapControllable : MapboxLifecycleObserver { * Set [OnFpsChangedListener] to get map rendering FPS. */ fun setOnFpsChangedListener(listener: OnFpsChangedListener) + + /** + * Add [Widget] to the map. + */ + @MapboxExperimental + fun addWidget(widget: Widget) + + /** + * Remove [Widget] from the map. + * + * @return true if widget was present and removed, false otherwise + */ + @MapboxExperimental + fun removeWidget(widget: Widget): Boolean } \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/MapController.kt b/sdk/src/main/java/com/mapbox/maps/MapController.kt index eec807afe9..9993bc2db5 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapController.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapController.kt @@ -41,6 +41,7 @@ import com.mapbox.maps.plugin.scalebar.ScaleBarPluginImpl import com.mapbox.maps.plugin.viewport.ViewportPluginImpl import com.mapbox.maps.renderer.MapboxRenderer import com.mapbox.maps.renderer.OnFpsChangedListener +import com.mapbox.maps.renderer.widget.Widget import java.lang.ref.WeakReference internal class MapController : MapPluginProviderDelegate, MapControllable { @@ -205,6 +206,12 @@ internal class MapController : MapPluginProviderDelegate, MapControllable { renderer.setOnFpsChangedListener(listener) } + override fun addWidget(widget: Widget) { + renderer.renderThread.addWidget(widget) + } + + override fun removeWidget(widget: Widget) = renderer.renderThread.removeWidget(widget) + // // Telemetry // diff --git a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt index 2d50e5e4f8..c304ea0517 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapSurface.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapSurface.kt @@ -8,6 +8,7 @@ import com.mapbox.maps.plugin.MapPlugin import com.mapbox.maps.plugin.delegates.MapPluginProviderDelegate import com.mapbox.maps.renderer.MapboxSurfaceRenderer import com.mapbox.maps.renderer.OnFpsChangedListener +import com.mapbox.maps.renderer.widget.Widget /** * A [MapSurface] provides an embeddable map interface. @@ -27,7 +28,7 @@ import com.mapbox.maps.renderer.OnFpsChangedListener */ class MapSurface @JvmOverloads constructor( context: Context, - private val surface: Surface, + val surface: Surface, mapInitOptions: MapInitOptions = MapInitOptions(context) ) : MapPluginProviderDelegate, MapControllable { @@ -178,6 +179,22 @@ class MapSurface @JvmOverloads constructor( mapController.onLowMemory() } + /** + * Add [Widget] to the map. + */ + @MapboxExperimental + override fun addWidget(widget: Widget) { + mapController.addWidget(widget) + } + + /** + * Remove [Widget] from the map. + * + * @return true if widget was present and removed, false otherwise + */ + @MapboxExperimental + override fun removeWidget(widget: Widget) = mapController.removeWidget(widget) + /** * Get the plugin instance. * diff --git a/sdk/src/main/java/com/mapbox/maps/MapView.kt b/sdk/src/main/java/com/mapbox/maps/MapView.kt index c02233a3ff..29be22cde8 100644 --- a/sdk/src/main/java/com/mapbox/maps/MapView.kt +++ b/sdk/src/main/java/com/mapbox/maps/MapView.kt @@ -20,6 +20,7 @@ import com.mapbox.maps.renderer.MapboxSurfaceHolderRenderer import com.mapbox.maps.renderer.MapboxTextureViewRenderer import com.mapbox.maps.renderer.OnFpsChangedListener import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.widget.Widget import com.mapbox.maps.viewannotation.ViewAnnotationManager /** @@ -309,6 +310,22 @@ open class MapView : FrameLayout, MapPluginProviderDelegate, MapControllable { mapController.setOnFpsChangedListener(listener) } + /** + * Add [Widget] to the map. + */ + @MapboxExperimental + override fun addWidget(widget: Widget) { + mapController.addWidget(widget) + } + + /** + * Remove [Widget] from the map. + * + * @return true if widget was present and removed, false otherwise + */ + @MapboxExperimental + override fun removeWidget(widget: Widget) = mapController.removeWidget(widget) + /** * Interface for getting snapshot result [Bitmap]. */ diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt index e83392ee6a..f02a552b55 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderThread.kt @@ -9,6 +9,8 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import com.mapbox.common.Logger import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.gl.TextureRenderer +import com.mapbox.maps.renderer.widget.Widget import java.util.LinkedList import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.locks.ReentrantLock @@ -45,6 +47,10 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { private var width: Int = 0 private var height: Int = 0 + private val widgetRenderer: MapboxWidgetRenderer + private var widgetRenderCreated = false + private val widgetTextureRenderer: TextureRenderer + @Volatile @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var renderTimeNs = 0L @@ -84,26 +90,33 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { constructor( mapboxRenderer: MapboxRenderer, + mapboxWidgetRenderer: MapboxWidgetRenderer, translucentSurface: Boolean, antialiasingSampleCount: Int, ) { this.translucentSurface = translucentSurface this.mapboxRenderer = mapboxRenderer + this.widgetRenderer = mapboxWidgetRenderer this.eglCore = EGLCore(translucentSurface, antialiasingSampleCount) this.eglSurface = eglCore.eglNoSurface + this.widgetTextureRenderer = TextureRenderer() renderHandlerThread = RenderHandlerThread().apply { start() } } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) constructor( mapboxRenderer: MapboxRenderer, + mapboxWidgetRenderer: MapboxWidgetRenderer, handlerThread: RenderHandlerThread, - eglCore: EGLCore + eglCore: EGLCore, + widgetTextureRenderer: TextureRenderer, ) { this.translucentSurface = false this.mapboxRenderer = mapboxRenderer + this.widgetRenderer = mapboxWidgetRenderer this.renderHandlerThread = handlerThread this.eglCore = eglCore + this.widgetTextureRenderer = widgetTextureRenderer this.eglSurface = eglCore.eglNoSurface } @@ -201,14 +214,19 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { private fun checkSurfaceSizeChanged() { if (sizeChanged) { - mapboxRenderer.onSurfaceChanged( - width = width, - height = height - ) + mapboxRenderer.onSurfaceChanged(width = width, height = height) + widgetRenderer.onSurfaceChanged(width = width, height = height) sizeChanged = false } } + private fun checkWidgetRender() { + if (eglPrepared && !widgetRenderCreated && widgetRenderer.hasWidgets()) { + widgetRenderer.setSharedContext(eglCore.eglContext) + widgetRenderCreated = true + } + } + private fun draw() { val renderTimeNsCopy = renderTimeNs val currentTimeNs = SystemClock.elapsedRealtimeNanos() @@ -219,7 +237,22 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { postPrepareRenderFrame() return } - mapboxRenderer.render() + + if (widgetRenderer.hasWidgets()) { + if (widgetRenderer.needTextureUpdate) { + widgetRenderer.updateTexture() + eglCore.makeCurrent(eglSurface) + } + + mapboxRenderer.render() + + if (widgetRenderer.hasTexture()) { + widgetTextureRenderer.render(widgetRenderer.getTexture()) + } + } else { + mapboxRenderer.render() + } + // assuming render event queue holds user's runnables with OpenGL ES commands // it makes sense to execute them after drawing a map but before swapping buffers // **note** this queue also holds snapshot tasks @@ -259,9 +292,12 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } private fun releaseEglSurface() { + widgetTextureRenderer.release() eglCore.releaseSurface(eglSurface) eglContextCreated = false eglSurface = eglCore.eglNoSurface + widgetRenderCreated = false + widgetRenderer.release() } private fun releaseAll() { @@ -296,6 +332,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { return } } + checkWidgetRender() checkSurfaceSizeChanged() Choreographer.getInstance().postFrameCallback(this) awaitingNextVsync = true @@ -336,6 +373,12 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } } + fun addWidget(widget: Widget) { + widgetRenderer.addWidget(widget) + } + + fun removeWidget(widget: Widget) = widgetRenderer.removeWidget(widget) + @WorkerThread internal fun processAndroidSurface(surface: Surface, width: Int, height: Int) { if (this.surface != surface) { @@ -347,6 +390,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { } this.width = width this.height = height + widgetRenderer.onSurfaceChanged(width = width, height = height) renderEventQueue.removeAll { it.eventType == EventType.SDK } nonRenderEventQueue.removeAll { it.eventType == EventType.SDK } // we do not want to clear render events scheduled by user @@ -485,6 +529,7 @@ internal class MapboxRenderThread : Choreographer.FrameCallback { companion object { private const val TAG = "Mbgl-RenderThread" + private val ONE_SECOND_NS = 10.0.pow(9.0).toLong() private val ONE_MILLISECOND_NS = 10.0.pow(6.0).toLong() diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt index 54699196f7..a1ad1f7d55 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxRenderer.kt @@ -20,6 +20,7 @@ internal abstract class MapboxRenderer : MapClient { @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) internal lateinit var renderThread: MapboxRenderThread + internal abstract val widgetRenderer: MapboxWidgetRenderer @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var map: MapInterface? = null diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt index 5d92511607..246c1eb1aa 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxSurfaceRenderer.kt @@ -7,9 +7,15 @@ internal open class MapboxSurfaceRenderer : MapboxRenderer { private var createSurface = false + override val widgetRenderer: MapboxWidgetRenderer + constructor(antialiasingSampleCount: Int) { + widgetRenderer = MapboxWidgetRenderer( + antialiasingSampleCount = antialiasingSampleCount, + ) renderThread = MapboxRenderThread( mapboxRenderer = this, + mapboxWidgetRenderer = widgetRenderer, translucentSurface = false, antialiasingSampleCount = antialiasingSampleCount, ) @@ -17,6 +23,9 @@ internal open class MapboxSurfaceRenderer : MapboxRenderer { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(renderThread: MapboxRenderThread) { + widgetRenderer = MapboxWidgetRenderer( + antialiasingSampleCount = 1, + ) this.renderThread = renderThread } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt index cbf7bf8375..668b55cf14 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxTextureViewRenderer.kt @@ -7,9 +7,16 @@ import androidx.annotation.VisibleForTesting internal class MapboxTextureViewRenderer : MapboxRenderer, TextureView.SurfaceTextureListener { + override val widgetRenderer: MapboxWidgetRenderer + constructor(textureView: TextureView, antialiasingSampleCount: Int) { + val widgetRenderer = MapboxWidgetRenderer( + antialiasingSampleCount = antialiasingSampleCount, + ) + this.widgetRenderer = widgetRenderer renderThread = MapboxRenderThread( mapboxRenderer = this, + mapboxWidgetRenderer = widgetRenderer, translucentSurface = true, antialiasingSampleCount = antialiasingSampleCount, ) @@ -21,6 +28,10 @@ internal class MapboxTextureViewRenderer : MapboxRenderer, TextureView.SurfaceTe @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal constructor(renderThread: MapboxRenderThread) { + val widgetRenderer = MapboxWidgetRenderer( + antialiasingSampleCount = 1, + ) + this.widgetRenderer = widgetRenderer this.renderThread = renderThread } diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt new file mode 100644 index 0000000000..237f986d23 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/MapboxWidgetRenderer.kt @@ -0,0 +1,202 @@ +package com.mapbox.maps.renderer + +import android.opengl.GLES20 +import com.mapbox.common.Logger +import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.widget.Widget +import javax.microedition.khronos.egl.EGLContext +import javax.microedition.khronos.egl.EGLSurface + +internal class MapboxWidgetRenderer( + private val antialiasingSampleCount: Int, +) { + private var eglCore: EGLCore? = null + private var eglPrepared = false + private var eglSurface: EGLSurface? = null + private var sizeChanged = false + + private val textures = intArrayOf(0) + private val framebuffers = intArrayOf(0) + + private val widgets = mutableListOf() + + private var width = 0 + private var height = 0 + + val needTextureUpdate: Boolean + get() = widgets.any { it.renderer.needRender } + + fun hasWidgets() = widgets.isNotEmpty() + + fun hasTexture() = textures[0] != 0 + + fun getTexture() = textures[0] + + fun setSharedContext(sharedContext: EGLContext) { + if (eglPrepared) { + release() + } + eglCore = EGLCore( + translucentSurface = false, + antialiasingSampleCount = antialiasingSampleCount, + sharedContext = sharedContext, + ) + } + + fun onSurfaceChanged(width: Int, height: Int) { + sizeChanged = true + this.width = width + this.height = height + widgets.forEach { it.renderer.onSurfaceChanged(width, height) } + } + + private fun attachTexture() { + if (textures[0] != 0) { + GLES20.glDeleteTextures(textures.size, textures, 0) + } + GLES20.glGenTextures(1, textures, 0) + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLES20.glTexImage2D( + GLES20.GL_TEXTURE_2D, + 0, + GLES20.GL_RGBA, + width, + height, + 0, + GLES20.GL_RGBA, + GLES20.GL_UNSIGNED_BYTE, + null + ) + GLES20.glFramebufferTexture2D( + GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_TEXTURE_2D, textures[0], 0 + ) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) + } + + fun release() { + val eglCore = this.eglCore + val eglSurface = this.eglSurface + if (eglCore != null) { + if (eglSurface != null && eglSurface != eglCore.eglNoSurface) { + eglCore.makeCurrent(eglSurface) + + GLES20.glDeleteFramebuffers(framebuffers.size, framebuffers, 0) + GLES20.glDeleteTextures(textures.size, textures, 0) + widgets.forEach { + it.renderer.release() + } + widgets.clear() + + eglCore.releaseSurface(eglSurface) + } + + eglCore.release() + } + this.eglSurface = null + this.eglCore = null + } + + fun updateTexture() { + if (needTextureUpdate) { + checkSizeChanged() + checkEgl() + val eglCore = this.eglCore + val eglSurface = this.eglSurface + if (eglCore != null && eglSurface != null && eglSurface != eglCore.eglNoSurface) { + eglCore.makeCurrent(eglSurface) + bindFramebuffer() + attachTexture() + widgets.forEach { + it.renderer.render() + } + unbindFramebuffer() + } + } + } + + private fun checkSizeChanged() { + if (sizeChanged) { + val eglCore = this.eglCore + val eglSurface = this.eglSurface + if (eglCore != null && eglSurface != null && eglSurface != eglCore.eglNoSurface) { + eglCore.releaseSurface(eglSurface) + this.eglSurface = eglCore.eglNoSurface + } + + sizeChanged = false + } + } + + private fun unbindFramebuffer() { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0) + } + + private fun bindFramebuffer() { + if (framebuffers[0] == 0) { + GLES20.glGenFramebuffers(1, framebuffers, 0) + } + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffers[0]) + } + + private fun checkEgl() { + val eglSurface = this.eglSurface + val eglCore = this.eglCore + + if (eglCore == null) { + Logger.e(TAG, "Cannot prepare egl, eglCore has not been initialized yet.") + return + } + if (eglSurface != null && eglSurface != eglCore.eglNoSurface) { + return + } + + if (!eglPrepared) { + eglPrepared = eglCore.prepareEgl() + if (!eglPrepared) { + Logger.e(TAG, "Widget EGL was not configured, please check logs above.") + return + } + } + + if (eglSurface == null || eglSurface == eglCore.eglNoSurface) { + this.eglSurface = eglCore.createOffscreenSurface(width = width, height = height) + if (eglSurface == eglCore.eglNoSurface) { + Logger.e(TAG, "Widget offscreen surface was not configured, please check logs above.") + return + } + } + } + + fun addWidget(widget: Widget) { + widget.renderer.onSurfaceChanged(width, height) + widgets.add(widget) + } + + fun removeWidget(widget: Widget) = widgets.remove(widget) + + private companion object { + const val TAG: String = "MapboxWidgetRenderer" + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt b/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt index 50569d4232..d1601d7667 100644 --- a/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt +++ b/sdk/src/main/java/com/mapbox/maps/renderer/egl/EGLCore.kt @@ -14,11 +14,12 @@ import javax.microedition.khronos.egl.* internal class EGLCore( private val translucentSurface: Boolean, private val antialiasingSampleCount: Int, + private val sharedContext: EGLContext = EGL10.EGL_NO_CONTEXT, ) { private lateinit var egl: EGL10 private lateinit var eglConfig: EGLConfig private var eglDisplay: EGLDisplay = EGL10.EGL_NO_DISPLAY - private var eglContext: EGLContext = EGL10.EGL_NO_CONTEXT + internal var eglContext: EGLContext = EGL10.EGL_NO_CONTEXT internal val eglNoSurface: EGLSurface = EGL10.EGL_NO_SURFACE @@ -45,7 +46,7 @@ internal class EGLCore( val context = egl.eglCreateContext( eglDisplay, eglConfig, - EGL10.EGL_NO_CONTEXT, + sharedContext, intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE) ) val contextCreated = checkEglErrorNoException("eglCreateContext") @@ -113,6 +114,25 @@ internal class EGLCore( return eglSurface } + /** + * Creates an EGL surface associated with an offscreen buffer. + */ + fun createOffscreenSurface(width: Int, height: Int): EGLSurface { + val surfaceAttribs = + intArrayOf(EGL10.EGL_WIDTH, width, EGL10.EGL_HEIGHT, height, EGL10.EGL_NONE) + val eglSurface = egl.eglCreatePbufferSurface( + eglDisplay, + eglConfig, + surfaceAttribs + ) + checkEglErrorNoException("eglCreatePbufferSurface") + if (eglSurface == null) { + Logger.e(TAG, "Offscreen surface was null") + return eglNoSurface + } + return eglSurface + } + /** * Makes no context current. */ @@ -145,7 +165,7 @@ internal class EGLCore( } /** - * Calls eglSwapBuffers. Use this to "publish" the current frame. + * Calls eglSwapBuffers. Use this to "publish" the current frame. */ fun swapBuffers(eglSurface: EGLSurface): Int { val swapStatus = egl.eglSwapBuffers(eglDisplay, eglSurface) diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt new file mode 100644 index 0000000000..7e83629282 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/GlUtils.kt @@ -0,0 +1,72 @@ +package com.mapbox.maps.renderer.gl + +import android.opengl.GLES20 +import android.opengl.Matrix +import com.mapbox.maps.BuildConfig +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.FloatBuffer + +internal object GlUtils { + + fun FloatArray.put(vararg values: Float) { + values.forEachIndexed { index, value -> + this[index] = value + } + } + + fun FloatBuffer.put(vararg values: Float) { + rewind() + values.forEach { value -> + this.put(value) + } + rewind() + } + + fun FloatArray.toFloatBuffer(): FloatBuffer = ByteBuffer.allocateDirect(size * 4) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer().also { + it.put(this@toFloatBuffer) + it.rewind() + } + + /** + * @param type GLES20.GL_VERTEX_SHADER or GLES20.GL_FRAGMENT_SHADER + */ + fun loadShader(type: Int, shaderCode: String) = + GLES20.glCreateShader(type).also { shader -> + GLES20.glShaderSource(shader, shaderCode) + GLES20.glCompileShader(shader) + } + + fun checkError(cmd: String? = null) { + if (BuildConfig.DEBUG) { + when (val error = GLES20.glGetError()) { + GLES20.GL_NO_ERROR -> {} + else -> throw java.lang.RuntimeException( + "$cmd - error in GL : ${when (error) { + GLES20.GL_INVALID_ENUM -> "GL_INVALID_ENUM" + GLES20.GL_INVALID_VALUE -> "GL_INVALID_VALUE" + GLES20.GL_INVALID_OPERATION -> "GL_INVALID_OPERATION" + GLES20.GL_INVALID_FRAMEBUFFER_OPERATION -> "GL_INVALID_FRAMEBUFFER_OPERATION" + GLES20.GL_OUT_OF_MEMORY -> "GL_OUT_OF_MEMORY" + else -> error + }}" + ) + } + } + } + + fun checkCompileStatus(shader: Int) { + if (BuildConfig.DEBUG) { + val isCompiled = IntArray(1) + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, isCompiled, 0) + if (isCompiled[0] == GLES20.GL_FALSE) { + val infoLog = GLES20.glGetShaderInfoLog(shader) + throw RuntimeException("checkCompileStatus error: $infoLog") + } + } + } + + fun getIdentityMatrix(): FloatArray = FloatArray(16).also { Matrix.setIdentityM(it, 0) } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt new file mode 100644 index 0000000000..2d44bf2511 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/gl/TextureRenderer.kt @@ -0,0 +1,157 @@ +package com.mapbox.maps.renderer.gl + +import android.opengl.GLES20 +import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer + +internal class TextureRenderer( + private val depth: Float = 0f +) { + + private var program: Int = 0 + private var attributePosition: Int = 0 + private var attributeTextureCoord: Int = 0 + private var uniformTexture: Int = 0 + private var vbo: IntArray = IntArray(2) + + fun prepare() { + setupVbo( + vertexArray = floatArrayOf( + -1f, -1f, depth, + 1f, -1f, depth, + -1f, 1f, depth, + 1f, 1f, depth, + ), + textureArray = floatArrayOf( + 0f, 0f, + 1f, 0f, + 0f, 1f, + 1f, 1f, + ) + ) + + val vertexShader = GlUtils.loadShader( + GLES20.GL_VERTEX_SHADER, + VERTEX_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + val fragmentShader = GlUtils.loadShader( + GLES20.GL_FRAGMENT_SHADER, + FRAGMENT_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + program = GLES20.glCreateProgram().also { + GlUtils.checkError("glCreateProgram") + GLES20.glAttachShader(it, vertexShader) + GlUtils.checkError("glAttachShader") + + GLES20.glAttachShader(it, fragmentShader) + GlUtils.checkError("glAttachShader") + + GLES20.glLinkProgram(it) + GlUtils.checkError("glLinkProgram") + } + attributePosition = GLES20.glGetAttribLocation(program, "aPosition") + attributeTextureCoord = GLES20.glGetAttribLocation(program, "aTexCoord") + + uniformTexture = GLES20.glGetUniformLocation(program, "uTexture") + } + + fun render(textureID: Int) { + if (program == 0) { + prepare() + } + // Reset to guarantee widgets are drawn on top of map + GLES20.glDisable(GLES20.GL_STENCIL_TEST) + + GLES20.glUseProgram(program) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]) + GLES20.glVertexAttribPointer( + attributePosition, + COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + VERTEX_STRIDE, + 0 + ) + GLES20.glEnableVertexAttribArray(attributePosition) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[1]) + GLES20.glVertexAttribPointer( + attributeTextureCoord, + COORDS_PER_TEX, + GLES20.GL_FLOAT, + false, + TEX_STRIDE, + 0 + ) + GLES20.glEnableVertexAttribArray(attributeTextureCoord) + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureID) + GLES20.glUniform1i(uniformTexture, 0) + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) + + GLES20.glDisableVertexAttribArray(attributePosition) + GLES20.glDisableVertexAttribArray(attributeTextureCoord) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + GLES20.glUseProgram(0) + } + + fun release() { + if (program != 0) { + GLES20.glDeleteBuffers(vbo.size, vbo, 0) + GLES20.glDeleteProgram(program) + program = 0 + } + } + + private fun setupVbo(vertexArray: FloatArray, textureArray: FloatArray) { + GLES20.glGenBuffers(2, vbo, 0) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[0]) + GLES20.glBufferData( + GLES20.GL_ARRAY_BUFFER, + vertexArray.size * BYTES_PER_FLOAT, + vertexArray.toFloatBuffer(), + GLES20.GL_STATIC_DRAW + ) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vbo[1]) + GLES20.glBufferData( + GLES20.GL_ARRAY_BUFFER, + textureArray.size * BYTES_PER_FLOAT, + textureArray.toFloatBuffer(), + GLES20.GL_STATIC_DRAW + ) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + } + + private companion object { + const val COORDS_PER_VERTEX = 3 + const val COORDS_PER_TEX = 2 + const val BYTES_PER_FLOAT = 4 + const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT + const val TEX_STRIDE = COORDS_PER_TEX * BYTES_PER_FLOAT + const val VERTEX_COUNT = 4 + + val VERTEX_SHADER_CODE = """ + precision highp float; + attribute vec4 aPosition; + attribute vec2 aTexCoord; + varying vec2 vTexCoord; + void main() + { + gl_Position = aPosition; + vTexCoord = aTexCoord; + } + """.trimIndent() + + val FRAGMENT_SHADER_CODE = """ + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uTexture; + void main() + { + gl_FragColor = texture2D(uTexture, vTexCoord); + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt new file mode 100644 index 0000000000..ac24d4f65b --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidget.kt @@ -0,0 +1,45 @@ +package com.mapbox.maps.renderer.widget + +import android.graphics.Bitmap +import com.mapbox.maps.MapboxExperimental + +/** + * Widget displaying bitmap within specified position and margins. + * + * @param bitmap bitmap used to draw widget + * @param position position of widget + * @param marginX horizontal margin in pixels + * @param marginY vertical margin in pixels + */ +@MapboxExperimental +open class BitmapWidget @JvmOverloads constructor( + bitmap: Bitmap, + position: WidgetPosition = WidgetPosition( + vertical = WidgetPosition.Vertical.TOP, + horizontal = WidgetPosition.Horizontal.LEFT, + ), + marginX: Float = 0f, + marginY: Float = 0f, +) : Widget() { + override val renderer = BitmapWidgetRenderer( + bitmap = bitmap, + position = position, + marginX = marginX, + marginY = marginY, + ) + + /** + * Update bitmap widget uses. + */ + fun updateBitmap(bitmap: Bitmap) { + renderer.updateBitmap(bitmap) + } + + override fun setTranslation(translationX: Float, translationY: Float) { + renderer.setTranslation(translationX = translationX, translationY = translationY) + } + + override fun setRotation(angleDegrees: Float) { + renderer.setRotation(angleDegrees = angleDegrees) + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt new file mode 100644 index 0000000000..4381847571 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/BitmapWidgetRenderer.kt @@ -0,0 +1,318 @@ +package com.mapbox.maps.renderer.widget + +import android.graphics.Bitmap +import android.opengl.GLES20 +import android.opengl.GLUtils +import android.opengl.Matrix +import com.mapbox.maps.renderer.gl.GlUtils +import com.mapbox.maps.renderer.gl.GlUtils.put +import com.mapbox.maps.renderer.gl.GlUtils.toFloatBuffer + +internal class BitmapWidgetRenderer( + @Volatile + private var bitmap: Bitmap?, + private val position: WidgetPosition, + private val marginX: Float, + private val marginY: Float, +) : WidgetRenderer { + + private var bitmapWidth = bitmap?.width ?: 0 + private var bitmapHeight = bitmap?.height ?: 0 + + private var surfaceWidth = 0 + private var surfaceHeight = 0 + + private var program = 0 + private var vertexShader = 0 + private var fragmentShader = 0 + + private var attributeVertexPosition = 0 + private var attributeTexturePosition = 0 + private var uniformTexture = 0 + private var uniformMvpMatrix = 0 + private val textures = intArrayOf(0) + + private var screenMatrix = FloatArray(16) + private var translateRotate = FloatArray(16) + private val rotationMatrix = GlUtils.getIdentityMatrix() + private val translateMatrix = GlUtils.getIdentityMatrix() + private val mvpMatrix = GlUtils.getIdentityMatrix() + private val mvpMatrixBuffer = mvpMatrix.toFloatBuffer() + + private var updateMatrix: Boolean = true + + private val vertexPositionBuffer = FloatArray(8).toFloatBuffer() + private val texturePositionBuffer = floatArrayOf( + 0f, 0f, + 0f, 1f, + 1f, 0f, + 1f, 1f + ).toFloatBuffer() + + override var needRender: Boolean = true + + override fun onSurfaceChanged(width: Int, height: Int) { + surfaceWidth = width + surfaceHeight = height + + // transforms from (0,0) - (width, height) in screen pixels + // to (-1, -1) - (1, 1) for GL + screenMatrix.put( + 2f / width, 0f, 0f, 0f, + 0f, -2f / height, 0f, 0f, + 0f, 0f, 0f, 0f, + -1f, 1f, 0f, 1f + ) + + Matrix.translateM( + translateMatrix, + 0, + leftX(), + topY(), + 0f + ) + + updateVertexBuffer() + + updateMatrix = true + needRender = true + } + + private fun updateVertexBuffer() { + // in pixels, (-bitmapWidth / 2, -bitmapHeight/2) - (bitmapWidth / 2, bitmapHeight/2) + vertexPositionBuffer.put( + -bitmapWidth / 2f, -bitmapHeight / 2f, + -bitmapWidth / 2f, bitmapHeight / 2f, + bitmapWidth / 2f, -bitmapHeight / 2f, + bitmapWidth / 2f, bitmapHeight / 2f, + ) + } + + private fun topY() = when (position.vertical) { + WidgetPosition.Vertical.BOTTOM -> surfaceHeight.toFloat() - bitmapHeight.toFloat() / 2f - marginY + WidgetPosition.Vertical.CENTER -> surfaceHeight.toFloat() / 2 - bitmapHeight.toFloat() / 2f + marginY + WidgetPosition.Vertical.TOP -> marginY + bitmapHeight.toFloat() / 2f + } + + private fun leftX() = when (position.horizontal) { + WidgetPosition.Horizontal.LEFT -> marginX + bitmapWidth.toFloat() / 2f + WidgetPosition.Horizontal.CENTER -> surfaceWidth.toFloat() / 2 - bitmapWidth.toFloat() / 2f + marginX + WidgetPosition.Horizontal.RIGHT -> surfaceWidth.toFloat() - bitmapWidth.toFloat() / 2f - marginX + } + + override fun prepare() { + val maxAttrib = IntArray(1) + GLES20.glGetIntegerv(GLES20.GL_MAX_VERTEX_ATTRIBS, maxAttrib, 0) + + vertexShader = GlUtils.loadShader( + GLES20.GL_VERTEX_SHADER, + VERTEX_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + fragmentShader = GlUtils.loadShader( + GLES20.GL_FRAGMENT_SHADER, + FRAGMENT_SHADER_CODE + ).also(GlUtils::checkCompileStatus) + + program = GLES20.glCreateProgram().also { program -> + GlUtils.checkError("glCreateProgram") + + GLES20.glAttachShader(program, vertexShader) + GlUtils.checkError("glAttachShader") + + GLES20.glAttachShader(program, fragmentShader) + GlUtils.checkError("glAttachShader") + + GLES20.glLinkProgram(program) + GlUtils.checkError("glLinkProgram") + } + + uniformMvpMatrix = + GLES20.glGetUniformLocation(program, "uMvpMatrix") + GlUtils.checkError("glGetUniformLocation") + + attributeVertexPosition = + GLES20.glGetAttribLocation(program, "aPosition") + GlUtils.checkError("glGetAttribLocation") + + attributeTexturePosition = + GLES20.glGetAttribLocation(program, "aCoordinate") + GlUtils.checkError("glGetAttribLocation") + + uniformTexture = + GLES20.glGetUniformLocation(program, "uTexture") + GlUtils.checkError("glGetUniformLocation") + + needRender = true + } + + override fun render() { + if (program == 0) { + prepare() + } + GLES20.glUseProgram(program) + GlUtils.checkError("glUseProgram") + + if (updateMatrix) { + Matrix.setIdentityM(mvpMatrix, 0) + + Matrix.multiplyMM(translateRotate, 0, translateMatrix, 0, rotationMatrix, 0) + Matrix.multiplyMM(mvpMatrix, 0, screenMatrix, 0, translateRotate, 0) + + mvpMatrixBuffer.rewind() + mvpMatrixBuffer.put(mvpMatrix) + mvpMatrixBuffer.rewind() + + updateMatrix = false + } + + GLES20.glUniformMatrix4fv(uniformMvpMatrix, 1, false, mvpMatrixBuffer) + + textureFromBitmapIfChanged() + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + + GLES20.glUniform1i(uniformTexture, 0) + + GLES20.glClearColor(1f, 1f, 1f, 1f) + + GLES20.glEnableVertexAttribArray(attributeVertexPosition) + GlUtils.checkError("glEnableVertexAttribArray") + + GLES20.glVertexAttribPointer( + attributeVertexPosition, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + VERTEX_STRIDE, vertexPositionBuffer + ) + GlUtils.checkError("glVertexAttribPointer") + + GLES20.glEnableVertexAttribArray(attributeTexturePosition) + GlUtils.checkError("glEnableVertexAttribArray") + + GLES20.glVertexAttribPointer( + attributeTexturePosition, COORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, + VERTEX_STRIDE, texturePositionBuffer + ) + GlUtils.checkError("glVertexAttribPointer") + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT) + GlUtils.checkError("glDrawArrays") + + GLES20.glDisableVertexAttribArray(attributeVertexPosition) + GLES20.glDisableVertexAttribArray(attributeTexturePosition) + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) + GLES20.glUseProgram(0) + + needRender = false + } + + override fun release() { + if (program != 0) { + GLES20.glDisableVertexAttribArray(attributeVertexPosition) + GLES20.glDetachShader(program, vertexShader) + GLES20.glDetachShader(program, fragmentShader) + GLES20.glDeleteShader(vertexShader) + GLES20.glDeleteShader(fragmentShader) + GLES20.glDeleteTextures(textures.size, textures, 0) + GLES20.glDeleteProgram(program) + program = 0 + } + needRender = false + } + + /** + * Updates texture from bitmap once and nullifies bitmap. + */ + private fun textureFromBitmapIfChanged() { + bitmap?.let { + if (textures[0] == 0) { + GLES20.glGenTextures(1, textures, 0) + } + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLES20.glTexParameterf( + GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE.toFloat() + ) + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, it, 0) + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0) + + bitmap = null + } + } + + fun updateBitmap(bitmap: Bitmap) { + this.bitmap = bitmap + this.bitmapWidth = bitmap.width + this.bitmapHeight = bitmap.height + updateVertexBuffer() + updateMatrix = true + needRender = true + } + + override fun setRotation(angleDegrees: Float) { + Matrix.setIdentityM(rotationMatrix, 0) + Matrix.setRotateM(rotationMatrix, 0, angleDegrees, 0f, 0f, 1f) + updateMatrix = true + needRender = true + } + + override fun setTranslation(translationX: Float, translationY: Float) { + Matrix.setIdentityM(translateMatrix, 0) + Matrix.translateM( + translateMatrix, + 0, + leftX() + translationX, + topY() + translationY, + 0f + ) + + updateMatrix = true + needRender = true + } + + private companion object { + const val COORDS_PER_VERTEX = 2 + const val BYTES_PER_FLOAT = 4 + const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT + const val VERTEX_COUNT = 4 + + val VERTEX_SHADER_CODE = """ + precision highp float; + uniform mat4 uMvpMatrix; + attribute vec2 aPosition; + attribute vec2 aCoordinate; + varying vec2 vCoordinate; + void main() { + vCoordinate = aCoordinate; + gl_Position = uMvpMatrix * vec4(aPosition, 0.0, 1.0); + } + """.trimIndent() + + val FRAGMENT_SHADER_CODE = """ + precision mediump float; + uniform sampler2D uTexture; + varying vec2 vCoordinate; + void main() { + gl_FragColor = texture2D(uTexture, vCoordinate); + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt new file mode 100644 index 0000000000..de792d176b --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/Widget.kt @@ -0,0 +1,21 @@ +package com.mapbox.maps.renderer.widget + +import com.mapbox.maps.MapboxExperimental + +/** + * Base class for widgets displayed on top of the map. + */ +@MapboxExperimental +abstract class Widget { + internal abstract val renderer: WidgetRenderer + + /** + * Set absolute translation of widget in pixels. + */ + abstract fun setTranslation(translateX: Float, translateY: Float) + + /** + * Set absolute rotation of widget in angles. + */ + abstract fun setRotation(angleDegrees: Float) +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt new file mode 100644 index 0000000000..127d01cb6d --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetPosition.kt @@ -0,0 +1,24 @@ +package com.mapbox.maps.renderer.widget + +import com.mapbox.maps.MapboxExperimental + +/** + * Specifies widget position relative to the screen. + */ +@MapboxExperimental +class WidgetPosition( + val horizontal: Horizontal, + val vertical: Vertical, +) { + enum class Horizontal { + LEFT, + CENTER, + RIGHT, + } + + enum class Vertical { + TOP, + CENTER, + BOTTOM, + } +} \ No newline at end of file diff --git a/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt new file mode 100644 index 0000000000..f0bac97087 --- /dev/null +++ b/sdk/src/main/java/com/mapbox/maps/renderer/widget/WidgetRenderer.kt @@ -0,0 +1,13 @@ +package com.mapbox.maps.renderer.widget + +internal interface WidgetRenderer { + val needRender: Boolean + + fun onSurfaceChanged(width: Int, height: Int) + fun prepare() + fun render() + fun release() + + fun setRotation(angleDegrees: Float) + fun setTranslation(translationX: Float, translationY: Float) +} \ No newline at end of file diff --git a/sdk/src/test/java/com/mapbox/TestUtils.kt b/sdk/src/test/java/com/mapbox/TestUtils.kt new file mode 100644 index 0000000000..444bd17178 --- /dev/null +++ b/sdk/src/test/java/com/mapbox/TestUtils.kt @@ -0,0 +1,40 @@ +package com.mapbox + +import io.mockk.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +internal fun verifyNo( + ordering: Ordering = Ordering.UNORDERED, + timeout: Long = 0, + verifyBlock: MockKVerificationScope.() -> Unit +) = verify( + ordering = ordering, + exactly = 0, + timeout = timeout, + verifyBlock = verifyBlock, +) + +internal fun verifyOnce( + ordering: Ordering = Ordering.UNORDERED, + timeout: Long = 0, + verifyBlock: MockKVerificationScope.() -> Unit +) = verify( + ordering = ordering, + exactly = 1, + timeout = timeout, + verifyBlock = verifyBlock, +) + +internal fun waitZeroCounter(startCounter: Int = 1, timeoutMillis: Int = 1000, runnable: CountDownLatch.() -> Unit) { + val countDownLatch = CountDownLatch(startCounter) + runnable(countDownLatch) + if (!countDownLatch.await(timeoutMillis.toLong(), TimeUnit.MILLISECONDS)) { + throw TimeoutException("Test had failed, counter is not zero but $startCounter after $timeoutMillis milliseconds!") + } +} + +internal fun CountDownLatch.countDownEvery(stubBlock: MockKMatcherScope.() -> Unit) { + every(stubBlock).answers { countDown() } +} \ No newline at end of file diff --git a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt index 7b2da78bfa..24d4ea1b9a 100644 --- a/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt +++ b/sdk/src/test/java/com/mapbox/maps/renderer/MapboxRenderThreadTest.kt @@ -2,12 +2,17 @@ package com.mapbox.maps.renderer import android.view.Surface import com.mapbox.common.ShadowLogger +import com.mapbox.countDownEvery import com.mapbox.maps.renderer.MapboxRenderThread.Companion.RETRY_DELAY_MS import com.mapbox.maps.renderer.egl.EGLCore +import com.mapbox.maps.renderer.gl.TextureRenderer +import com.mapbox.verifyNo +import com.mapbox.verifyOnce +import com.mapbox.waitZeroCounter import io.mockk.* import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Before +import org.junit.Assert.assertFalse import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -16,8 +21,8 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLContext @RunWith(RobolectricTestRunner::class) @Config(shadows = [ShadowLogger::class]) @@ -26,58 +31,78 @@ class MapboxRenderThreadTest { private lateinit var mapboxRenderThread: MapboxRenderThread private lateinit var mapboxRenderer: MapboxRenderer + private lateinit var mapboxWidgetRenderer: MapboxWidgetRenderer private lateinit var eglCore: EGLCore private lateinit var renderHandlerThread: RenderHandlerThread - private val waitTime = 200L + private lateinit var textureRenderer: TextureRenderer + private lateinit var surface: Surface - private fun mockValidSurface(): Surface { - val surface = mockk() + private fun initRenderThread(mapboxRenderer: MapboxRenderer = mockk(relaxUnitFun = true)) { + this.mapboxRenderer = mapboxRenderer + mockEglCore() + mockWidgetRenderer() + renderHandlerThread = RenderHandlerThread() + textureRenderer = mockk(relaxed = true) + mapboxRenderThread = MapboxRenderThread( + mapboxRenderer, + mapboxWidgetRenderer, + renderHandlerThread, + eglCore, + textureRenderer, + ) + renderHandlerThread.start() + } + + private fun mockSurface() { + surface = mockk() every { surface.isValid } returns true every { surface.release() } just Runs + } + + private fun mockWidgetRenderer() { + mapboxWidgetRenderer = mockk(relaxUnitFun = true) + every { mapboxWidgetRenderer.getTexture() } returns 0 + every { mapboxWidgetRenderer.hasTexture() } returns false + every { mapboxWidgetRenderer.needTextureUpdate } returns false + every { mapboxWidgetRenderer.hasWidgets() } returns false + } + + private fun mockEglCore() { + eglCore = mockk(relaxUnitFun = true) + every { eglCore.eglNoSurface } returns mockk() + every { eglCore.eglContext } returns mockk() every { eglCore.prepareEgl() } returns true every { eglCore.createWindowSurface(any()) } returns mockk(relaxed = true) every { eglCore.makeNothingCurrent() } returns true every { eglCore.makeCurrent(any()) } returns true every { eglCore.swapBuffers(any()) } returns EGL10.EGL_SUCCESS + } + + private fun provideValidSurface() { + mockSurface() mapboxRenderThread.onSurfaceCreated(surface, 1, 1) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - return surface + idleHandler() } private fun mockCountdownRunnable(latch: CountDownLatch) = mockk(relaxUnitFun = true).also { every { it.run() } answers { latch.countDown() } } - @Before - fun setUp() { - mapboxRenderer = mockk(relaxUnitFun = true) - eglCore = mockk(relaxUnitFun = true) - every { eglCore.eglNoSurface } returns mockk() - renderHandlerThread = RenderHandlerThread() - mapboxRenderThread = MapboxRenderThread( - mapboxRenderer, - renderHandlerThread, - eglCore - ).apply { - renderHandlerThread.start() - } - } + private fun pauseHandler() = Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + + private fun idleHandler() = Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() @After fun cleanup() { - clearAllMocks() renderHandlerThread.stop() + clearAllMocks() } @Test fun onSurfaceCreatedTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } - mockValidSurface() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } - verify { + initRenderThread() + provideValidSurface() + verifyOnce { mapboxRenderer.createRenderer() eglCore.makeNothingCurrent() mapboxRenderer.onSurfaceChanged(1, 1) @@ -85,32 +110,59 @@ class MapboxRenderThreadTest { } @Test - fun onSurfaceCreatedNotNativeSupportedTest() { - val latch = CountDownLatch(1) + fun onInvalidEglSurfaceNotCreateRenderer() { + initRenderThread() + every { eglCore.createWindowSurface(any()) } returns eglCore.eglNoSurface + provideValidSurface() + verifyNo { + mapboxRenderer.createRenderer() + mapboxRenderer.onSurfaceChanged(1, 1) + } + } + + @Test + fun onInvalidSurfaceNotInitNativeRenderer() { + initRenderThread() val surface = mockk() - every { surface.isValid } returns true -// every { eglCore.eglStatusSuccess } returns false - every { eglCore.createWindowSurface(any()) } returns mockk(relaxed = true) + every { surface.isValid } returns false mapboxRenderThread.onSurfaceCreated(surface, 1, 1) - latch.await(waitTime, TimeUnit.MILLISECONDS) - verify(exactly = 0) { + idleHandler() + verifyNo { mapboxRenderer.createRenderer() mapboxRenderer.onSurfaceChanged(1, 1) } } @Test - fun onSurfaceSizeChangedIndeedTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() - mapboxRenderThread.onSurfaceSizeChanged(2, 2) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() + fun onEglCorePrepareFailNotInitNativeRenderer() { + initRenderThread() + every { eglCore.prepareEgl() } returns false + provideValidSurface() + verifyNo { + mapboxRenderer.createRenderer() + mapboxRenderer.onSurfaceChanged(1, 1) } - verify { + } + + @Test + fun onMakeCurrentErrorNotInitNativeRenderer() { + initRenderThread() + every { eglCore.makeCurrent(any()) } returns false + provideValidSurface() + verifyNo { + mapboxRenderer.createRenderer() + mapboxRenderer.onSurfaceChanged(1, 1) + } + } + + @Test + fun onSurfaceSizeChangedTest() { + initRenderThread() + provideValidSurface() + pauseHandler() + mapboxRenderThread.onSurfaceSizeChanged(2, 2) + idleHandler() + verifyOnce { mapboxRenderer.createRenderer() mapboxRenderer.onSurfaceChanged(1, 1) mapboxRenderer.onSurfaceChanged(2, 2) @@ -119,16 +171,12 @@ class MapboxRenderThreadTest { @Test fun onSurfaceSizeChangedSameSizeTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.onSurfaceSizeChanged(1, 1) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } - verify { + idleHandler() + verifyOnce { mapboxRenderer.createRenderer() mapboxRenderer.onSurfaceChanged(1, 1) } @@ -136,63 +184,59 @@ class MapboxRenderThreadTest { @Test fun onSurfaceWithActivityDestroyedAfterSurfaceTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.destroyRenderer() } answers { latch.countDown() } - mockValidSurface() - mapboxRenderThread.onSurfaceDestroyed() - mapboxRenderThread.destroy() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() + initRenderThread() + provideValidSurface() + waitZeroCounter { + countDownEvery { mapboxRenderer.destroyRenderer() } + mapboxRenderThread.onSurfaceDestroyed() + mapboxRenderThread.destroy() } - verify(exactly = 1) { eglCore.release() } - verify { mapboxRenderer.destroyRenderer() } - assert(!renderHandlerThread.started) + verifyOnce { eglCore.release() } + verifyOnce { mapboxRenderer.destroyRenderer() } + assertFalse(renderHandlerThread.started) } @Test fun onSurfaceWithActivityDestroyedBeforeSurfaceTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.destroyRenderer() } answers { latch.countDown() } - mockValidSurface() - mapboxRenderThread.destroy() - mapboxRenderThread.onSurfaceDestroyed() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() + initRenderThread() + provideValidSurface() + waitZeroCounter { + countDownEvery { mapboxRenderer.destroyRenderer() } + mapboxRenderThread.destroy() + mapboxRenderThread.onSurfaceDestroyed() } - verify(exactly = 1) { eglCore.release() } - verify(exactly = 1) { mapboxRenderer.destroyRenderer() } - assert(!renderHandlerThread.started) + verifyOnce { eglCore.release() } + verifyOnce { mapboxRenderer.destroyRenderer() } + assertFalse(renderHandlerThread.started) } @Test fun onDrawFrameSeparateRequestRender() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // one swap buffer for surface creation, two for not squashed render requests verify(exactly = 3) { eglCore.swapBuffers(any()) } // make EGL context current only once when creating surface - verify(exactly = 1) { + verifyOnce { eglCore.makeNothingCurrent() } } @Test fun onDrawFrameSquashedRequestRender() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // one swap buffer for surface creation, 2 render requests squash in one swap buffers call verify(exactly = 2) { eglCore.swapBuffers(any()) @@ -201,22 +245,22 @@ class MapboxRenderThreadTest { @Test fun setMaximumFpsTest() { + initRenderThread() mapboxRenderThread.setMaximumFps(30) assert(mapboxRenderThread.renderTimeNs == 33333333L) } @Test fun pauseTest() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.pause() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // one swap buffer for surface creation, one request render after pause is omitted verify(exactly = 2) { eglCore.swapBuffers(any()) @@ -225,22 +269,21 @@ class MapboxRenderThreadTest { @Test fun resumeTestWithRequestRenderAtPause() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.pause() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.resume() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // render requests after pause do not swap buffer, we do it on resume if needed once verify(exactly = 4) { eglCore.swapBuffers(any()) @@ -249,18 +292,17 @@ class MapboxRenderThreadTest { @Test fun resumeTestWithoutRequestRenderAtPause() { - val latch = CountDownLatch(1) - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.pause() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.resume() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - latch.await(waitTime, TimeUnit.MILLISECONDS) + idleHandler() // we always do extra render call on resume verify(exactly = 4) { eglCore.swapBuffers(any()) @@ -269,16 +311,17 @@ class MapboxRenderThreadTest { @Test fun destroyTest() { + initRenderThread() mapboxRenderThread.destroy() - assert(!renderHandlerThread.started) + assertFalse(renderHandlerThread.started) } @Test fun queueRenderEventTest() { - val latch = CountDownLatch(1) - mockValidSurface() - val runnable = mockCountdownRunnable(latch) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + initRenderThread() + provideValidSurface() + val runnable = mockk(relaxUnitFun = true) + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -287,12 +330,9 @@ class MapboxRenderThreadTest { ) ) assertEquals(1, mapboxRenderThread.renderEventQueue.size) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } + idleHandler() assert(mapboxRenderThread.renderEventQueue.isEmpty()) - verify { runnable.run() } + verifyOnce { runnable.run() } // one swap buffer from surface creation, one for custom event verify(exactly = 2) { eglCore.swapBuffers(any()) @@ -301,10 +341,11 @@ class MapboxRenderThreadTest { @Test fun queueSdkNonRenderEventTestNoVsync() { - mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) mapboxRenderThread.awaitingNextVsync = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -315,20 +356,21 @@ class MapboxRenderThreadTest { // we do not add non-render event to the queue assert(mapboxRenderThread.nonRenderEventQueue.isEmpty()) // do not schedule any render requests explicitly - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - verify { runnable.run() } + idleHandler() + verifyOnce { runnable.run() } // one swap buffer from surface creation only - verify(exactly = 1) { + verifyOnce { eglCore.swapBuffers(any()) } } @Test fun queueSdkNonRenderEventTestWithVsync() { - mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) mapboxRenderThread.awaitingNextVsync = true - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -339,27 +381,28 @@ class MapboxRenderThreadTest { // we add to the queue assert(mapboxRenderThread.nonRenderEventQueue.size == 1) // do not schedule any render requests explicitly - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() // without explicit render event runnable should not be executed - verify(exactly = 0) { runnable.run() } + verifyNo { runnable.run() } mapboxRenderThread.awaitingNextVsync = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() // schedule render request mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() assert(mapboxRenderThread.nonRenderEventQueue.isEmpty()) // one swap buffer from surface creation + one for render request verify(exactly = 2) { eglCore.swapBuffers(any()) } - verify(exactly = 1) { runnable.run() } + verifyOnce { runnable.run() } } @Test fun queueUserNonRenderEventLoosingSurfaceTest() { - val surface = mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -369,23 +412,24 @@ class MapboxRenderThreadTest { ) // simulate render thread is not fully prepared, e.g. EGL context is lost mapboxRenderThread.eglContextCreated = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - verify(exactly = 0) { runnable.run() } - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + idleHandler() + verifyNo { runnable.run() } + pauseHandler() // simulate render thread is fully prepared again mapboxRenderThread.eglContextCreated = true mapboxRenderThread.processAndroidSurface(surface, 1, 1) // taking into account we try to reschedule event with some delay Shadows.shadowOf(renderHandlerThread.handler?.looper).idleFor(RETRY_DELAY_MS, TimeUnit.MILLISECONDS) // user's runnable is executed when thread is fully prepared again - verify(exactly = 1) { runnable.run() } + verifyOnce { runnable.run() } } @Test fun queueSdkNonRenderEventLoosingSurfaceTest() { - val surface = mockValidSurface() + initRenderThread() + provideValidSurface() val runnable = mockk(relaxUnitFun = true) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent( RenderEvent( runnable, @@ -395,34 +439,30 @@ class MapboxRenderThreadTest { ) // simulate render thread is not fully prepared, e.g. EGL context is lost mapboxRenderThread.eglContextCreated = false - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - verify(exactly = 0) { runnable.run() } - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + idleHandler() + verifyNo { runnable.run() } + pauseHandler() // simulate render thread is fully prepared again mapboxRenderThread.eglContextCreated = true mapboxRenderThread.processAndroidSurface(surface, 1, 1) // taking into account we try to reschedule event with some delay Shadows.shadowOf(renderHandlerThread.handler?.looper).idleFor(RETRY_DELAY_MS, TimeUnit.MILLISECONDS) // SDK's task is not executed with new surface - verify(exactly = 0) { runnable.run() } + verifyNo { runnable.run() } } @Test fun fpsListenerTest() { - val latch = CountDownLatch(2) + initRenderThread() val listener = mockk(relaxUnitFun = true) - every { listener.onFpsChanged(any()) } answers { latch.countDown() } mapboxRenderThread.fpsChangedListener = listener - mockValidSurface() - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + provideValidSurface() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } + idleHandler() verify(exactly = 2) { listener.onFpsChanged(any()) } @@ -430,37 +470,36 @@ class MapboxRenderThreadTest { @Test fun surfaceCreatedCalledBeforeActivityStartTest() { - val latch = CountDownLatch(1) - every { mapboxRenderer.createRenderer() } answers { latch.countDown() } + initRenderThread() mapboxRenderThread.paused = true - mockValidSurface() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() - } - verify(exactly = 1) { mapboxRenderer.createRenderer() } + provideValidSurface() + verifyOnce { mapboxRenderer.createRenderer() } // EGL should be fully prepared - verify(exactly = 0) { eglCore.releaseSurface(any()) } + verifyNo { eglCore.releaseSurface(any()) } } @Test fun snapshotsAreTakenAfterDrawAndBeforeSwapBuffers() { - val latch = CountDownLatch(3) + initRenderThread() + provideValidSurface() - mockValidSurface() + lateinit var runnable: Runnable + lateinit var runnable2: Runnable + lateinit var runnable3: Runnable + waitZeroCounter(startCounter = 3) { + runnable = mockCountdownRunnable(this) + runnable2 = mockCountdownRunnable(this) + runnable3 = mockCountdownRunnable(this) - val runnable = mockCountdownRunnable(latch) - val runnable2 = mockCountdownRunnable(latch) - val runnable3 = mockCountdownRunnable(latch) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() - mapboxRenderThread.queueRenderEvent(RenderEvent(runnable, true, EventType.SDK)) - mapboxRenderThread.queueRenderEvent(RenderEvent(runnable2, true, EventType.SDK)) - mapboxRenderThread.queueRenderEvent(RenderEvent(runnable3, true, EventType.SDK)) + mapboxRenderThread.queueRenderEvent(RenderEvent(runnable, true, EventType.SDK)) + mapboxRenderThread.queueRenderEvent(RenderEvent(runnable2, true, EventType.SDK)) + mapboxRenderThread.queueRenderEvent(RenderEvent(runnable3, true, EventType.SDK)) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - if (!latch.await(waitTime, TimeUnit.MILLISECONDS)) { - throw TimeoutException() + idleHandler() } + verifyOrder { mapboxRenderer.render() runnable.run() @@ -472,60 +511,170 @@ class MapboxRenderThreadTest { @Test fun onSurfaceDestroyedWithRenderCallAfterTestSurfaceView() { - mapboxRenderer = mockk(relaxUnitFun = true) - mapboxRenderThread = MapboxRenderThread( - mapboxRenderer, - renderHandlerThread, - eglCore - ).apply { - renderHandlerThread.start() - } - mockValidSurface() + initRenderThread(mockk(relaxUnitFun = true)) + provideValidSurface() mapboxRenderThread.onSurfaceDestroyed() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - // we do not destroy native renderer if it's stop and not destroy - verify(exactly = 0) { mapboxRenderer.destroyRenderer() } + idleHandler() + verifyNo { + // we do not destroy native renderer if it's stop and not destroy + mapboxRenderer.destroyRenderer() + eglCore.release() + } // we clear only EGLSurface but not all EGL - verify(exactly = 1) { eglCore.releaseSurface(any()) } - verify(exactly = 0) { eglCore.release() } + verifyOnce { eglCore.releaseSurface(any()) } } @Test fun onSurfaceDestroyedWithRenderCallAfterTestTextureView() { - mapboxRenderer = mockk(relaxUnitFun = true) - mapboxRenderThread = MapboxRenderThread( - mapboxRenderer, - renderHandlerThread, - eglCore - ).apply { - renderHandlerThread.start() - } - val latch = CountDownLatch(1) - every { mapboxRenderer.destroyRenderer() } answers { latch.countDown() } - mockValidSurface() + initRenderThread(mockk(relaxUnitFun = true)) + provideValidSurface() mapboxRenderThread.onSurfaceDestroyed() - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() - // we do destroy native renderer if it's stop (for texture renderer) - verify(exactly = 1) { mapboxRenderer.destroyRenderer() } - // we clear all EGL - verify(exactly = 1) { eglCore.releaseSurface(any()) } - verify(exactly = 1) { eglCore.release() } + idleHandler() + + verifyOnce { + // we do destroy native renderer if it's stop (for texture renderer) + mapboxRenderer.destroyRenderer() + // we clear all EGL + eglCore.releaseSurface(any()) + eglCore.release() + } } @Test fun renderWithMaxFpsSet() { - mockValidSurface() + initRenderThread() + provideValidSurface() mapboxRenderThread.setMaximumFps(15) - Shadows.shadowOf(renderHandlerThread.handler?.looper).pause() + pauseHandler() mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) - Shadows.shadowOf(renderHandlerThread.handler?.looper).idle() + idleHandler() // 1 swap when creating surface + 1 for request render call verify(exactly = 2) { eglCore.swapBuffers(any()) } } + + @Test + fun onDrawDoesNotRenderWidgets() { + initRenderThread() + provideValidSurface() + every { mapboxWidgetRenderer.needTextureUpdate } returns false + every { mapboxWidgetRenderer.getTexture() } returns 0 + pauseHandler() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + idleHandler() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + idleHandler() + verifyNo { + mapboxWidgetRenderer.updateTexture() + textureRenderer.render(any()) + } + } + + @Test + fun onDrawRendersWidgets() { + initRenderThread() + provideValidSurface() + val textureId = 1 + every { mapboxWidgetRenderer.needTextureUpdate } returns true + every { mapboxWidgetRenderer.hasWidgets() } returns true + every { mapboxWidgetRenderer.hasTexture() } returns true + every { mapboxWidgetRenderer.getTexture() } returns textureId + pauseHandler() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + idleHandler() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + idleHandler() + verify(exactly = 2) { + mapboxWidgetRenderer.updateTexture() + textureRenderer.render(textureId) + } + } + + @Test + fun onDrawRendersWidgetsBeforeMap() { + initRenderThread() + provideValidSurface() + val textureId = 1 + every { mapboxWidgetRenderer.needTextureUpdate } returns true + every { mapboxWidgetRenderer.hasWidgets() } returns true + every { mapboxWidgetRenderer.hasTexture() } returns true + every { mapboxWidgetRenderer.getTexture() } returns textureId + pauseHandler() + mapboxRenderThread.queueRenderEvent(MapboxRenderer.renderEventSdk) + idleHandler() + verifyOrder { + mapboxWidgetRenderer.updateTexture() + mapboxRenderer.render() + textureRenderer.render(textureId) + } + } + + @Test + fun onSurfaceCreatedWidgetsInitWidgetRender() { + initRenderThread() + val eglContext = mockk() + every { eglCore.eglContext } returns eglContext + every { mapboxWidgetRenderer.hasWidgets() } returns true + provideValidSurface() + verifyOnce { + mapboxWidgetRenderer.setSharedContext(eglContext) + } + } + + @Test + fun onSurfaceCreatedNoWidgetsNotInitWidgetRender() { + initRenderThread() + every { mapboxWidgetRenderer.hasWidgets() } returns false + provideValidSurface() + verifyNo { + mapboxWidgetRenderer.setSharedContext(any()) + } + } + + @Test + fun onEglCorePrepareFailNotInitWidgetRender() { + initRenderThread() + every { eglCore.prepareEgl() } returns false + provideValidSurface() + verifyNo { + mapboxWidgetRenderer.setSharedContext(any()) + } + } + + @Test + fun onInvalidSurfaceNotInitWidgetRender() { + initRenderThread() + val surface = mockk() + every { surface.isValid } returns false + mapboxRenderThread.onSurfaceCreated(surface, 1, 1) + idleHandler() + verifyNo { + mapboxWidgetRenderer.setSharedContext(any()) + } + } + + @Test + fun onInvalidEglSurfaceNotInitWidgetRender() { + initRenderThread() + every { eglCore.createWindowSurface(any()) } returns eglCore.eglNoSurface + provideValidSurface() + verifyNo { + mapboxWidgetRenderer.setSharedContext(any()) + } + } + + @Test + fun onMakeCurrentErrorNotInitWidgetRender() { + initRenderThread() + every { eglCore.makeCurrent(any()) } returns false + provideValidSurface() + verifyNo { + mapboxWidgetRenderer.setSharedContext(any()) + } + } } \ No newline at end of file