package expo.modules.kotlin.views import android.annotation.SuppressLint import android.content.Context import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.RowScope import androidx.compose.runtime.Composable import androidx.compose.runtime.RecomposeScope import androidx.compose.runtime.currentRecomposeScope import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.size import expo.modules.kotlin.AppContext import expo.modules.kotlin.viewevent.CoalescingKey import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.viewevent.ViewEvent import expo.modules.kotlin.viewevent.ViewEventDelegate data class ComposableScope( val rowScope: RowScope? = null, val columnScope: ColumnScope? = null, val boxScope: BoxScope? = null, val nestedScrollConnection: NestedScrollConnection? = null ) inline fun ComposableScope.withIf( condition: Boolean, block: ComposableScope.() -> ComposableScope ): ComposableScope { return if (condition) block() else this } fun ComposableScope.with(rowScope: RowScope?): ComposableScope { return this.copy(rowScope = rowScope) } fun ComposableScope.with(columnScope: ColumnScope?): ComposableScope { return this.copy(columnScope = columnScope) } fun ComposableScope.with(boxScope: BoxScope?): ComposableScope { return this.copy(boxScope = boxScope) } fun ComposableScope.with(nestedScrollConnection: NestedScrollConnection?): ComposableScope { return this.copy(nestedScrollConnection = nestedScrollConnection) } /** * 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 protected var recomposeScope: RecomposeScope? = null private val globalEvent = ViewEvent>>(GLOBAL_EVENT_NAME, this, null) /** * A global event dispatcher */ val globalEventDispatcher: (String, Map) -> Unit = { name, params -> globalEvent.invoke(Pair(name, params)) } @Composable abstract fun ComposableScope.Content() 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) } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) // Makes sure the child ComposeView is sticky with the current hosting view if (withHostingView) { for (i in 0 until childCount) { val child = getChildAt(i) if (child is ComposeView) { val offsetX = paddingLeft val offsetY = paddingRight child.layout(offsetX, offsetY, offsetX + width, offsetY + height) } } } } @Composable fun Children(composableScope: ComposableScope?) { recomposeScope = currentRecomposeScope for (index in 0.. ?: continue with(composableScope ?: ComposableScope()) { with(child) { Content() } } } } @Composable fun Children(composableScope: ComposableScope?, filter: (child: ExpoComposeView<*>) -> Boolean) { recomposeScope = currentRecomposeScope for (index in 0.. ?: continue if (!filter(child)) { continue } with(composableScope ?: ComposableScope()) { with(child) { Content() } } } } @Composable fun Child(composableScope: ComposableScope, index: Int) { recomposeScope = currentRecomposeScope val child = getChildAt(index) as? ExpoComposeView<*> ?: return with(composableScope) { with(child) { Content() } } } @Composable fun Child(index: Int) { Child(ComposableScope(), index) } init { if (withHostingView) { clipChildren = false clipToPadding = false 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 { with(ComposableScope()) { Content() } } 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 && this !is RNHostViewInterface) { ExpoComposeAndroidView(child, appContext) } else { child } super.addView(view, index, params) } override fun onViewAdded(child: View?) { super.onViewAdded(child) recomposeScope?.invalidate() } override fun onViewRemoved(child: View?) { super.onViewRemoved(child) recomposeScope?.invalidate() } } /** * A composable DSL scope that wraps an [ExpoComposeView] to provide syntax sugar. * * This scope allows defining view content using a functional, DSL-style API * without creating a dedicated subclass of [ExpoComposeView]. */ class FunctionalComposableScope( val view: ComposeFunctionHolder<*>, val composableScope: ComposableScope ) { val appContext = view.appContext val globalEventDispatcher = view.globalEventDispatcher @Composable fun Child(composableScope: ComposableScope, index: Int) { view.Child(composableScope, index) } @Composable fun Child(index: Int) { view.Child(index) } @Composable fun Children(composableScope: ComposableScope?) { view.Children(composableScope) } @Composable fun Children(composableScope: ComposableScope?, filter: (child: ExpoComposeView<*>) -> Boolean) { view.Children(composableScope, filter) } inline fun EventDispatcher(noinline coalescingKey: CoalescingKey? = null): ViewEventDelegate { return view.EventDispatcher(coalescingKey) } } @SuppressLint("ViewConstructor") class ComposeFunctionHolder( context: Context, appContext: AppContext, override val name: String, private val composableContent: @Composable FunctionalComposableScope.(props: Props) -> Unit, override val props: Props ) : ExpoComposeView(context, appContext), ViewFunctionHolder { val propsMutableState = mutableStateOf(props) @Composable override fun ComposableScope.Content() { val props by propsMutableState with(FunctionalComposableScope(this@ComposeFunctionHolder, this@Content)) { composableContent(props) } } }