package com.rtnmapsgl import android.util.Log import android.view.Choreographer import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import androidx.fragment.app.FragmentActivity import com.facebook.react.bridge.Callback import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactPropGroup; import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableArray class MapsGLManager( private val reactContext: ReactApplicationContext ) : ViewGroupManager() { private var propWidth: Int? = null private var propHeight: Int? = null private var aerisId: String? = null private var aerisSecretKey: String? = null private var mapboxAccessToken: String? = null private var mapLayerState: ReadableMap? = null private var pins: ReadableArray? = null private var selectedPin: ReadableMap? = null private var styleUrl: String? = null private var userLocation: DoubleArray? = null override fun getExportedCustomBubblingEventTypeConstants(): Map { return mapOf( "onMapsglViewDidMove" to mapOf( "phasedRegistrationNames" to mapOf( "bubbled" to "onMapsglViewDidMove" ) ), "onSelectWaterbody" to mapOf( "phasedRegistrationNames" to mapOf( "bubbled" to "onSelectWaterbody" ) ) ) } // Variable to store the MapsGLFragment instance private var mapsGLFragment: MapsGLFragment? = null // Track whether the fragment is created private var isFragmentCreated = false // Track whether we've processed pending props private var isPendingPropsProcessed = false // Use the static NAME property from the shared implementation override fun getName() = REACT_CLASS override fun createViewInstance(context: ThemedReactContext) = FrameLayout(reactContext) /** * Map the "create" command to an integer */ override fun getCommandsMap() = mapOf("create" to COMMAND_CREATE) /** * Handle "create" command (called from JS) and call createFragment method */ override fun receiveCommand( root: FrameLayout, commandId: String, args: ReadableArray? ) { super.receiveCommand(root, commandId, args) val reactNativeViewId = requireNotNull(args).getInt(0) Log.d("MapsGLManager", "CONNOR - receiveCommand called with $commandId and $reactNativeViewId") when (commandId.toInt()) { COMMAND_CREATE -> createFragment(root, reactNativeViewId) } } /** * Replace your React Native view with a custom fragment */ fun createFragment(root: FrameLayout, reactNativeViewId: Int) { Log.d("MapsGLManager", "CONNOR - creating fragment at id: $reactNativeViewId") val parentView = root.findViewById(reactNativeViewId) setupLayout(parentView) val myFragment = MapsGLFragment(reactContext, reactNativeViewId) mapsGLFragment = myFragment // Store the MapsGLFragment instance val activity = reactContext.currentActivity as FragmentActivity activity.supportFragmentManager .beginTransaction() .replace(reactNativeViewId, myFragment, reactNativeViewId.toString()) .commit() Log.d("MapsGLManager", "CONNOR - create fragment complete") // The fragment is now created isFragmentCreated = true processPendingProps() } fun setupLayout(view: View) { Log.d("MapsGLManager", "CONNOR - setting up layout") // Worth noting that this fires 60 times a second Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback { override fun doFrame(frameTimeNanos: Long) { manuallyLayoutChildren(view) view.viewTreeObserver.dispatchOnGlobalLayout() Choreographer.getInstance().postFrameCallback(this) } }) } /** * Layout all children properly */ private fun manuallyLayoutChildren(view: View) { // propWidth and propHeight coming from react-native props val width = requireNotNull(propWidth) val height = requireNotNull(propHeight) view.measure( View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)) view.layout(0, 0, width, height) } // Helper method to process the React Native props after the fragment is created private fun processPendingProps() { if (isFragmentCreated) { // Process the React Native props here mapsGLFragment?.apply { aerisId?.let { setAerisId(it) } aerisSecretKey?.let { setAerisSecretKey(it) } mapboxAccessToken?.let { setMapboxAccessToken(it) } mapLayerState?.let { setMapLayerState(it) } pins?.let { updatePins(it) } selectedPin?.let { setSelectedPin(it) } styleUrl?.let { setStyleUrl(it) } setUserLocation(userLocation) } // Clear all the stored prop values after using them aerisId = null aerisSecretKey = null mapboxAccessToken = null mapLayerState = null pins = null selectedPin = null styleUrl = null userLocation = null // Avoid bulk-setting props once we know the fragment has been created isPendingPropsProcessed = true } } /** * Handle React Native Props */ @ReactPropGroup(names = ["width", "height"], customType = "Style") fun setStyle(view: FrameLayout, index: Int, value: Int) { if (index == 0) propWidth = value if (index == 1) propHeight = value } @ReactProp(name = "aerisId") fun setAerisId(view: FrameLayout, aerisId: String) { this.aerisId = aerisId if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.setAerisId(aerisId) } } @ReactProp(name = "aerisSecretKey") fun setAerisSecretKey(view: FrameLayout, aerisSecretKey: String) { this.aerisSecretKey = aerisSecretKey if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.setAerisSecretKey(aerisSecretKey) } } @ReactProp(name = "mapboxAccessToken") fun setMapboxAccessToken(view: FrameLayout, mapboxAccessToken: String) { this.mapboxAccessToken = mapboxAccessToken if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.setMapboxAccessToken(mapboxAccessToken) } } @ReactProp(name = "mapLayerState") fun setMapLayerState(view: FrameLayout, mapLayerState: ReadableMap) { this.mapLayerState = mapLayerState if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.setMapLayerState(mapLayerState) } } @ReactProp(name = "selectedPin") fun setSelectedPin(view: FrameLayout, selectedPin: ReadableMap?) { this.selectedPin = selectedPin selectedPin?.let { if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.setSelectedPin(it) } } } @ReactProp(name = "pins") fun updatePins(view: FrameLayout, pins: ReadableArray) { this.pins = pins if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.updatePins(pins) } } @ReactProp(name = "styleUrl") fun setStyleUrl(view: FrameLayout, styleUrl: String) { this.styleUrl = styleUrl if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.setStyleUrl(styleUrl) } } @ReactProp(name = "userLocation") fun setUserLocation(view: FrameLayout, userLocation: ReadableArray?) { val location = userLocation?.let { doubleArrayOf(it.getDouble(0), it.getDouble(1)) } this.userLocation = location // Store the prop value if (!isPendingPropsProcessed) { processPendingProps() } else { mapsGLFragment?.setUserLocation(location) } } companion object { private const val REACT_CLASS = "RTNMapsGL" private const val COMMAND_CREATE = 1 } }