diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/platform/MediaScope.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/platform/MediaScope.ios.kt new file mode 100644 index 0000000000000..7d11bed53d071 --- /dev/null +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/platform/MediaScope.ios.kt @@ -0,0 +1,278 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalMediaQueryApi::class) + +package androidx.compose.ui.platform + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.ExperimentalMediaQueryApi +import androidx.compose.ui.UiMediaScope +import androidx.compose.ui.uikit.InterfaceOrientation +import androidx.compose.ui.unit.Dp +import org.jetbrains.skiko.SystemTheme +import androidx.compose.ui.uikit.utils.CMPKeyValueObserver +import androidx.compose.ui.uikit.utils.CMPUIWindowSceneUtils +import androidx.compose.ui.window.KeyboardVisibilityListener +import androidx.compose.ui.window.KeyboardVisibilitySubscriber +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CPointed +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ObjCAction +import kotlinx.cinterop.useContents +import platform.AVFoundation.AVCaptureDevice +import platform.AVFoundation.AVCaptureDeviceWasConnectedNotification +import platform.AVFoundation.AVCaptureDeviceWasDisconnectedNotification +import platform.AVFoundation.AVMediaTypeAudio +import platform.AVFoundation.AVMediaTypeVideo +import platform.CoreGraphics.CGRect +import platform.Foundation.NSKeyValueObservingOptionNew +import platform.Foundation.NSNotification +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSSelectorFromString +import platform.Foundation.addObserver +import platform.Foundation.removeObserver +import platform.darwin.NSObject +import platform.UIKit.UIUserInterfaceStyle +import platform.UIKit.UIViewAnimationOptions +import platform.UIKit.UIWindow +import platform.UIKit.UIWindowScene + +internal class MediaScope(val windowInfo: WindowInfo) : UiMediaScope, KeyboardVisibilitySubscriber { + + private var window: UIWindow? = null + /* + * Initial value is arbitrarily chosen to avoid propagating invalid value logic + * It's never the case in the real usage scenario to reflect that in type system + */ + internal val interfaceOrientationState: MutableState = mutableStateOf( + InterfaceOrientation.Portrait + ) + private val systemThemeState: MutableState = mutableStateOf(SystemTheme.UNKNOWN) + + private val isImeShowing = mutableStateOf(KeyboardVisibilityListener.keyboardFrame.useContents { size.height > 0 }) + private val pointerPrecisionState: MutableState = mutableStateOf( + UiMediaScope.PointerPrecision.Coarse + ) + + private val hasMicrophoneState: MutableState = mutableStateOf(detectHasMicrophone()) + private val hasCameraState: MutableState = mutableStateOf(detectHasCamera()) + + private fun detectHasMicrophone(): Boolean = + AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio) != null + + private fun detectHasCamera(): Boolean = + AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeVideo) != null + + + private fun updateCaptureDeviceAvailabilityState() { + hasMicrophoneState.value = detectHasMicrophone() + hasCameraState.value = detectHasCamera() + } + + private val captureDeviceAvailabilityObserver = CaptureDeviceAvailabilityObserver(::updateCaptureDeviceAvailabilityState) + + fun updateInterfaceOrientationState() { + currentInterfaceOrientation?.let { + interfaceOrientationState.value = it + } + } + fun updateUserInterfaceStyle(style: UIUserInterfaceStyle) { + systemThemeState.value = style.asComposeSystemTheme() + } + fun updatePointerPrecision(precision: UiMediaScope.PointerPrecision) { + pointerPrecisionState.value = precision + } + + private val currentInterfaceOrientation: InterfaceOrientation? + get() { + return InterfaceOrientation.getByRawValue( + CMPUIWindowSceneUtils.interfaceOrientationForWindowScene(window?.windowScene) + ) + } + + private val interfaceOrientationObserver = SceneGeometryObserver { + updateInterfaceOrientationState() + } + fun onDidMoveToWindow(window: UIWindow?) { + this.window = window + interfaceOrientationObserver.windowScene = window?.windowScene + window ?: return + + updateInterfaceOrientationState() + + } + + val systemTheme: SystemTheme + get() = systemThemeState.value + + fun startObserving() { + interfaceOrientationObserver.isObservingEnabled = true + captureDeviceAvailabilityObserver.isObservingEnabled = true + KeyboardVisibilityListener.addSubscriber(this) + } + + fun stopObserving() { + interfaceOrientationObserver.isObservingEnabled = false + captureDeviceAvailabilityObserver.isObservingEnabled = false + KeyboardVisibilityListener.removeSubscriber(this) + } + + override val windowPosture: UiMediaScope.Posture + get() = UiMediaScope.Posture.Flat //iOS doesn't have foldables yet! + override val windowWidth: Dp + get() = windowInfo.containerDpSize.width + override val windowHeight: Dp + get() = windowInfo.containerDpSize.height + override val pointerPrecision: UiMediaScope.PointerPrecision + get() = pointerPrecisionState.value + override val keyboardKind: UiMediaScope.KeyboardKind + get() = when { + isImeShowing.value -> UiMediaScope.KeyboardKind.Virtual + else -> UiMediaScope.KeyboardKind.None + } + override val hasMicrophone: Boolean + get() = hasMicrophoneState.value + override val hasCamera: Boolean + get() = hasCameraState.value + override val viewingDistance: UiMediaScope.ViewingDistance + get() = UiMediaScope.ViewingDistance.Near + + override fun keyboardWillShow( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + isImeShowing.value = targetFrame.useContents { size.height > 0 } + } + + override fun keyboardWillHide( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + isImeShowing.value = false + } + + override fun keyboardWillChangeFrame( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + isImeShowing.value = targetFrame.useContents { size.height > 0 } + } +} + + +private class SceneGeometryObserver( + val onGeometryChanged: () -> Unit +) : CMPKeyValueObserver() { + private val observingKey = "effectiveGeometry" + + var windowScene: UIWindowScene? = null + set(value) { + if (field == value) return + removeObserverIfNeeded() + field = value + addObserverIfNeeded() + } + + var isObservingEnabled = false + set(value) { + if (field == value) return + field = value + if (value) { + addObserverIfNeeded() + } else { + removeObserverIfNeeded() + } + } + + private var isObservingAdded = false + + private fun addObserverIfNeeded() { + if (isObservingEnabled && !isObservingAdded) { + isObservingAdded = true + windowScene?.addObserver(this, observingKey, NSKeyValueObservingOptionNew, null) + } + } + + private fun removeObserverIfNeeded() { + windowScene?.removeObserver(this, observingKey) + isObservingAdded = false + } + + override fun observeValueForKeyPath( + keyPath: String?, + ofObject: Any?, + change: Map?, + context: CPointer? + ) { + onGeometryChanged() + } +} + +private class CaptureDeviceAvailabilityObserver( + val onDeviceAvailabilityChanged: () -> Unit, + private val notificationCenter: NSNotificationCenter = NSNotificationCenter.defaultCenter +) : NSObject() { + + var isObservingEnabled = false + set(value) { + if (field == value) return + field = value + if (value) { + addObservers() + } else { + removeObservers() + } + } + + private fun addObservers() { + notificationCenter.addObserver( + observer = this, + selector = NSSelectorFromString(::deviceAvailabilityDidChange.name + ":"), + name = AVCaptureDeviceWasConnectedNotification, + `object` = null + ) + notificationCenter.addObserver( + observer = this, + selector = NSSelectorFromString(::deviceAvailabilityDidChange.name + ":"), + name = AVCaptureDeviceWasDisconnectedNotification, + `object` = null + ) + } + + private fun removeObservers() { + notificationCenter.removeObserver(this) + } + + @OptIn(BetaInteropApi::class) + @ObjCAction + fun deviceAvailabilityDidChange(arg: NSNotification) { + onDeviceAvailabilityChanged() + } +} + +private fun UIUserInterfaceStyle.asComposeSystemTheme(): SystemTheme { + return when (this) { + UIUserInterfaceStyle.UIUserInterfaceStyleLight -> SystemTheme.LIGHT + UIUserInterfaceStyle.UIUserInterfaceStyleDark -> SystemTheme.DARK + else -> SystemTheme.UNKNOWN + } +} \ No newline at end of file diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt index f29f5fe8fb8cf..da4d53251bfef 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeContainer.ios.kt @@ -19,27 +19,23 @@ package androidx.compose.ui.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.LocalSystemTheme import androidx.compose.ui.graphics.asComposeCanvas import androidx.compose.ui.navigationevent.IosBackNavigationEventInput import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner import androidx.compose.ui.platform.FrameChoreographer +import androidx.compose.ui.platform.MediaScope import androidx.compose.ui.platform.MotionDurationScaleImpl import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.WindowContext import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.uikit.ComposeContainerConfiguration import androidx.compose.ui.uikit.PreferredSizeReportingStrategy -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalUIViewController import androidx.compose.ui.uikit.PlistSanityCheck import androidx.compose.ui.uikit.density import androidx.compose.ui.uikit.embedSubview -import androidx.compose.ui.uikit.utils.CMPKeyValueObserver -import androidx.compose.ui.uikit.utils.CMPUIWindowSceneUtils import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection @@ -59,8 +55,6 @@ import androidx.savedstate.SavedState import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlinx.cinterop.COpaquePointer -import kotlinx.cinterop.CPointed -import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.IntVar import kotlinx.cinterop.alloc @@ -71,11 +65,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.job import kotlinx.coroutines.launch -import org.jetbrains.skia.Canvas -import org.jetbrains.skiko.SystemTheme -import platform.Foundation.NSKeyValueObservingOptionNew -import platform.Foundation.addObserver -import platform.Foundation.removeObserver import platform.UIKit.UIAccessibilityIsReduceMotionEnabled import platform.UIKit.UIApplication import platform.UIKit.UIResponder @@ -152,9 +141,7 @@ internal class ComposeContainer( get() = mediatorComponentsOwner ?: error("ArchitectureComponentsOwner is not initialized yet.") - private val interfaceOrientationObserver = SceneGeometryObserver { - updateInterfaceOrientationState() - } + private val mediaScope = MediaScope(windowContext.windowInfo) private val navigationEventInput = IosBackNavigationEventInput( density = view.density, initialLayoutDirection = layoutDirection, @@ -168,14 +155,6 @@ internal class ComposeContainer( ) val hasInteropViews: Boolean get() = mediator?.hasInteropViews ?: false - /* - * Initial value is arbitrarily chosen to avoid propagating invalid value logic - * It's never the case in the real usage scenario to reflect that in type system - */ - private val interfaceOrientationState: MutableState = mutableStateOf( - InterfaceOrientation.Portrait - ) - private val systemThemeState: MutableState = mutableStateOf(SystemTheme.UNKNOWN) private val focusedViewsList = FocusedViewsList() @@ -209,13 +188,6 @@ internal class ComposeContainer( return mediator?.hasInvalidations == true || layersHolder?.layersViewController?.hasInvalidations == true } - private val currentInterfaceOrientation: InterfaceOrientation? - get() { - return InterfaceOrientation.getByRawValue( - CMPUIWindowSceneUtils.interfaceOrientationForWindowScene(windowScene) - ) - } - private fun onLayoutSubviews() { windowContext.updateWindowContainerSize() @@ -230,11 +202,10 @@ internal class ComposeContainer( private fun onDidMoveToWindow(window: UIWindow?) { navigationEventInput.onDidMoveToWindow(window, view) - interfaceOrientationObserver.windowScene = window?.windowScene + mediaScope.onDidMoveToWindow(window) window ?: return - updateInterfaceOrientationState() layersHolder?.layersViewController?.containerWindow = view.window windowContext.window = window @@ -242,11 +213,7 @@ internal class ComposeContainer( lifecycleDelegate.windowScene = window.windowScene } - fun updateInterfaceOrientationState() { - currentInterfaceOrientation?.let { - interfaceOrientationState.value = it - } - } + fun updateInterfaceOrientationState() = mediaScope.updateInterfaceOrientationState() fun sceneDidAppear() { mediator?.sceneDidAppear() @@ -265,9 +232,7 @@ internal class ComposeContainer( navigationEventInput.onDidMoveToWindow(null, view) } - fun updateUserInterfaceStyle(style: UIUserInterfaceStyle) { - systemThemeState.value = style.asComposeSystemTheme() - } + fun updateUserInterfaceStyle(style: UIUserInterfaceStyle) = mediaScope.updateUserInterfaceStyle(style) fun initializeComposeScene() { sceneJob = Job() @@ -333,7 +298,7 @@ internal class ComposeContainer( ) }, navigationEventInput = navigationEventInput, - interfaceOrientationState = interfaceOrientationState, + mediaScope = mediaScope, schedulePendingInteropViewUpdates = view::setNeedsDisplay, ).also { mediator -> mediator.rootForTestListener = rootForTestListener @@ -367,7 +332,7 @@ internal class ComposeContainer( } } - interfaceOrientationObserver.isObservingEnabled = true + mediaScope.startObserving() architectureComponentsOwner.navigationEventDispatcher.addInput(navigationEventInput) lifecycleDelegate.windowScene = windowScene @@ -394,7 +359,7 @@ internal class ComposeContainer( layersHolder = null - interfaceOrientationObserver.isObservingEnabled = false + mediaScope.stopObserving() } private fun createComposeSceneContext( @@ -438,7 +403,7 @@ internal class ComposeContainer( consumePointerInputOutside = consumePointerInputOutside, parentCoroutineContext = containerCoroutineContext, ownerProvider = architectureComponentsOwner, - interfaceOrientationState = interfaceOrientationState, + mediaScope = mediaScope, invalidateLayout = { layersHolder.getLayersViewController().invalidateLayout() }, invalidateDraw = { layersHolder.getLayersViewController().invalidateDraw() }, ) @@ -491,7 +456,7 @@ internal class ComposeContainer( private fun ProvideContainerCompositionLocals(content: @Composable () -> Unit) = CompositionLocalProvider( LocalUIViewController provides containingViewController, - LocalSystemTheme provides systemThemeState.value, + LocalSystemTheme provides mediaScope.systemTheme, content = content ) @@ -509,14 +474,6 @@ internal class ComposeContainer( get() = view.window?.windowScene } -private fun UIUserInterfaceStyle.asComposeSystemTheme(): SystemTheme { - return when (this) { - UIUserInterfaceStyle.UIUserInterfaceStyleLight -> SystemTheme.LIGHT - UIUserInterfaceStyle.UIUserInterfaceStyleDark -> SystemTheme.DARK - else -> SystemTheme.UNKNOWN - } -} - private fun getApplicationLayoutDirection() = when (UIApplication.sharedApplication().userInterfaceLayoutDirection) { UIUserInterfaceLayoutDirectionRightToLeft -> LayoutDirection.Rtl @@ -545,54 +502,6 @@ private class ComposeLayersHolder( } } -private class SceneGeometryObserver( - val onGeometryChanged: () -> Unit -) : CMPKeyValueObserver() { - private val observingKey = "effectiveGeometry" - - var windowScene: UIWindowScene? = null - set(value) { - if (field == value) return - removeObserverIfNeeded() - field = value - addObserverIfNeeded() - } - - var isObservingEnabled = false - set(value) { - if (field == value) return - field = value - if (value) { - addObserverIfNeeded() - } else { - removeObserverIfNeeded() - } - } - - private var isObservingAdded = false - - private fun addObserverIfNeeded() { - if (isObservingEnabled && !isObservingAdded) { - isObservingAdded = true - windowScene?.addObserver(this, observingKey, NSKeyValueObservingOptionNew, null) - } - } - - private fun removeObserverIfNeeded() { - windowScene?.removeObserver(this, observingKey) - isObservingAdded = false - } - - override fun observeValueForKeyPath( - keyPath: String?, - ofObject: Any?, - change: Map?, - context: CPointer? - ) { - onGeometryChanged() - } -} - private fun UIUserInterfaceLayoutDirection.asLayoutDirection(): LayoutDirection = when (this) { UIUserInterfaceLayoutDirectionLeftToRight -> LayoutDirection.Ltr UIUserInterfaceLayoutDirectionRightToLeft -> LayoutDirection.Rtl diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt index 7b727bb295c52..141cd6d20f5d4 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.ios.kt @@ -20,12 +20,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.annotation.VisibleForTesting +import androidx.compose.ui.ExperimentalMediaQueryApi import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.UiMediaScope import androidx.compose.ui.draganddrop.IosDragAndDropManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect @@ -66,17 +67,16 @@ import androidx.compose.ui.platform.PlatformScreenReader import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.WindowContext import androidx.compose.ui.platform.ApplicationIdleTimer +import androidx.compose.ui.platform.MediaScope import androidx.compose.ui.platform.TaskDispatchers import androidx.compose.ui.platform.TextInputService import androidx.compose.ui.platform.WindowInsetsManager import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalNativeTextInputContext import androidx.compose.ui.uikit.LocalUIView import androidx.compose.ui.uikit.OnFocusBehavior -import androidx.compose.ui.uikit.density import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset @@ -201,7 +201,7 @@ internal class ComposeSceneMediator( private val architectureComponentsOwner: PlatformArchitectureComponentsOwner, val coroutineContext: CoroutineContext, private val navigationEventInput: IosBackNavigationEventInput, - interfaceOrientationState: State, + private val mediaScope: MediaScope, composeSceneFactory: (platformContext: PlatformContext) -> ComposeScene, private val schedulePendingInteropViewUpdates: () -> Unit = {}, ) { @@ -396,7 +396,7 @@ internal class ComposeSceneMediator( { _overlayView }, { windowContext.window?.rootViewController?.view }, ), - interfaceOrientation = interfaceOrientationState + interfaceOrientation = mediaScope.interfaceOrientationState ) /** @@ -507,12 +507,14 @@ internal class ComposeSceneMediator( } } + @OptIn(ExperimentalMediaQueryApi::class) private fun onScrollEvent( position: DpOffset, delta: DpOffset, event: UIEvent?, eventKind: TouchesEventKind ) { + mediaScope.updatePointerPrecision(UiMediaScope.PointerPrecision.Fine) when (eventKind) { TouchesEventKind.BEGAN -> activitiesHandler.onActivitiesStarted() TouchesEventKind.MOVED -> {} @@ -536,11 +538,13 @@ internal class ComposeSceneMediator( ) } + @OptIn(ExperimentalMediaQueryApi::class) private fun onHoverEvent( position: DpOffset, event: UIEvent?, eventKind: TouchesEventKind ) { + mediaScope.updatePointerPrecision(UiMediaScope.PointerPrecision.Fine) val eventType = when (eventKind) { TouchesEventKind.BEGAN -> PointerEventType.Enter TouchesEventKind.MOVED -> PointerEventType.Move @@ -579,6 +583,7 @@ internal class ComposeSceneMediator( * @param event the [UIEvent] associated with the touches * @param eventKind the [TouchesEventKind] of the touches */ + @OptIn(ExperimentalMediaQueryApi::class) private fun onTouchesEvent( touches: Set<*>, event: UIEvent?, @@ -590,6 +595,7 @@ internal class ComposeSceneMediator( TouchesEventKind.MOVED -> {} } + var anyIsStylus = false val pointers = touches.mapIndexed { index, touch -> touch as UITouch val position = touch.offsetInView(_backgroundView, screenDensity.density) @@ -599,6 +605,9 @@ internal class ComposeSceneMediator( UITouchTypePencil -> PointerType.Stylus else -> PointerType.Touch } + if (pointerType == PointerType.Stylus) { + anyIsStylus = true + } val id = touch.hashCode().toLong().takeIf { pointerType != PointerType.Mouse } ?: index.toLong() @@ -616,6 +625,12 @@ internal class ComposeSceneMediator( ) } + if (anyIsStylus) { + mediaScope.updatePointerPrecision(UiMediaScope.PointerPrecision.Fine) + } else { + mediaScope.updatePointerPrecision(UiMediaScope.PointerPrecision.Coarse) + } + // UIKit sends buttonMask that was before the release action. It should be empty if no // pressed pointers left. val pointerButtonsMask = event.buttonMaskOrZero.takeIf { @@ -907,6 +922,8 @@ internal class ComposeSceneMediator( private inner class IosPlatformContext : PlatformContext { override val windowInfo: WindowInfo get() = windowContext.windowInfo + @OptIn(ExperimentalMediaQueryApi::class) + override val mediaScope: UiMediaScope get() = this@ComposeSceneMediator.mediaScope override val taskDispatchers: TaskDispatchers = object : TaskDispatchers { override val Default = Dispatchers.Default override val IO = Dispatchers.IO diff --git a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/IosComposeSceneLayer.ios.kt b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/IosComposeSceneLayer.ios.kt index 19917ff5b35c1..0dc1fe8e2f187 100644 --- a/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/IosComposeSceneLayer.ios.kt +++ b/compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/scene/IosComposeSceneLayer.ios.kt @@ -19,7 +19,6 @@ package androidx.compose.ui.scene import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.State import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Paint @@ -28,10 +27,10 @@ import androidx.compose.ui.input.pointer.PointerButton import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.navigationevent.IosBackNavigationEventInput import androidx.compose.ui.platform.FrameChoreographer +import androidx.compose.ui.platform.MediaScope import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.uikit.ComposeContainerConfiguration -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalUIViewController import androidx.compose.ui.uikit.density import androidx.compose.ui.uikit.embedSubview @@ -61,7 +60,7 @@ internal class IosComposeSceneLayer( consumePointerInputOutside: Boolean = focusedViewsList != null, parentCoroutineContext: CoroutineContext, private val ownerProvider: PlatformArchitectureComponentsOwner, - private val interfaceOrientationState: State, + private val mediaScope: MediaScope, private var invalidateLayout: () -> Unit, private var invalidateDraw: () -> Unit, ) : ComposeSceneLayer { @@ -115,7 +114,7 @@ internal class IosComposeSceneLayer( coroutineContext = layerCoroutineContext, composeSceneFactory = ::createComposeScene, navigationEventInput = navigationEventInput, - interfaceOrientationState = interfaceOrientationState, + mediaScope = mediaScope, schedulePendingInteropViewUpdates = layersViewController::invalidateDraw, ).also { interactionView.embedSubview(it.backgroundView) diff --git a/compose/ui/ui/src/iosTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorUnitTest.kt b/compose/ui/ui/src/iosTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorUnitTest.kt index f43e54a892bba..58590d14c03c8 100644 --- a/compose/ui/ui/src/iosTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorUnitTest.kt +++ b/compose/ui/ui/src/iosTest/kotlin/androidx/compose/ui/integrations/ComposeSceneMediatorUnitTest.kt @@ -16,17 +16,16 @@ package androidx.compose.ui.integrations -import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.navigationevent.IosBackNavigationEventInput import androidx.compose.ui.platform.DefaultArchitectureComponentsOwner import androidx.compose.ui.platform.FrameChoreographer +import androidx.compose.ui.platform.MediaScope import androidx.compose.ui.platform.WindowContext import androidx.compose.ui.platform.registerSkikoComposeImplementation import androidx.compose.ui.scene.ComposeSceneContext import androidx.compose.ui.scene.ComposeSceneMediator import androidx.compose.ui.scene.PlatformLayersComposeScene import androidx.compose.ui.uikit.EndEdgePanGestureBehavior -import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.OnFocusBehavior import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset @@ -93,32 +92,36 @@ class ComposeSceneMediatorUnitTest { private fun makeMediator( coroutineContext: CoroutineContext, frameChoreographer: FrameChoreographer = FrameChoreographer.choreographerForScene(UIWindowScene()), - ): ComposeSceneMediator = ComposeSceneMediator( - frameChoreographer = frameChoreographer, - onFocusBehavior = OnFocusBehavior.DoNothing, - isClearFocusOnMouseDownEnabled = false, - focusedViewsList = null, - windowContext = WindowContext(), - architectureComponentsOwner = DefaultArchitectureComponentsOwner(), - coroutineContext = coroutineContext, - navigationEventInput = IosBackNavigationEventInput( - density = Density(1f), - initialLayoutDirection = LayoutDirection.Ltr, - getTopLeftOffsetInWindow = { IntOffset.Zero }, - endEdgePanGestureBehavior = EndEdgePanGestureBehavior.Disabled, - ), - interfaceOrientationState = mutableStateOf(InterfaceOrientation.Portrait), - composeSceneFactory = { platformContext -> - registerSkikoComposeImplementation() - PlatformLayersComposeScene( - frameRecomposer = frameChoreographer.frameRecomposer, + ): ComposeSceneMediator { + val windowContext = WindowContext() + val mediaScope = MediaScope(windowContext.windowInfo) + return ComposeSceneMediator( + frameChoreographer = frameChoreographer, + onFocusBehavior = OnFocusBehavior.DoNothing, + isClearFocusOnMouseDownEnabled = false, + focusedViewsList = null, + windowContext = windowContext, + architectureComponentsOwner = DefaultArchitectureComponentsOwner(), + coroutineContext = coroutineContext, + navigationEventInput = IosBackNavigationEventInput( density = Density(1f), - composeSceneContext = object : ComposeSceneContext { - override val platformContext = platformContext - }, - invalidateLayout = {}, - invalidateDraw = {}, - ) - }, - ) + initialLayoutDirection = LayoutDirection.Ltr, + getTopLeftOffsetInWindow = { IntOffset.Zero }, + endEdgePanGestureBehavior = EndEdgePanGestureBehavior.Disabled, + ), + mediaScope = mediaScope, + composeSceneFactory = { platformContext -> + registerSkikoComposeImplementation() + PlatformLayersComposeScene( + frameRecomposer = frameChoreographer.frameRecomposer, + density = Density(1f), + composeSceneContext = object : ComposeSceneContext { + override val platformContext = platformContext + }, + invalidateLayout = {}, + invalidateDraw = {}, + ) + }, + ) + } }