package expo.modules.kotlin.views import android.content.Context import android.view.View import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.core.view.size import expo.modules.kotlin.AppContext /** * Applies a test tag to a modifier if a testID is provided. */ @OptIn(ExperimentalComposeUiApi::class) private fun Modifier.applyTestTag(testID: String?): Modifier = if (!testID.isNullOrEmpty()) { this.semantics { testTagsAsResourceId = true }.testTag(testID) } else { this } /** * A base class that should be used by compose views. */ abstract class ExpoComposeView( context: Context, appContext: AppContext, private val withHostingView: Boolean = false ) : ExpoView(context, appContext) { open val props: T? = null var testID: String? = null @Composable abstract fun Content(modifier: Modifier) override val shouldUseAndroidLayout = withHostingView override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // In case of issues there's an alternative solution in previous commits at https://github.com/expo/expo/pull/33759 if (shouldUseAndroidLayout && !isAttachedToWindow) { setMeasuredDimension(widthMeasureSpec, heightMeasureSpec) return } super.onMeasure(widthMeasureSpec, heightMeasureSpec) } @Composable protected fun Children() { if (withHostingView) { Content(modifier = Modifier.applyTestTag(testID)) return } for (index in 0.. ?: continue child.Content(modifier = Modifier.applyTestTag(child.testID)) } } init { if (withHostingView) { addComposeView() } else { this.visibility = GONE this.setWillNotDraw(true) } } private fun addComposeView() { val composeView = ComposeView(context).also { it.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) it.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) it.setContent { Children() } it.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { it.disposeComposition() } override fun onViewDetachedFromWindow(v: View) = Unit }) } addView(composeView) } override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) { val view = if (child !is ExpoComposeView<*> && child !is ComposeView) { ExpoComposeAndroidView(child, appContext) } else { child } super.addView(view, index, params) } }