From 1bff33ed6df2b97dec337c848fbf2eec818311cd Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Sun, 17 Mar 2024 18:07:33 +0100 Subject: [PATCH 01/12] Fix IME windows insets --- .../foundation/layout/WindowInsets.uikit.kt | 23 +- .../ui/platform/PlatformInsets.uikit.kt | 6 +- .../platform/UIKitTextInputService.uikit.kt | 4 +- .../ui/scene/ComposeSceneMediator.uikit.kt | 76 +---- .../ui/scene/UIViewComposeSceneLayer.uikit.kt | 8 +- .../ui/uikit/KeyboardOverlapHeight.uikit.kt | 8 + .../ui/window/ComposeContainer.uikit.kt | 23 +- .../ComposeContainerKeyboardManager.uikit.kt | 259 +++++++++++++++++ .../IntermediateTextInputUIView.uikit.kt | 10 +- .../KeyboardVisibilityListener.uikit.kt | 266 +++++++----------- 10 files changed, 433 insertions(+), 250 deletions(-) create mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt diff --git a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt index 68c0402c979f8..a153cd435ed3a 100644 --- a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt +++ b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt @@ -18,11 +18,16 @@ package androidx.compose.foundation.layout import androidx.compose.runtime.Composable import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.State import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.platform.LocalLayoutMargins import androidx.compose.ui.platform.LocalSafeArea import androidx.compose.ui.platform.PlatformInsets -import androidx.compose.ui.uikit.* +import androidx.compose.ui.uikit.InterfaceOrientation +import androidx.compose.ui.uikit.LocalInterfaceOrientation +import androidx.compose.ui.uikit.LocalKeyboardBottomInset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp private val ZeroInsets = WindowInsets(0, 0, 0, 0) @@ -35,6 +40,20 @@ private fun PlatformInsets.toWindowInsets() = WindowInsets( bottom = bottom, ) +private class ImeWindowInsets( + val keyboardBottomInset: State +): WindowInsets { + override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0 + + override fun getTop(density: Density): Int = 0 + + override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0 + + override fun getBottom(density: Density): Int = with(density) { + keyboardBottomInset.value.dp.roundToPx() + } +} + /** * This insets represents iOS SafeAreas. */ @@ -78,7 +97,7 @@ actual val WindowInsets.Companion.displayCutout: WindowInsets actual val WindowInsets.Companion.ime: WindowInsets @Composable @OptIn(InternalComposeApi::class) - get() = WindowInsets(bottom = LocalKeyboardOverlapHeight.current.dp) + get() = ImeWindowInsets(LocalKeyboardBottomInset.current) /** * These insets represent the space where system gestures have priority over application gestures. diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt index 803e58eb0e4c5..852abaaa27a4e 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt @@ -19,7 +19,9 @@ package androidx.compose.ui.platform import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.uikit.LocalKeyboardBottomInset import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight import androidx.compose.ui.unit.dp @@ -41,7 +43,7 @@ private object SafeAreaInsetsConfig : InsetsConfig { @Composable get() = LocalSafeArea.current override val ime: PlatformInsets - @Composable get() = PlatformInsets(bottom = LocalKeyboardOverlapHeight.current.dp) + @Composable get() = PlatformInsets(bottom = LocalKeyboardBottomInset.current.value.dp) @Composable override fun excludeInsets( @@ -52,10 +54,12 @@ private object SafeAreaInsetsConfig : InsetsConfig { val safeArea = LocalSafeArea.current val layoutMargins = LocalLayoutMargins.current val keyboardOverlapHeight = LocalKeyboardOverlapHeight.current + val keyboardInsets = LocalKeyboardBottomInset.current CompositionLocalProvider( LocalSafeArea provides if (safeInsets) PlatformInsets() else safeArea, LocalLayoutMargins provides if (safeInsets) layoutMargins.exclude(safeArea) else layoutMargins, LocalKeyboardOverlapHeight provides if (ime) 0f else keyboardOverlapHeight, + LocalKeyboardBottomInset provides if (ime) mutableStateOf(0f) else keyboardInsets, content = content ) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt index a1ea6562d6cc7..dde425b4ff4e0 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt @@ -112,9 +112,9 @@ internal class UIKitTextInputService( textUIView?.removeFromSuperview() textUIView = IntermediateTextInputUIView( - keyboardEventHandler = keyboardEventHandler, viewConfiguration = viewConfiguration ).also { + it.keyboardEventHandler = keyboardEventHandler rootView.addSubview(it) it.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activateConstraints( @@ -136,8 +136,10 @@ internal class UIKitTextInputService( textUIView?.inputTraits = EmptyInputTraits textUIView?.input = null + textUIView?.keyboardEventHandler = null textUIView?.let { view -> mainScope.launch { + view.resignFirstResponder() view.removeFromSuperview() } } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 18add9d96f590..cc4cfea74c77c 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight +import androidx.compose.ui.uikit.LocalKeyboardBottomInset import androidx.compose.ui.uikit.systemDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset @@ -66,10 +67,10 @@ import androidx.compose.ui.unit.round import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.toDpRect import androidx.compose.ui.unit.toOffset +import androidx.compose.ui.window.ComposeContainerKeyboardManager import androidx.compose.ui.window.FocusStack import androidx.compose.ui.window.InteractionUIView import androidx.compose.ui.window.KeyboardEventHandler -import androidx.compose.ui.window.KeyboardVisibilityListenerImpl import androidx.compose.ui.window.RenderingUIView import androidx.compose.ui.window.UITouchesEventPhase import kotlin.coroutines.CoroutineContext @@ -77,7 +78,6 @@ import kotlin.math.floor import kotlin.math.roundToInt import kotlin.math.roundToLong import kotlinx.cinterop.CValue -import kotlinx.cinterop.ObjCAction import kotlinx.cinterop.readValue import kotlinx.cinterop.useContents import org.jetbrains.skia.Canvas @@ -91,21 +91,15 @@ import platform.CoreGraphics.CGRect import platform.CoreGraphics.CGRectMake import platform.CoreGraphics.CGRectZero import platform.CoreGraphics.CGSize -import platform.Foundation.NSNotification -import platform.Foundation.NSNotificationCenter -import platform.Foundation.NSSelectorFromString import platform.Foundation.NSTimeInterval import platform.QuartzCore.CATransaction import platform.UIKit.NSLayoutConstraint import platform.UIKit.UIEvent -import platform.UIKit.UIKeyboardWillHideNotification -import platform.UIKit.UIKeyboardWillShowNotification import platform.UIKit.UITouch import platform.UIKit.UITouchPhase import platform.UIKit.UIView import platform.UIKit.UIViewControllerTransitionCoordinatorProtocol import platform.UIKit.UIWindow -import platform.darwin.NSObject /** * Layout of sceneView on the screen @@ -191,22 +185,6 @@ private class RenderingUIViewDelegateImpl( } } -private class NativeKeyboardVisibilityListener( - private val keyboardVisibilityListener: KeyboardVisibilityListenerImpl -) : NSObject() { - @Suppress("unused") - @ObjCAction - fun keyboardWillShow(arg: NSNotification) { - keyboardVisibilityListener.keyboardWillShow(arg) - } - - @Suppress("unused") - @ObjCAction - fun keyboardWillHide(arg: NSNotification) { - keyboardVisibilityListener.keyboardWillHide(arg) - } -} - private class ComposeSceneMediatorRootUIView : UIView(CGRectZero.readValue()) { override fun hitTest(point: CValue, withEvent: UIEvent?): UIView? { // forwards touches forward to the children, is never a target for a touch @@ -233,7 +211,6 @@ internal class ComposeSceneMediator( coroutineContext: CoroutineContext ) -> ComposeScene ) { - private val focusable: Boolean get() = focusStack != null private val keyboardOverlapHeightState: MutableState = mutableStateOf(0f) private var _layout: SceneLayout = SceneLayout.Undefined private var constraints: List = emptyList() @@ -266,7 +243,7 @@ internal class ComposeSceneMediator( set(value) { scene.compositionLocalContext = value } - private val focusManager get() = scene.focusManager + val focusManager get() = scene.focusManager private val renderingView by lazy { renderingUIViewFactory(renderDelegate) @@ -342,14 +319,13 @@ internal class ComposeSceneMediator( ) } - private val keyboardVisibilityListener by lazy { - KeyboardVisibilityListenerImpl( + private val keyboardManager by lazy { + ComposeContainerKeyboardManager( configuration = configuration, keyboardOverlapHeightState = keyboardOverlapHeightState, - viewProvider = { container }, - densityProvider = { container.systemDensity }, - composeSceneMediatorProvider = { this }, - focusManager = focusManager, + viewProvider = { rootView }, + densityProvider = { rootView.systemDensity }, + composeSceneMediatorProvider = { this } ) } @@ -508,13 +484,16 @@ internal class ComposeSceneMediator( LocalUIKitInteropContext provides interopContext, LocalUIKitInteropContainer provides interopViewContainer, LocalKeyboardOverlapHeight provides keyboardOverlapHeightState.value, + LocalKeyboardBottomInset provides keyboardOverlapHeightState, LocalSafeArea provides safeAreaState.value, LocalLayoutMargins provides layoutMarginsState.value, content = content ) fun dispose() { + uiKitTextInputService.stopInput() focusStack?.popUntilNext(renderingView) + keyboardManager.stop() renderingView.dispose() interactionView.dispose() rootView.removeFromSuperview() @@ -646,37 +625,12 @@ internal class ComposeSceneMediator( ) } - private val nativeKeyboardVisibilityListener = NativeKeyboardVisibilityListener( - keyboardVisibilityListener - ) - - fun viewDidAppear(animated: Boolean) { - NSNotificationCenter.defaultCenter.addObserver( - observer = nativeKeyboardVisibilityListener, - selector = NSSelectorFromString(nativeKeyboardVisibilityListener::keyboardWillShow.name + ":"), - name = UIKeyboardWillShowNotification, - `object` = null - ) - NSNotificationCenter.defaultCenter.addObserver( - observer = nativeKeyboardVisibilityListener, - selector = NSSelectorFromString(nativeKeyboardVisibilityListener::keyboardWillHide.name + ":"), - name = UIKeyboardWillHideNotification, - `object` = null - ) + fun sceneDidAppear() { + keyboardManager.start() } - // viewDidUnload() is deprecated and not called. - fun viewWillDisappear(animated: Boolean) { - NSNotificationCenter.defaultCenter.removeObserver( - observer = nativeKeyboardVisibilityListener, - name = UIKeyboardWillShowNotification, - `object` = null - ) - NSNotificationCenter.defaultCenter.removeObserver( - observer = nativeKeyboardVisibilityListener, - name = UIKeyboardWillHideNotification, - `object` = null - ) + fun sceneWillDisappear() { + keyboardManager.stop() } fun getViewHeight(): Double = renderingView.frame.useContents { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt index dc651e848ff20..d7429752ec1df 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/UIViewComposeSceneLayer.uikit.kt @@ -198,12 +198,12 @@ internal class UIViewComposeSceneLayer( return positionInWindow } - fun viewDidAppear(animated: Boolean) { - mediator.viewDidAppear(animated) + fun sceneDidAppear() { + mediator.sceneDidAppear() } - fun viewWillDisappear(animated: Boolean) { - mediator.viewWillDisappear(animated) + fun sceneWillDisappear() { + mediator.sceneWillDisappear() } fun viewSafeAreaInsetsDidChange() { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt index 4a3b0fb10901e..aaebba618b962 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt @@ -17,11 +17,19 @@ package androidx.compose.ui.uikit import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.staticCompositionLocalOf /** * Composition local for height that is overlapped with keyboard over Compose view. */ +@Deprecated("Use LocalWindowKeyboardInsets instead") @InternalComposeApi val LocalKeyboardOverlapHeight = staticCompositionLocalOf { 0f } + +@InternalComposeApi +val LocalKeyboardBottomInset = staticCompositionLocalOf> { + mutableStateOf(0f) +} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt index 4bb608d12a6b0..98df898fcfb35 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt @@ -96,6 +96,7 @@ internal class ComposeContainer( private var mediator: ComposeSceneMediator? = null private val layers: MutableList = mutableListOf() private val layoutDirection get() = getLayoutDirection() + private var isViewAppeared: Boolean = false @OptIn(ExperimentalComposeApi::class) private val windowContainer: UIView @@ -239,9 +240,10 @@ internal class ComposeContainer( override fun viewDidAppear(animated: Boolean) { super.viewDidAppear(animated) - mediator?.viewDidAppear(animated) + isViewAppeared = true + mediator?.sceneDidAppear() layers.fastForEach { - it.viewDidAppear(animated) + it.sceneDidAppear() } updateWindowContainer() configuration.delegate.viewDidAppear(animated) @@ -249,9 +251,10 @@ internal class ComposeContainer( override fun viewWillDisappear(animated: Boolean) { super.viewWillDisappear(animated) - mediator?.viewWillDisappear(animated) + isViewAppeared = false + mediator?.sceneWillDisappear() layers.fastForEach { - it.viewWillDisappear(animated) + it.sceneWillDisappear() } configuration.delegate.viewWillDisappear(animated) } @@ -327,7 +330,7 @@ internal class ComposeContainer( windowContext = windowContext, coroutineContext = coroutineDispatcher, renderingUIViewFactory = ::createSkikoUIView, - composeSceneFactory = ::createComposeScene, + composeSceneFactory = ::createComposeScene ) mediator.setContent { ProvideContainerCompositionLocals(this, content) @@ -342,14 +345,19 @@ internal class ComposeContainer( layers.fastForEach { it.close() } - } fun attachLayer(layer: UIViewComposeSceneLayer) { layers.add(layer) + if (isViewAppeared) { + layer.sceneDidAppear() + } } fun detachLayer(layer: UIViewComposeSceneLayer) { + if (isViewAppeared) { + layer.sceneWillDisappear() + } layers.remove(layer) } @@ -369,10 +377,9 @@ internal class ComposeContainer( configuration = configuration, focusStack = if (focusable) focusStack else null, windowContext = windowContext, - compositionContext = compositionContext, + compositionContext = compositionContext ) } - } private fun UIViewController.checkIfInsideSwiftUI(): Boolean { diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt new file mode 100644 index 0000000000000..366417e4b5639 --- /dev/null +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt @@ -0,0 +1,259 @@ +/* + * Copyright 2024 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. + */ + +package androidx.compose.ui.window + +import androidx.compose.runtime.MutableState +import androidx.compose.ui.scene.ComposeSceneMediator +import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration +import androidx.compose.ui.uikit.OnFocusBehavior +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpRect +import androidx.compose.ui.unit.toDpRect +import kotlin.math.max +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ObjCAction +import kotlinx.cinterop.useContents +import platform.CoreGraphics.CGFloat +import platform.CoreGraphics.CGPointMake +import platform.CoreGraphics.CGRect +import platform.CoreGraphics.CGRectGetMinY +import platform.CoreGraphics.CGRectIsEmpty +import platform.CoreGraphics.CGRectMake +import platform.Foundation.NSDefaultRunLoopMode +import platform.Foundation.NSRunLoop +import platform.QuartzCore.CADisplayLink +import platform.QuartzCore.CATransaction +import platform.QuartzCore.kCATransactionDisableActions +import platform.UIKit.UIView +import platform.UIKit.UIViewAnimationOptionBeginFromCurrentState +import platform.UIKit.UIViewAnimationOptionCurveEaseInOut +import platform.UIKit.UIViewAnimationOptions +import platform.darwin.NSObject +import platform.darwin.sel_registerName + + +internal class ComposeContainerKeyboardManager( + private val configuration: ComposeUIViewControllerConfiguration, + private val keyboardOverlapHeightState: MutableState, + private val viewProvider: () -> UIView, + private val densityProvider: () -> Density, + private val composeSceneMediatorProvider: () -> ComposeSceneMediator? +) : KeyboardVisibilityObserver { + + val view get() = viewProvider() + + fun start() { + KeyboardVisibilityListener.addObserver(this) + adjustViewBounds( + KeyboardVisibilityListener.keyboardFrame, + 0.25, + UIViewAnimationOptionCurveEaseInOut + ) + } + + fun stop() { + KeyboardVisibilityListener.removeObserver(this) + } + + //invisible view to track system keyboard animation + private val keyboardAnimationView: UIView by lazy { + UIView(CGRectMake(0.0, 0.0, 0.0, 0.0)).apply { + hidden = true + } + } + private var keyboardAnimationListener: CADisplayLink? = null + + override fun keyboardWillShow( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + } + + override fun keyboardWillChangeFrame( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) = adjustViewBounds(targetFrame, duration, animationOptions) + + override fun keyboardWillHide( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + } + + private fun adjustViewBounds( + keyboardFrame: CValue, duration: Double, animationOptions: UIViewAnimationOptions + ) { + val screen = view.window?.screen ?: return + val keyboardScreenHeight = if (CGRectIsEmpty(keyboardFrame)) { + 0.0 + } else { + max(0.0, screen.bounds.useContents { size.height } - CGRectGetMinY(keyboardFrame)) + } + + val imeFrameLeft: Double = if ( + configuration.onFocusBehavior == OnFocusBehavior.FocusableAboveKeyboard + ) { + if (keyboardScreenHeight <= 0) { + // Keyboard is not visible + updateViewBounds(offsetY = 0.0) + 0.0 + } else { + val mediator = composeSceneMediatorProvider() + val focusedRect = + mediator?.focusManager?.getFocusRect()?.toDpRect(densityProvider()) + + if (focusedRect != null) { + val offsetY = calcFocusedLiftingY(mediator, focusedRect, keyboardScreenHeight) + updateViewBounds(offsetY = offsetY) + keyboardScreenHeight - offsetY + } else { + updateViewBounds(offsetY = 0.0) + keyboardScreenHeight + } + } + } else { + max(keyboardScreenHeight, 0.0) + } + + val bottomIndent = run { + val screenHeight = screen.bounds.useContents { size.height } + val composeViewBottomY = screen.coordinateSpace.convertPoint( + point = CGPointMake(0.0, view.frame.useContents { size.height }), + fromCoordinateSpace = view.coordinateSpace + ).useContents { y } + screenHeight - composeViewBottomY + } + + animateKeyboard(imeFrameLeft, bottomIndent, duration, animationOptions) + } + + private fun animateKeyboard( + keyboardHeight: CGFloat, + bottomIndent: CGFloat, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + //return actual keyboard height during animation + fun getCurrentKeyboardHeight(): CGFloat { + val layer = keyboardAnimationView.layer.presentationLayer() ?: return 0.0 + return layer.frame.useContents { origin.y } + } + + //attach to root view if needed + if (keyboardAnimationView.superview == null) { + view.addSubview(keyboardAnimationView) + } + + //cancel previous animation + keyboardAnimationView.layer.removeAllAnimations() + keyboardAnimationListener?.invalidate() + + //synchronize actual keyboard height with keyboardAnimationView without animation + val current = getCurrentKeyboardHeight() + CATransaction.begin() + CATransaction.setValue(true, kCATransactionDisableActions) + keyboardAnimationView.setFrame(CGRectMake(0.0, current, 0.0, 0.0)) + CATransaction.commit() + + //animation listener + keyboardAnimationListener = CADisplayLink.displayLinkWithTarget( + target = object : NSObject() { + @OptIn(BetaInteropApi::class) + @Suppress("unused") + @ObjCAction + fun animationDidUpdate() { + val currentHeight = getCurrentKeyboardHeight() + keyboardOverlapHeightState.value = + max(0f, (currentHeight - bottomIndent).toFloat()) + } + }, + selector = sel_registerName("animationDidUpdate") + ).apply { + addToRunLoop(NSRunLoop.mainRunLoop(), NSDefaultRunLoopMode) + } + + fun completeAnimation() { + keyboardAnimationListener?.invalidate() + keyboardAnimationListener = null + keyboardAnimationView.removeFromSuperview() + keyboardOverlapHeightState.value = max(0f, (keyboardHeight - bottomIndent).toFloat()) + } + + if (duration > 0 && current != keyboardHeight) { + UIView.animateWithDuration( + duration = duration, + delay = 0.0, + options = animationOptions or UIViewAnimationOptionBeginFromCurrentState, + animations = { + //set final destination for animation + keyboardAnimationView.setFrame(CGRectMake(0.0, keyboardHeight, 0.0, 0.0)) + }, + completion = { isFinished -> + if (isFinished) { + completeAnimation() + } + } + ) + } else { + keyboardAnimationView.setFrame(CGRectMake(0.0, keyboardHeight, 0.0, 0.0)) + completeAnimation() + } + } + + private fun calcFocusedLiftingY( + composeSceneMediator: ComposeSceneMediator, + focusedRect: DpRect, + keyboardHeight: Double + ): Double { + val viewHeight = composeSceneMediator.getViewHeight() + val hiddenPartOfFocusedElement: Double = + keyboardHeight - viewHeight + focusedRect.bottom.value + return if (hiddenPartOfFocusedElement > 0) { + // If focused element is partially hidden by the keyboard, we need to lift it upper + val focusedTopY = focusedRect.top.value + val isFocusedElementRemainsVisible = hiddenPartOfFocusedElement < focusedTopY + if (isFocusedElementRemainsVisible) { + // We need to lift focused element to be fully visible + hiddenPartOfFocusedElement + } else { + // In this case focused element height is bigger than remain part of the screen after showing the keyboard. + // Top edge of focused element should be visible. Same logic on Android. + maxOf(focusedTopY, 0f).toDouble() + } + } else { + // Focused element is not hidden by the keyboard. + 0.0 + } + } + + private fun updateViewBounds(offsetX: Double = 0.0, offsetY: Double = 0.0) { + view.layer.setBounds( + view.frame.useContents { + CGRectMake( + x = offsetX, + y = offsetY, + width = size.width, + height = size.height + ) + } + ) + } +} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt index 6be295290652d..030d27359de94 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/IntermediateTextInputUIView.uikit.kt @@ -79,7 +79,6 @@ import platform.darwin.NSInteger */ @Suppress("CONFLICTING_OVERLOADS") internal class IntermediateTextInputUIView( - private val keyboardEventHandler: KeyboardEventHandler, private val viewConfiguration: ViewConfiguration ) : UIView(frame = CGRectZero.readValue()), UIKeyInputProtocol, UITextInputProtocol { @@ -92,6 +91,7 @@ internal class IntermediateTextInputUIView( cancelContextMenuUpdate() } } + var keyboardEventHandler: KeyboardEventHandler? = null private var _currentTextMenuActions: TextActions? = null var inputTraits: SkikoUITextInputTraits = EmptyInputTraits @@ -99,12 +99,16 @@ internal class IntermediateTextInputUIView( override fun canBecomeFirstResponder() = true override fun pressesBegan(presses: Set<*>, withEvent: UIPressesEvent?) { - handleUIViewPressesBegan(keyboardEventHandler, presses, withEvent) + keyboardEventHandler?.let { + handleUIViewPressesBegan(it, presses, withEvent) + } super.pressesBegan(presses, withEvent) } override fun pressesEnded(presses: Set<*>, withEvent: UIPressesEvent?) { - handleUIViewPressesEnded(keyboardEventHandler, presses, withEvent) + keyboardEventHandler?.let { + handleUIViewPressesEnded(it, presses, withEvent) + } super.pressesEnded(presses, withEvent) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt index 87b44e79b4c39..bd53420ddecb2 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt @@ -16,199 +16,125 @@ package androidx.compose.ui.window -import androidx.compose.runtime.MutableState -import androidx.compose.ui.scene.ComposeSceneFocusManager -import androidx.compose.ui.scene.ComposeSceneMediator -import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration -import androidx.compose.ui.uikit.OnFocusBehavior -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.DpRect -import androidx.compose.ui.unit.toDpRect +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CValue import kotlinx.cinterop.ObjCAction -import kotlinx.cinterop.useContents -import platform.CoreGraphics.CGFloat -import platform.CoreGraphics.CGPointMake -import platform.CoreGraphics.CGRectMake -import platform.Foundation.NSDefaultRunLoopMode +import kotlinx.cinterop.readValue +import platform.CoreGraphics.CGRect +import platform.CoreGraphics.CGRectZero import platform.Foundation.NSNotification -import platform.Foundation.NSRunLoop +import platform.Foundation.NSNotificationCenter +import platform.Foundation.NSNumber +import platform.Foundation.NSSelectorFromString import platform.Foundation.NSValue -import platform.QuartzCore.CADisplayLink -import platform.QuartzCore.CATransaction -import platform.QuartzCore.kCATransactionDisableActions import platform.UIKit.CGRectValue +import platform.UIKit.UIKeyboardAnimationCurveUserInfoKey import platform.UIKit.UIKeyboardAnimationDurationUserInfoKey import platform.UIKit.UIKeyboardFrameEndUserInfoKey -import platform.UIKit.UIScreen -import platform.UIKit.UIView +import platform.UIKit.UIKeyboardWillChangeFrameNotification +import platform.UIKit.UIKeyboardWillHideNotification +import platform.UIKit.UIKeyboardWillShowNotification +import platform.UIKit.UIViewAnimationOptions import platform.darwin.NSObject -import platform.darwin.sel_registerName -internal interface KeyboardVisibilityListener { - fun keyboardWillShow(arg: NSNotification) - fun keyboardWillHide(arg: NSNotification) +internal interface KeyboardVisibilityObserver { + fun keyboardWillShow( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) + + fun keyboardWillHide( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) + + fun keyboardWillChangeFrame( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) } -internal class KeyboardVisibilityListenerImpl( - private val configuration: ComposeUIViewControllerConfiguration, - private val keyboardOverlapHeightState: MutableState, - private val viewProvider: () -> UIView, - private val densityProvider: () -> Density, - private val composeSceneMediatorProvider: () -> ComposeSceneMediator, - private val focusManager: ComposeSceneFocusManager, -) : KeyboardVisibilityListener { - - val view get() = viewProvider() - - //invisible view to track system keyboard animation - private val keyboardAnimationView: UIView by lazy { - UIView(CGRectMake(0.0, 0.0, 0.0, 0.0)).apply { - hidden = true - } - } - private var keyboardAnimationListener: CADisplayLink? = null - - override fun keyboardWillShow(arg: NSNotification) { - animateKeyboard(arg, true) - - val mediator = composeSceneMediatorProvider() - val userInfo = arg.userInfo ?: return - val keyboardInfo = userInfo[UIKeyboardFrameEndUserInfoKey] as NSValue - val keyboardHeight = keyboardInfo.CGRectValue().useContents { size.height } - if (configuration.onFocusBehavior == OnFocusBehavior.FocusableAboveKeyboard) { - val focusedRect = focusManager.getFocusRect()?.toDpRect(densityProvider()) - - if (focusedRect != null) { - updateViewBounds( - offsetY = calcFocusedLiftingY(mediator, focusedRect, keyboardHeight) - ) - } - } - } +internal object KeyboardVisibilityListener { + private val listener = NativeKeyboardVisibilityListener() - override fun keyboardWillHide(arg: NSNotification) { - animateKeyboard(arg, false) + fun addObserver(observer: KeyboardVisibilityObserver) = listener.observers.add(observer) - if (configuration.onFocusBehavior == OnFocusBehavior.FocusableAboveKeyboard) { - updateViewBounds(offsetY = 0.0) - } + fun removeObserver(observer: KeyboardVisibilityObserver) = listener.observers.remove(observer) + + val keyboardFrame: CValue get() = listener.keyboardFrame +} + +private class NativeKeyboardVisibilityListener : NSObject() { + val observers = mutableSetOf() + + init { + NSNotificationCenter.defaultCenter.addObserver( + observer = this, + selector = NSSelectorFromString(::keyboardWillShow.name + ":"), + name = UIKeyboardWillShowNotification, + `object` = null + ) + NSNotificationCenter.defaultCenter.addObserver( + observer = this, + selector = NSSelectorFromString(::keyboardWillHide.name + ":"), + name = UIKeyboardWillHideNotification, + `object` = null + ) + NSNotificationCenter.defaultCenter.addObserver( + observer = this, + selector = NSSelectorFromString(::keyboardWillChangeFrame.name + ":"), + name = UIKeyboardWillChangeFrameNotification, + `object` = null + ) } - private fun animateKeyboard(arg: NSNotification, isShow: Boolean) { - val userInfo = arg.userInfo!! + var keyboardFrame = CGRectZero.readValue() + private set - //return actual keyboard height during animation - fun getCurrentKeyboardHeight(): CGFloat { - val layer = keyboardAnimationView.layer.presentationLayer() ?: return 0.0 - return layer.frame.useContents { origin.y } + @OptIn(BetaInteropApi::class) + @ObjCAction + fun keyboardWillShow(arg: NSNotification) { + observers.forEach { + it.keyboardWillShow(arg.endFrame, arg.duration, arg.animationOptions) } + keyboardFrame = arg.endFrame + } - //attach to root view if needed - if (keyboardAnimationView.superview == null) { - view.addSubview(keyboardAnimationView) + @OptIn(BetaInteropApi::class) + @ObjCAction + fun keyboardWillHide(arg: NSNotification) { + observers.forEach { + it.keyboardWillHide(CGRectZero.readValue(), arg.duration, arg.animationOptions) } + keyboardFrame = CGRectZero.readValue() + } - //cancel previous animation - keyboardAnimationView.layer.removeAllAnimations() - keyboardAnimationListener?.invalidate() - - //synchronize actual keyboard height with keyboardAnimationView without animation - val current = getCurrentKeyboardHeight() - CATransaction.begin() - CATransaction.setValue(true, kCATransactionDisableActions) - keyboardAnimationView.setFrame(CGRectMake(0.0, current, 0.0, 0.0)) - CATransaction.commit() - - //animation listener - keyboardAnimationListener = CADisplayLink.displayLinkWithTarget( - target = object : NSObject() { - val bottomIndent: CGFloat - - init { - val screenHeight = UIScreen.mainScreen.bounds.useContents { size.height } - val composeViewBottomY = UIScreen.mainScreen.coordinateSpace.convertPoint( - point = CGPointMake(0.0, view.frame.useContents { size.height }), - fromCoordinateSpace = view.coordinateSpace - ).useContents { y } - bottomIndent = screenHeight - composeViewBottomY - } - - @Suppress("unused") - @ObjCAction - fun animationDidUpdate() { - val currentHeight = getCurrentKeyboardHeight() - if (bottomIndent < currentHeight) { - keyboardOverlapHeightState.value = (currentHeight - bottomIndent).toFloat() - } - } - }, - selector = sel_registerName("animationDidUpdate") - ).apply { - addToRunLoop(NSRunLoop.mainRunLoop(), NSDefaultRunLoopMode) + @OptIn(BetaInteropApi::class) + @ObjCAction + fun keyboardWillChangeFrame(arg: NSNotification) { + observers.forEach { + it.keyboardWillChangeFrame(arg.endFrame, arg.duration, arg.animationOptions) } + keyboardFrame = arg.endFrame + } - //start system animation with duration - val duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Double ?: 0.0 - val toValue: CGFloat = if (isShow) { - val keyboardInfo = userInfo[UIKeyboardFrameEndUserInfoKey] as NSValue - keyboardInfo.CGRectValue().useContents { size.height } - } else { - 0.0 + private val NSNotification.duration: Double + get() { + return userInfo?.get(UIKeyboardAnimationDurationUserInfoKey) as? Double ?: 0.0 } - UIView.animateWithDuration( - duration = duration, - animations = { - //set final destination for animation - keyboardAnimationView.setFrame(CGRectMake(0.0, toValue, 0.0, 0.0)) - }, - completion = { isFinished -> - if (isFinished) { - keyboardAnimationListener?.invalidate() - keyboardAnimationListener = null - keyboardAnimationView.removeFromSuperview() - } else { - //animation was canceled by other animation - } - } - ) - } - private fun calcFocusedLiftingY( - composeSceneMediator: ComposeSceneMediator, - focusedRect: DpRect, - keyboardHeight: Double - ): Double { - val viewHeight = composeSceneMediator.getViewHeight() - val hiddenPartOfFocusedElement: Double = - keyboardHeight - viewHeight + focusedRect.bottom.value - return if (hiddenPartOfFocusedElement > 0) { - // If focused element is partially hidden by the keyboard, we need to lift it upper - val focusedTopY = focusedRect.top.value - val isFocusedElementRemainsVisible = hiddenPartOfFocusedElement < focusedTopY - if (isFocusedElementRemainsVisible) { - // We need to lift focused element to be fully visible - hiddenPartOfFocusedElement - } else { - // In this case focused element height is bigger than remain part of the screen after showing the keyboard. - // Top edge of focused element should be visible. Same logic on Android. - maxOf(focusedTopY, 0f).toDouble() - } - } else { - // Focused element is not hidden by the keyboard. - 0.0 + private val NSNotification.endFrame: CValue + get() { + val value = userInfo?.get(UIKeyboardFrameEndUserInfoKey) as? NSValue + return value?.CGRectValue() ?: CGRectZero.readValue() } - } - private fun updateViewBounds(offsetX: Double = 0.0, offsetY: Double = 0.0) { - view.layer.setBounds( - view.frame.useContents { - CGRectMake( - x = offsetX, - y = offsetY, - width = size.width, - height = size.height - ) - } - ) - } + private val NSNotification.animationOptions: UIViewAnimationOptions + get() { + val value = userInfo?.get(UIKeyboardAnimationCurveUserInfoKey) as? NSNumber + return value?.unsignedIntegerValue() as UIViewAnimationOptions + } } From 57e1e249a49d7fbb7963284dcf1a1ac541e15ec5 Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Mon, 1 Apr 2024 11:51:16 +0200 Subject: [PATCH 02/12] Rework keyboard offsets logic and fix insets bugs connected with multiple layers --- .../foundation/layout/WindowInsets.uikit.kt | 21 +- .../text/selection/SelectionHandles.uikit.kt | 13 +- .../ui/platform/PlatformInsets.uikit.kt | 9 +- .../ui/scene/ComposeSceneMediator.uikit.kt | 14 +- .../ui/uikit/KeyboardOverlapHeight.uikit.kt | 16 +- .../ComposeContainerKeyboardManager.uikit.kt | 259 ----------------- .../ComposeSceneKeyboardOffsetManager.kt | 267 ++++++++++++++++++ 7 files changed, 300 insertions(+), 299 deletions(-) delete mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt create mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt diff --git a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt index a153cd435ed3a..a42ccf76a5716 100644 --- a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt +++ b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt @@ -18,16 +18,13 @@ package androidx.compose.foundation.layout import androidx.compose.runtime.Composable import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.State import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.platform.LocalLayoutMargins import androidx.compose.ui.platform.LocalSafeArea import androidx.compose.ui.platform.PlatformInsets import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalInterfaceOrientation -import androidx.compose.ui.uikit.LocalKeyboardBottomInset -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight import androidx.compose.ui.unit.dp private val ZeroInsets = WindowInsets(0, 0, 0, 0) @@ -40,20 +37,6 @@ private fun PlatformInsets.toWindowInsets() = WindowInsets( bottom = bottom, ) -private class ImeWindowInsets( - val keyboardBottomInset: State -): WindowInsets { - override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0 - - override fun getTop(density: Density): Int = 0 - - override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0 - - override fun getBottom(density: Density): Int = with(density) { - keyboardBottomInset.value.dp.roundToPx() - } -} - /** * This insets represents iOS SafeAreas. */ @@ -97,7 +80,7 @@ actual val WindowInsets.Companion.displayCutout: WindowInsets actual val WindowInsets.Companion.ime: WindowInsets @Composable @OptIn(InternalComposeApi::class) - get() = ImeWindowInsets(LocalKeyboardBottomInset.current) + get() = WindowInsets(bottom = LocalKeyboardOverlapHeight.current.dp) /** * These insets represent the space where system gestures have priority over application gestures. diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt index d9b61339bdd29..9bd215580a661 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.text.selection.HandleReferencePoint.TopLeft import androidx.compose.foundation.text.selection.HandleReferencePoint.TopMiddle import androidx.compose.foundation.text.selection.HandleReferencePoint.TopRight import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -35,6 +36,7 @@ import androidx.compose.ui.geometry.takeOrElse import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.ResolvedTextDirection +import androidx.compose.ui.uikit.LocalTextSelectionHandlersOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize @@ -60,6 +62,7 @@ private val RADIUS = 6.dp */ private val THICKNESS = 2.dp +@OptIn(InternalComposeApi::class) @Composable internal actual fun SelectionHandle( offsetProvider: OffsetProvider, @@ -148,6 +151,7 @@ internal fun Modifier.drawSelectionHandle( } } +@OptIn(InternalComposeApi::class) @Composable internal fun HandlePopup( offset: Offset, @@ -155,8 +159,13 @@ internal fun HandlePopup( handleReferencePoint: HandleReferencePoint, content: @Composable () -> Unit ) { - val popupPositionProvider = remember(handleReferencePoint, positionProvider, offset) { - HandlePositionProvider(handleReferencePoint, positionProvider, offset) + val cursorOffset = with(LocalDensity.current) { + LocalTextSelectionHandlersOffset.current.dp.toPx() + } + val handleOffset = offset.copy(y = offset.y - cursorOffset) + + val popupPositionProvider = remember(handleReferencePoint, positionProvider, handleOffset) { + HandlePositionProvider(handleReferencePoint, positionProvider, handleOffset) } Popup( popupPositionProvider = popupPositionProvider, diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt index 852abaaa27a4e..e92341c699cd5 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt @@ -19,10 +19,9 @@ package androidx.compose.ui.platform import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.uikit.LocalKeyboardBottomInset import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight +import androidx.compose.ui.uikit.LocalTextSelectionHandlersOffset import androidx.compose.ui.unit.dp /** @@ -43,7 +42,7 @@ private object SafeAreaInsetsConfig : InsetsConfig { @Composable get() = LocalSafeArea.current override val ime: PlatformInsets - @Composable get() = PlatformInsets(bottom = LocalKeyboardBottomInset.current.value.dp) + @Composable get() = PlatformInsets(bottom = LocalKeyboardOverlapHeight.current.dp) @Composable override fun excludeInsets( @@ -54,12 +53,12 @@ private object SafeAreaInsetsConfig : InsetsConfig { val safeArea = LocalSafeArea.current val layoutMargins = LocalLayoutMargins.current val keyboardOverlapHeight = LocalKeyboardOverlapHeight.current - val keyboardInsets = LocalKeyboardBottomInset.current + val selectionHandlersOffset = LocalTextSelectionHandlersOffset.current CompositionLocalProvider( LocalSafeArea provides if (safeInsets) PlatformInsets() else safeArea, LocalLayoutMargins provides if (safeInsets) layoutMargins.exclude(safeArea) else layoutMargins, LocalKeyboardOverlapHeight provides if (ime) 0f else keyboardOverlapHeight, - LocalKeyboardBottomInset provides if (ime) mutableStateOf(0f) else keyboardInsets, + LocalTextSelectionHandlersOffset provides if (ime) 0f else selectionHandlersOffset, content = content ) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index cc4cfea74c77c..543d9f1b64963 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -52,7 +52,7 @@ import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight -import androidx.compose.ui.uikit.LocalKeyboardBottomInset +import androidx.compose.ui.uikit.LocalTextSelectionHandlersOffset import androidx.compose.ui.uikit.systemDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset @@ -67,7 +67,7 @@ import androidx.compose.ui.unit.round import androidx.compose.ui.unit.roundToIntRect import androidx.compose.ui.unit.toDpRect import androidx.compose.ui.unit.toOffset -import androidx.compose.ui.window.ComposeContainerKeyboardManager +import androidx.compose.ui.window.ComposeSceneKeyboardOffsetManager import androidx.compose.ui.window.FocusStack import androidx.compose.ui.window.InteractionUIView import androidx.compose.ui.window.KeyboardEventHandler @@ -212,6 +212,7 @@ internal class ComposeSceneMediator( ) -> ComposeScene ) { private val keyboardOverlapHeightState: MutableState = mutableStateOf(0f) + private val textSelectionHandlersOffsetState: MutableState = mutableStateOf(0f) private var _layout: SceneLayout = SceneLayout.Undefined private var constraints: List = emptyList() set(value) { @@ -320,9 +321,10 @@ internal class ComposeSceneMediator( } private val keyboardManager by lazy { - ComposeContainerKeyboardManager( + ComposeSceneKeyboardOffsetManager( configuration = configuration, keyboardOverlapHeightState = keyboardOverlapHeightState, + textSelectionHandlersOffsetState = textSelectionHandlersOffsetState, viewProvider = { rootView }, densityProvider = { rootView.systemDensity }, composeSceneMediatorProvider = { this } @@ -343,8 +345,8 @@ internal class ComposeSceneMediator( renderingView.setNeedsDisplay() // redraw on next frame CATransaction.flush() // clear all animations }, - rootViewProvider = { container }, - densityProvider = { container.systemDensity }, + rootViewProvider = { rootView }, + densityProvider = { rootView.systemDensity }, viewConfiguration = viewConfiguration, focusStack = focusStack, keyboardEventHandler = keyboardEventHandler @@ -484,7 +486,7 @@ internal class ComposeSceneMediator( LocalUIKitInteropContext provides interopContext, LocalUIKitInteropContainer provides interopViewContainer, LocalKeyboardOverlapHeight provides keyboardOverlapHeightState.value, - LocalKeyboardBottomInset provides keyboardOverlapHeightState, + LocalTextSelectionHandlersOffset provides textSelectionHandlersOffsetState.value, LocalSafeArea provides safeAreaState.value, LocalLayoutMargins provides layoutMarginsState.value, content = content diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt index aaebba618b962..6cecfbad544f3 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt @@ -17,19 +17,19 @@ package androidx.compose.ui.uikit import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.runtime.compositionLocalOf /** * Composition local for height that is overlapped with keyboard over Compose view. */ -@Deprecated("Use LocalWindowKeyboardInsets instead") @InternalComposeApi -val LocalKeyboardOverlapHeight = staticCompositionLocalOf { 0f } +val LocalKeyboardOverlapHeight = compositionLocalOf { 0f } +/** + * Composition local for Selection Handlers vertical offset. + * Applied when [OnFocusBehavior.FocusableAboveKeyboard] used and + * [ComposeUIViewControllerConfiguration.platformLayers] are enabled. + */ @InternalComposeApi -val LocalKeyboardBottomInset = staticCompositionLocalOf> { - mutableStateOf(0f) -} +val LocalTextSelectionHandlersOffset = compositionLocalOf { 0f } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt deleted file mode 100644 index 366417e4b5639..0000000000000 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerKeyboardManager.uikit.kt +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2024 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. - */ - -package androidx.compose.ui.window - -import androidx.compose.runtime.MutableState -import androidx.compose.ui.scene.ComposeSceneMediator -import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration -import androidx.compose.ui.uikit.OnFocusBehavior -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.DpRect -import androidx.compose.ui.unit.toDpRect -import kotlin.math.max -import kotlinx.cinterop.BetaInteropApi -import kotlinx.cinterop.CValue -import kotlinx.cinterop.ObjCAction -import kotlinx.cinterop.useContents -import platform.CoreGraphics.CGFloat -import platform.CoreGraphics.CGPointMake -import platform.CoreGraphics.CGRect -import platform.CoreGraphics.CGRectGetMinY -import platform.CoreGraphics.CGRectIsEmpty -import platform.CoreGraphics.CGRectMake -import platform.Foundation.NSDefaultRunLoopMode -import platform.Foundation.NSRunLoop -import platform.QuartzCore.CADisplayLink -import platform.QuartzCore.CATransaction -import platform.QuartzCore.kCATransactionDisableActions -import platform.UIKit.UIView -import platform.UIKit.UIViewAnimationOptionBeginFromCurrentState -import platform.UIKit.UIViewAnimationOptionCurveEaseInOut -import platform.UIKit.UIViewAnimationOptions -import platform.darwin.NSObject -import platform.darwin.sel_registerName - - -internal class ComposeContainerKeyboardManager( - private val configuration: ComposeUIViewControllerConfiguration, - private val keyboardOverlapHeightState: MutableState, - private val viewProvider: () -> UIView, - private val densityProvider: () -> Density, - private val composeSceneMediatorProvider: () -> ComposeSceneMediator? -) : KeyboardVisibilityObserver { - - val view get() = viewProvider() - - fun start() { - KeyboardVisibilityListener.addObserver(this) - adjustViewBounds( - KeyboardVisibilityListener.keyboardFrame, - 0.25, - UIViewAnimationOptionCurveEaseInOut - ) - } - - fun stop() { - KeyboardVisibilityListener.removeObserver(this) - } - - //invisible view to track system keyboard animation - private val keyboardAnimationView: UIView by lazy { - UIView(CGRectMake(0.0, 0.0, 0.0, 0.0)).apply { - hidden = true - } - } - private var keyboardAnimationListener: CADisplayLink? = null - - override fun keyboardWillShow( - targetFrame: CValue, - duration: Double, - animationOptions: UIViewAnimationOptions - ) { - } - - override fun keyboardWillChangeFrame( - targetFrame: CValue, - duration: Double, - animationOptions: UIViewAnimationOptions - ) = adjustViewBounds(targetFrame, duration, animationOptions) - - override fun keyboardWillHide( - targetFrame: CValue, - duration: Double, - animationOptions: UIViewAnimationOptions - ) { - } - - private fun adjustViewBounds( - keyboardFrame: CValue, duration: Double, animationOptions: UIViewAnimationOptions - ) { - val screen = view.window?.screen ?: return - val keyboardScreenHeight = if (CGRectIsEmpty(keyboardFrame)) { - 0.0 - } else { - max(0.0, screen.bounds.useContents { size.height } - CGRectGetMinY(keyboardFrame)) - } - - val imeFrameLeft: Double = if ( - configuration.onFocusBehavior == OnFocusBehavior.FocusableAboveKeyboard - ) { - if (keyboardScreenHeight <= 0) { - // Keyboard is not visible - updateViewBounds(offsetY = 0.0) - 0.0 - } else { - val mediator = composeSceneMediatorProvider() - val focusedRect = - mediator?.focusManager?.getFocusRect()?.toDpRect(densityProvider()) - - if (focusedRect != null) { - val offsetY = calcFocusedLiftingY(mediator, focusedRect, keyboardScreenHeight) - updateViewBounds(offsetY = offsetY) - keyboardScreenHeight - offsetY - } else { - updateViewBounds(offsetY = 0.0) - keyboardScreenHeight - } - } - } else { - max(keyboardScreenHeight, 0.0) - } - - val bottomIndent = run { - val screenHeight = screen.bounds.useContents { size.height } - val composeViewBottomY = screen.coordinateSpace.convertPoint( - point = CGPointMake(0.0, view.frame.useContents { size.height }), - fromCoordinateSpace = view.coordinateSpace - ).useContents { y } - screenHeight - composeViewBottomY - } - - animateKeyboard(imeFrameLeft, bottomIndent, duration, animationOptions) - } - - private fun animateKeyboard( - keyboardHeight: CGFloat, - bottomIndent: CGFloat, - duration: Double, - animationOptions: UIViewAnimationOptions - ) { - //return actual keyboard height during animation - fun getCurrentKeyboardHeight(): CGFloat { - val layer = keyboardAnimationView.layer.presentationLayer() ?: return 0.0 - return layer.frame.useContents { origin.y } - } - - //attach to root view if needed - if (keyboardAnimationView.superview == null) { - view.addSubview(keyboardAnimationView) - } - - //cancel previous animation - keyboardAnimationView.layer.removeAllAnimations() - keyboardAnimationListener?.invalidate() - - //synchronize actual keyboard height with keyboardAnimationView without animation - val current = getCurrentKeyboardHeight() - CATransaction.begin() - CATransaction.setValue(true, kCATransactionDisableActions) - keyboardAnimationView.setFrame(CGRectMake(0.0, current, 0.0, 0.0)) - CATransaction.commit() - - //animation listener - keyboardAnimationListener = CADisplayLink.displayLinkWithTarget( - target = object : NSObject() { - @OptIn(BetaInteropApi::class) - @Suppress("unused") - @ObjCAction - fun animationDidUpdate() { - val currentHeight = getCurrentKeyboardHeight() - keyboardOverlapHeightState.value = - max(0f, (currentHeight - bottomIndent).toFloat()) - } - }, - selector = sel_registerName("animationDidUpdate") - ).apply { - addToRunLoop(NSRunLoop.mainRunLoop(), NSDefaultRunLoopMode) - } - - fun completeAnimation() { - keyboardAnimationListener?.invalidate() - keyboardAnimationListener = null - keyboardAnimationView.removeFromSuperview() - keyboardOverlapHeightState.value = max(0f, (keyboardHeight - bottomIndent).toFloat()) - } - - if (duration > 0 && current != keyboardHeight) { - UIView.animateWithDuration( - duration = duration, - delay = 0.0, - options = animationOptions or UIViewAnimationOptionBeginFromCurrentState, - animations = { - //set final destination for animation - keyboardAnimationView.setFrame(CGRectMake(0.0, keyboardHeight, 0.0, 0.0)) - }, - completion = { isFinished -> - if (isFinished) { - completeAnimation() - } - } - ) - } else { - keyboardAnimationView.setFrame(CGRectMake(0.0, keyboardHeight, 0.0, 0.0)) - completeAnimation() - } - } - - private fun calcFocusedLiftingY( - composeSceneMediator: ComposeSceneMediator, - focusedRect: DpRect, - keyboardHeight: Double - ): Double { - val viewHeight = composeSceneMediator.getViewHeight() - val hiddenPartOfFocusedElement: Double = - keyboardHeight - viewHeight + focusedRect.bottom.value - return if (hiddenPartOfFocusedElement > 0) { - // If focused element is partially hidden by the keyboard, we need to lift it upper - val focusedTopY = focusedRect.top.value - val isFocusedElementRemainsVisible = hiddenPartOfFocusedElement < focusedTopY - if (isFocusedElementRemainsVisible) { - // We need to lift focused element to be fully visible - hiddenPartOfFocusedElement - } else { - // In this case focused element height is bigger than remain part of the screen after showing the keyboard. - // Top edge of focused element should be visible. Same logic on Android. - maxOf(focusedTopY, 0f).toDouble() - } - } else { - // Focused element is not hidden by the keyboard. - 0.0 - } - } - - private fun updateViewBounds(offsetX: Double = 0.0, offsetY: Double = 0.0) { - view.layer.setBounds( - view.frame.useContents { - CGRectMake( - x = offsetX, - y = offsetY, - width = size.width, - height = size.height - ) - } - ) - } -} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt new file mode 100644 index 0000000000000..58df9f98e4ab9 --- /dev/null +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt @@ -0,0 +1,267 @@ +/* + * Copyright 2024 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. + */ + +package androidx.compose.ui.window + +import androidx.compose.runtime.ExperimentalComposeApi +import androidx.compose.runtime.MutableState +import androidx.compose.ui.scene.ComposeSceneMediator +import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration +import androidx.compose.ui.uikit.OnFocusBehavior +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.toDpRect +import kotlin.math.max +import kotlin.math.min +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ObjCAction +import kotlinx.cinterop.readValue +import kotlinx.cinterop.useContents +import platform.CoreGraphics.CGAffineTransformMakeTranslation +import platform.CoreGraphics.CGPointMake +import platform.CoreGraphics.CGRect +import platform.CoreGraphics.CGRectGetMinY +import platform.CoreGraphics.CGRectIsEmpty +import platform.CoreGraphics.CGRectMake +import platform.CoreGraphics.CGRectZero +import platform.Foundation.NSDefaultRunLoopMode +import platform.Foundation.NSRunLoop +import platform.QuartzCore.CADisplayLink +import platform.UIKit.UIView +import platform.UIKit.UIViewAnimationOptionBeginFromCurrentState +import platform.UIKit.UIViewAnimationOptionCurveEaseInOut +import platform.UIKit.UIViewAnimationOptions +import platform.darwin.NSObject +import platform.darwin.dispatch_async +import platform.darwin.dispatch_get_main_queue +import platform.darwin.sel_registerName + +internal class ComposeSceneKeyboardOffsetManager( + private val configuration: ComposeUIViewControllerConfiguration, + private val keyboardOverlapHeightState: MutableState, + private val textSelectionHandlersOffsetState: MutableState, + private val viewProvider: () -> UIView, + private val densityProvider: () -> Density, + private val composeSceneMediatorProvider: () -> ComposeSceneMediator? +) : KeyboardVisibilityObserver { + + val view get() = viewProvider() + + fun start() { + KeyboardVisibilityListener.addObserver(this) + adjustKeyboardBounds() + } + + fun adjustKeyboardBounds() { + adjustViewBounds( + KeyboardVisibilityListener.keyboardFrame, + KeyboardVisibilityListener.keyboardFrame, + 0.0, + UIViewAnimationOptionCurveEaseInOut + ) + } + + fun stop() { + KeyboardVisibilityListener.removeObserver(this) + } + + //invisible view to track system keyboard animation + private val animationView: UIView by lazy { + UIView(CGRectZero.readValue()).apply { + hidden = true + } + } + private var keyboardAnimationListener: CADisplayLink? = null + + override fun keyboardWillShow( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + } + + override fun keyboardWillChangeFrame( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + adjustViewBounds( + KeyboardVisibilityListener.keyboardFrame, + targetFrame, + max(0.1, duration), + animationOptions + ) + } + + override fun keyboardWillHide( + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + } + + private fun adjustViewBounds( + currentFrame: CValue, + targetFrame: CValue, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + val screen = view.window?.screen ?: return + + fun keyboardHeight(frame: CValue): Double { + return if (CGRectIsEmpty(frame)) { + 0.0 + } else { + max(0.0, screen.bounds.useContents { size.height } - CGRectGetMinY(frame)) + } + } + + val bottomIndent = run { + val screenHeight = screen.bounds.useContents { size.height } + val composeViewBottomY = screen.coordinateSpace.convertPoint( + point = CGPointMake(0.0, view.frame.useContents { size.height }), + fromCoordinateSpace = view.coordinateSpace + ).useContents { y } + screenHeight - composeViewBottomY - viewBottomOffset + } + + animateKeyboard( + previousKeyboardHeight = keyboardHeight(currentFrame), + keyboardHeight = keyboardHeight(targetFrame), + viewBottomIndent = bottomIndent, + duration = duration, + animationOptions = animationOptions + ) + } + + private fun animateKeyboard( + previousKeyboardHeight: Double, + keyboardHeight: Double, + viewBottomIndent: Double, + duration: Double, + animationOptions: UIViewAnimationOptions + ) { + // Animate view from 0 to [animationTargetSize] and normalize to animation progress with + // range of [0..1] to follow UIKit animation curve values. + val animationTargetSize = 1000.0 + val animationTargetFrame = CGRectMake(0.0, 0.0, 0.0, animationTargetSize) + fun getCurrentAnimationProgress(): Double { + val layer = animationView.layer.presentationLayer() ?: return 0.0 + return layer.frame.useContents { size.height / animationTargetSize } + } + + fun updateAnimationValues(progress: Double) { + val currentHeight = previousKeyboardHeight + + (keyboardHeight - previousKeyboardHeight) * progress + val currentOverlapHeight = max(0.0, currentHeight - viewBottomIndent) + keyboardOverlapHeightState.value = currentOverlapHeight.toFloat() + + val targetBottomOffset = calcFocusedBottomOffsetY(currentOverlapHeight) + viewBottomOffset += (targetBottomOffset - viewBottomOffset) * progress + } + + fun completeAnimation() { + animationView.removeFromSuperview() + updateAnimationValues(1.0) + } + + //attach to root view if needed + if (animationView.superview == null) { + view.addSubview(animationView) + } + + //cancel previous animation + animationView.layer.removeAllAnimations() + keyboardAnimationListener?.invalidate() + + UIView.performWithoutAnimation { + animationView.setFrame(CGRectZero.readValue()) + } + + //animation listener + val keyboardDisplayLink = CADisplayLink.displayLinkWithTarget( + target = object : NSObject() { + @OptIn(BetaInteropApi::class) + @Suppress("unused") + @ObjCAction + fun animationDidUpdate() { + updateAnimationValues(getCurrentAnimationProgress()) + } + }, + selector = sel_registerName("animationDidUpdate") + ) + keyboardAnimationListener = keyboardDisplayLink + UIView.animateWithDuration( + duration = duration, + delay = 0.0, + options = animationOptions or UIViewAnimationOptionBeginFromCurrentState, + animations = { + animationView.setFrame(animationTargetFrame) + }, + completion = { isFinished -> + keyboardDisplayLink.invalidate() + if (isFinished) { + completeAnimation() + } + } + ) + // HACK: Add display link observer to run loop in the next run loop cycle to fix issue + // where view's presentationLayer sometimes gets end bounds on the first animation frame + // instead of the initial one. + dispatch_async(dispatch_get_main_queue()) { + keyboardDisplayLink.addToRunLoop(NSRunLoop.mainRunLoop(), NSDefaultRunLoopMode) + } + } + + private fun calcFocusedBottomOffsetY(overlappingHeight: Double): Double { + if (configuration.onFocusBehavior != OnFocusBehavior.FocusableAboveKeyboard) { + return 0.0 + } + val mediator = composeSceneMediatorProvider() + val focusedRect = + mediator?.focusManager?.getFocusRect()?.toDpRect(densityProvider()) ?: return 0.0 + + val viewHeight = view.frame.useContents { size.height } + + val hiddenPartOfFocusedElement = overlappingHeight - viewHeight + focusedRect.bottom.value + return if (hiddenPartOfFocusedElement > 0) { + // If focused element is partially hidden by the keyboard, we need to lift it upper + val focusedTopY = focusedRect.top.value + val isFocusedElementRemainsVisible = hiddenPartOfFocusedElement < focusedTopY + if (isFocusedElementRemainsVisible) { + // We need to lift focused element to be fully visible + min(overlappingHeight, hiddenPartOfFocusedElement) + } else { + // In this case focused element height is bigger than remain part of the screen after showing the keyboard. + // Top edge of focused element should be visible. Same logic on Android. + min(overlappingHeight, maxOf(focusedTopY, 0f).toDouble()) + } + } else { + // Focused element is not hidden by the keyboard. + 0.0 + } + } + + @OptIn(ExperimentalComposeApi::class) + private var viewBottomOffset: Double = 0.0 + set(newValue) { + field = newValue + if (configuration.platformLayers) { + textSelectionHandlersOffsetState.value = newValue.toFloat() + } + view.layer.setAffineTransform(CGAffineTransformMakeTranslation(0.0, -newValue)) + } +} From 27f77085e8c6bd360015337afc4941c2ef27fb41 Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Thu, 4 Apr 2024 14:30:45 +0200 Subject: [PATCH 03/12] Group keyboard display parameters in single data structure --- .../foundation/layout/WindowInsets.uikit.kt | 4 +- .../text/selection/SelectionHandles.uikit.kt | 4 +- .../ui/platform/PlatformInsets.uikit.kt | 14 +++--- .../ui/scene/ComposeSceneMediator.uikit.kt | 21 +++----- .../ui/uikit/KeyboardOverlapHeight.uikit.kt | 35 ------------- .../KeyboardSceneDisplayParameters.uikit.kt | 50 +++++++++++++++++++ .../ComposeSceneKeyboardOffsetManager.kt | 24 +++++---- 7 files changed, 82 insertions(+), 70 deletions(-) delete mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt create mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardSceneDisplayParameters.uikit.kt diff --git a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt index a42ccf76a5716..e1945d0f2d77f 100644 --- a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt +++ b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.platform.LocalSafeArea import androidx.compose.ui.platform.PlatformInsets import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalInterfaceOrientation -import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight +import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters import androidx.compose.ui.unit.dp private val ZeroInsets = WindowInsets(0, 0, 0, 0) @@ -80,7 +80,7 @@ actual val WindowInsets.Companion.displayCutout: WindowInsets actual val WindowInsets.Companion.ime: WindowInsets @Composable @OptIn(InternalComposeApi::class) - get() = WindowInsets(bottom = LocalKeyboardOverlapHeight.current.dp) + get() = WindowInsets(bottom = LocalKeyboardSceneDisplayParameters.current.imeBottomInset.dp) /** * These insets represent the space where system gestures have priority over application gestures. diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt index 9bd215580a661..53ab15e6a10d7 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.geometry.takeOrElse import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.ResolvedTextDirection -import androidx.compose.ui.uikit.LocalTextSelectionHandlersOffset +import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize @@ -160,7 +160,7 @@ internal fun HandlePopup( content: @Composable () -> Unit ) { val cursorOffset = with(LocalDensity.current) { - LocalTextSelectionHandlersOffset.current.dp.toPx() + LocalKeyboardSceneDisplayParameters.current.textSelectionHandlersOffset.dp.toPx() } val handleOffset = offset.copy(y = offset.y - cursorOffset) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt index e92341c699cd5..098a6cef8bc40 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt @@ -20,8 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight -import androidx.compose.ui.uikit.LocalTextSelectionHandlersOffset +import androidx.compose.ui.uikit.KeyboardSceneDisplayParameters +import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters import androidx.compose.ui.unit.dp /** @@ -42,7 +42,9 @@ private object SafeAreaInsetsConfig : InsetsConfig { @Composable get() = LocalSafeArea.current override val ime: PlatformInsets - @Composable get() = PlatformInsets(bottom = LocalKeyboardOverlapHeight.current.dp) + @Composable get() = PlatformInsets( + bottom = LocalKeyboardSceneDisplayParameters.current.imeBottomInset.dp + ) @Composable override fun excludeInsets( @@ -52,13 +54,11 @@ private object SafeAreaInsetsConfig : InsetsConfig { ) { val safeArea = LocalSafeArea.current val layoutMargins = LocalLayoutMargins.current - val keyboardOverlapHeight = LocalKeyboardOverlapHeight.current - val selectionHandlersOffset = LocalTextSelectionHandlersOffset.current + val keyboardSceneDisplayParameters = LocalKeyboardSceneDisplayParameters.current CompositionLocalProvider( LocalSafeArea provides if (safeInsets) PlatformInsets() else safeArea, LocalLayoutMargins provides if (safeInsets) layoutMargins.exclude(safeArea) else layoutMargins, - LocalKeyboardOverlapHeight provides if (ime) 0f else keyboardOverlapHeight, - LocalTextSelectionHandlersOffset provides if (ime) 0f else selectionHandlersOffset, + LocalKeyboardSceneDisplayParameters provides if (ime) KeyboardSceneDisplayParameters.initial else keyboardSceneDisplayParameters, content = content ) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 52dc2d5b33b08..98d64bb1b0d32 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -50,12 +50,10 @@ import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration -import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight -import androidx.compose.ui.uikit.LocalTextSelectionHandlersOffset +import androidx.compose.ui.uikit.KeyboardSceneDisplayParameters +import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters import androidx.compose.ui.uikit.systemDensity import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.DpRect import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.asCGRect @@ -89,10 +87,6 @@ import platform.CoreGraphics.CGRect import platform.CoreGraphics.CGRectMake import platform.CoreGraphics.CGRectZero import platform.CoreGraphics.CGSize -import platform.Foundation.NSTimeInterval -import platform.Foundation.NSNotification -import platform.Foundation.NSNotificationCenter -import platform.Foundation.NSSelectorFromString import platform.QuartzCore.CATransaction import platform.UIKit.NSLayoutConstraint import platform.UIKit.UIEvent @@ -218,8 +212,9 @@ internal class ComposeSceneMediator( coroutineContext: CoroutineContext ) -> ComposeScene ) { - private val keyboardOverlapHeightState: MutableState = mutableStateOf(0f) - private val textSelectionHandlersOffsetState: MutableState = mutableStateOf(0f) + private val keyboardSceneDisplayParameters = mutableStateOf( + KeyboardSceneDisplayParameters.initial + ) private var _layout: SceneLayout = SceneLayout.Undefined private var constraints: List = emptyList() set(value) { @@ -337,8 +332,7 @@ internal class ComposeSceneMediator( private val keyboardManager by lazy { ComposeSceneKeyboardOffsetManager( configuration = configuration, - keyboardOverlapHeightState = keyboardOverlapHeightState, - textSelectionHandlersOffsetState = textSelectionHandlersOffsetState, + keyboardSceneDisplayParameters = keyboardSceneDisplayParameters, viewProvider = { rootView }, densityProvider = { rootView.systemDensity }, composeSceneMediatorProvider = { this } @@ -499,8 +493,7 @@ internal class ComposeSceneMediator( CompositionLocalProvider( LocalUIKitInteropContext provides interopContext, LocalUIKitInteropContainer provides interopViewContainer, - LocalKeyboardOverlapHeight provides keyboardOverlapHeightState.value, - LocalTextSelectionHandlersOffset provides textSelectionHandlersOffsetState.value, + LocalKeyboardSceneDisplayParameters provides keyboardSceneDisplayParameters.value, LocalSafeArea provides safeAreaState.value, LocalLayoutMargins provides layoutMarginsState.value, content = content diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt deleted file mode 100644 index 6cecfbad544f3..0000000000000 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 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. - */ - -package androidx.compose.ui.uikit - -import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.compositionLocalOf - - -/** - * Composition local for height that is overlapped with keyboard over Compose view. - */ -@InternalComposeApi -val LocalKeyboardOverlapHeight = compositionLocalOf { 0f } - -/** - * Composition local for Selection Handlers vertical offset. - * Applied when [OnFocusBehavior.FocusableAboveKeyboard] used and - * [ComposeUIViewControllerConfiguration.platformLayers] are enabled. - */ -@InternalComposeApi -val LocalTextSelectionHandlersOffset = compositionLocalOf { 0f } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardSceneDisplayParameters.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardSceneDisplayParameters.uikit.kt new file mode 100644 index 0000000000000..64fca0f99b4e6 --- /dev/null +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardSceneDisplayParameters.uikit.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2024 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. + */ + +package androidx.compose.ui.uikit + +import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.compositionLocalOf + + +data class KeyboardSceneDisplayParameters( + /** + * Height that is overlapped with keyboard over Compose view. + */ + val imeBottomInset: Float, + + /** + * Selection Handlers vertical offset when keyboard is visible. + * Applied when [OnFocusBehavior.FocusableAboveKeyboard] used and + * [ComposeUIViewControllerConfiguration.platformLayers] are enabled. + */ + val textSelectionHandlersOffset: Float +) { + companion object { + val initial = KeyboardSceneDisplayParameters( + imeBottomInset = 0f, + textSelectionHandlersOffset = 0f + ) + } +} + +/** + * Composition local for keyboard display parameters for the current scene. + */ +@InternalComposeApi +val LocalKeyboardSceneDisplayParameters = compositionLocalOf { + KeyboardSceneDisplayParameters.initial +} diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt index 58df9f98e4ab9..7b194b47b815b 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.ExperimentalComposeApi import androidx.compose.runtime.MutableState import androidx.compose.ui.scene.ComposeSceneMediator import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration +import androidx.compose.ui.uikit.KeyboardSceneDisplayParameters import androidx.compose.ui.uikit.OnFocusBehavior import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.toDpRect @@ -51,8 +52,7 @@ import platform.darwin.sel_registerName internal class ComposeSceneKeyboardOffsetManager( private val configuration: ComposeUIViewControllerConfiguration, - private val keyboardOverlapHeightState: MutableState, - private val textSelectionHandlersOffsetState: MutableState, + private val keyboardSceneDisplayParameters: MutableState, private val viewProvider: () -> UIView, private val densityProvider: () -> Density, private val composeSceneMediatorProvider: () -> ComposeSceneMediator? @@ -62,10 +62,7 @@ internal class ComposeSceneKeyboardOffsetManager( fun start() { KeyboardVisibilityListener.addObserver(this) - adjustKeyboardBounds() - } - fun adjustKeyboardBounds() { adjustViewBounds( KeyboardVisibilityListener.keyboardFrame, KeyboardVisibilityListener.keyboardFrame, @@ -147,6 +144,7 @@ internal class ComposeSceneKeyboardOffsetManager( ) } + @OptIn(ExperimentalComposeApi::class) private fun animateKeyboard( previousKeyboardHeight: Double, keyboardHeight: Double, @@ -167,10 +165,20 @@ internal class ComposeSceneKeyboardOffsetManager( val currentHeight = previousKeyboardHeight + (keyboardHeight - previousKeyboardHeight) * progress val currentOverlapHeight = max(0.0, currentHeight - viewBottomIndent) - keyboardOverlapHeightState.value = currentOverlapHeight.toFloat() val targetBottomOffset = calcFocusedBottomOffsetY(currentOverlapHeight) viewBottomOffset += (targetBottomOffset - viewBottomOffset) * progress + + val textSelectionHandlersOffset = if (configuration.platformLayers) { + viewBottomOffset.toFloat() + } else { + 0f + } + + keyboardSceneDisplayParameters.value = KeyboardSceneDisplayParameters( + imeBottomInset = currentOverlapHeight.toFloat(), + textSelectionHandlersOffset = textSelectionHandlersOffset + ) } fun completeAnimation() { @@ -255,13 +263,9 @@ internal class ComposeSceneKeyboardOffsetManager( } } - @OptIn(ExperimentalComposeApi::class) private var viewBottomOffset: Double = 0.0 set(newValue) { field = newValue - if (configuration.platformLayers) { - textSelectionHandlersOffsetState.value = newValue.toFloat() - } view.layer.setAffineTransform(CGAffineTransformMakeTranslation(0.0, -newValue)) } } From 9d89d92a7fa6ed3ca4e9fd8571ba37c5a8fea847 Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Thu, 4 Apr 2024 17:59:05 +0200 Subject: [PATCH 04/12] Refactor SoftwareKeyboardState --- .../foundation/layout/WindowInsets.uikit.kt | 5 ++--- .../text/selection/SelectionHandles.uikit.kt | 4 ++-- .../ui/platform/PlatformInsets.uikit.kt | 11 +++++----- .../ui/scene/ComposeSceneMediator.uikit.kt | 14 +++++++------ ...ikit.kt => SoftwareKeyboardState.uikit.kt} | 20 +++++++++---------- .../ComposeSceneKeyboardOffsetManager.kt | 13 +++++++----- 6 files changed, 35 insertions(+), 32 deletions(-) rename compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/{KeyboardSceneDisplayParameters.uikit.kt => SoftwareKeyboardState.uikit.kt} (71%) diff --git a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt index e1945d0f2d77f..6c453f1ce7f2a 100644 --- a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt +++ b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt @@ -24,8 +24,7 @@ import androidx.compose.ui.platform.LocalSafeArea import androidx.compose.ui.platform.PlatformInsets import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalInterfaceOrientation -import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters -import androidx.compose.ui.unit.dp +import androidx.compose.ui.uikit.LocalSoftwareKeyboardState private val ZeroInsets = WindowInsets(0, 0, 0, 0) @@ -80,7 +79,7 @@ actual val WindowInsets.Companion.displayCutout: WindowInsets actual val WindowInsets.Companion.ime: WindowInsets @Composable @OptIn(InternalComposeApi::class) - get() = WindowInsets(bottom = LocalKeyboardSceneDisplayParameters.current.imeBottomInset.dp) + get() = WindowInsets(bottom = LocalSoftwareKeyboardState.current.imeBottomInset) /** * These insets represent the space where system gestures have priority over application gestures. diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt index 53ab15e6a10d7..13b645296fd3a 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.geometry.takeOrElse import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.ResolvedTextDirection -import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters +import androidx.compose.ui.uikit.LocalSoftwareKeyboardState import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize @@ -160,7 +160,7 @@ internal fun HandlePopup( content: @Composable () -> Unit ) { val cursorOffset = with(LocalDensity.current) { - LocalKeyboardSceneDisplayParameters.current.textSelectionHandlersOffset.dp.toPx() + LocalSoftwareKeyboardState.current.textSelectionHandlersOffset.toPx() } val handleOffset = offset.copy(y = offset.y - cursorOffset) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt index 098a6cef8bc40..171956356a269 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt @@ -20,9 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.uikit.KeyboardSceneDisplayParameters -import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters -import androidx.compose.ui.unit.dp +import androidx.compose.ui.uikit.LocalSoftwareKeyboardState +import androidx.compose.ui.uikit.SoftwareKeyboardState /** * Composition local for SafeArea of ComposeUIViewController @@ -43,7 +42,7 @@ private object SafeAreaInsetsConfig : InsetsConfig { override val ime: PlatformInsets @Composable get() = PlatformInsets( - bottom = LocalKeyboardSceneDisplayParameters.current.imeBottomInset.dp + bottom = LocalSoftwareKeyboardState.current.imeBottomInset ) @Composable @@ -54,11 +53,11 @@ private object SafeAreaInsetsConfig : InsetsConfig { ) { val safeArea = LocalSafeArea.current val layoutMargins = LocalLayoutMargins.current - val keyboardSceneDisplayParameters = LocalKeyboardSceneDisplayParameters.current + val softwareKeyboardState = LocalSoftwareKeyboardState.current CompositionLocalProvider( LocalSafeArea provides if (safeInsets) PlatformInsets() else safeArea, LocalLayoutMargins provides if (safeInsets) layoutMargins.exclude(safeArea) else layoutMargins, - LocalKeyboardSceneDisplayParameters provides if (ime) KeyboardSceneDisplayParameters.initial else keyboardSceneDisplayParameters, + LocalSoftwareKeyboardState provides if (ime) SoftwareKeyboardState.Initial else softwareKeyboardState, content = content ) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 98d64bb1b0d32..278181d2a9321 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -50,8 +50,8 @@ import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration -import androidx.compose.ui.uikit.KeyboardSceneDisplayParameters -import androidx.compose.ui.uikit.LocalKeyboardSceneDisplayParameters +import androidx.compose.ui.uikit.LocalSoftwareKeyboardState +import androidx.compose.ui.uikit.SoftwareKeyboardState import androidx.compose.ui.uikit.systemDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntRect @@ -212,8 +212,9 @@ internal class ComposeSceneMediator( coroutineContext: CoroutineContext ) -> ComposeScene ) { - private val keyboardSceneDisplayParameters = mutableStateOf( - KeyboardSceneDisplayParameters.initial + @OptIn(InternalComposeApi::class) + private val softwareKeyboardState = mutableStateOf( + SoftwareKeyboardState.Initial ) private var _layout: SceneLayout = SceneLayout.Undefined private var constraints: List = emptyList() @@ -329,10 +330,11 @@ internal class ComposeSceneMediator( ) } + @OptIn(InternalComposeApi::class) private val keyboardManager by lazy { ComposeSceneKeyboardOffsetManager( configuration = configuration, - keyboardSceneDisplayParameters = keyboardSceneDisplayParameters, + softwareKeyboardState = softwareKeyboardState, viewProvider = { rootView }, densityProvider = { rootView.systemDensity }, composeSceneMediatorProvider = { this } @@ -493,7 +495,7 @@ internal class ComposeSceneMediator( CompositionLocalProvider( LocalUIKitInteropContext provides interopContext, LocalUIKitInteropContainer provides interopViewContainer, - LocalKeyboardSceneDisplayParameters provides keyboardSceneDisplayParameters.value, + LocalSoftwareKeyboardState provides softwareKeyboardState.value, LocalSafeArea provides safeAreaState.value, LocalLayoutMargins provides layoutMarginsState.value, content = content diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardSceneDisplayParameters.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/SoftwareKeyboardState.uikit.kt similarity index 71% rename from compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardSceneDisplayParameters.uikit.kt rename to compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/SoftwareKeyboardState.uikit.kt index 64fca0f99b4e6..1737ce632c170 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardSceneDisplayParameters.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/SoftwareKeyboardState.uikit.kt @@ -18,33 +18,33 @@ package androidx.compose.ui.uikit import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.unit.Dp -data class KeyboardSceneDisplayParameters( +@InternalComposeApi +data class SoftwareKeyboardState( /** * Height that is overlapped with keyboard over Compose view. */ - val imeBottomInset: Float, + val imeBottomInset: Dp, /** * Selection Handlers vertical offset when keyboard is visible. * Applied when [OnFocusBehavior.FocusableAboveKeyboard] used and * [ComposeUIViewControllerConfiguration.platformLayers] are enabled. */ - val textSelectionHandlersOffset: Float + val textSelectionHandlersOffset: Dp ) { companion object { - val initial = KeyboardSceneDisplayParameters( - imeBottomInset = 0f, - textSelectionHandlersOffset = 0f + internal val Initial = SoftwareKeyboardState( + imeBottomInset = Dp(0f), + textSelectionHandlersOffset = Dp(0f) ) } } /** - * Composition local for keyboard display parameters for the current scene. + * Composition local for software keyboard state for the current scene. */ @InternalComposeApi -val LocalKeyboardSceneDisplayParameters = compositionLocalOf { - KeyboardSceneDisplayParameters.initial -} +val LocalSoftwareKeyboardState = compositionLocalOf { SoftwareKeyboardState.Initial } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt index 7b194b47b815b..7be6030a77d27 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt @@ -17,12 +17,14 @@ package androidx.compose.ui.window import androidx.compose.runtime.ExperimentalComposeApi +import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.MutableState import androidx.compose.ui.scene.ComposeSceneMediator import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration -import androidx.compose.ui.uikit.KeyboardSceneDisplayParameters import androidx.compose.ui.uikit.OnFocusBehavior +import androidx.compose.ui.uikit.SoftwareKeyboardState import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toDpRect import kotlin.math.max import kotlin.math.min @@ -50,9 +52,10 @@ import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue import platform.darwin.sel_registerName +@OptIn(InternalComposeApi::class) internal class ComposeSceneKeyboardOffsetManager( private val configuration: ComposeUIViewControllerConfiguration, - private val keyboardSceneDisplayParameters: MutableState, + private val softwareKeyboardState: MutableState, private val viewProvider: () -> UIView, private val densityProvider: () -> Density, private val composeSceneMediatorProvider: () -> ComposeSceneMediator? @@ -175,9 +178,9 @@ internal class ComposeSceneKeyboardOffsetManager( 0f } - keyboardSceneDisplayParameters.value = KeyboardSceneDisplayParameters( - imeBottomInset = currentOverlapHeight.toFloat(), - textSelectionHandlersOffset = textSelectionHandlersOffset + softwareKeyboardState.value = SoftwareKeyboardState( + imeBottomInset = currentOverlapHeight.dp, + textSelectionHandlersOffset = textSelectionHandlersOffset.dp ) } From e3ad910afd3fe6e1017c26efb4a7e96ddf1976da Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Fri, 5 Apr 2024 09:38:16 +0200 Subject: [PATCH 05/12] MR comments fixes --- .../androidx/compose/ui/window/ComposeContainer.uikit.kt | 2 +- .../compose/ui/window/ComposeSceneKeyboardOffsetManager.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt index 94b02b0f46132..3103abdbb4aa9 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt @@ -385,7 +385,7 @@ internal class ComposeContainer( configuration = configuration, focusStack = if (focusable) focusStack else null, windowContext = windowContext, - compositionContext = compositionContext + compositionContext = compositionContext, ) } } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt index 7be6030a77d27..f4cb36461536e 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt @@ -78,7 +78,9 @@ internal class ComposeSceneKeyboardOffsetManager( KeyboardVisibilityListener.removeObserver(this) } - //invisible view to track system keyboard animation + /** + * Invisible view to track system keyboard animation + */ private val animationView: UIView by lazy { UIView(CGRectZero.readValue()).apply { hidden = true From a942749de30c7808dac224584a46d56724a78fae Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Mon, 8 Apr 2024 09:29:00 +0200 Subject: [PATCH 06/12] Update compose scene offset logic --- .../foundation/layout/WindowInsets.uikit.kt | 4 +- .../text/selection/SelectionHandles.uikit.kt | 11 +--- .../ui/platform/PlatformInsets.uikit.kt | 12 ++--- .../ui/scene/ComposeSceneMediator.uikit.kt | 28 +++++------ .../ui/uikit/KeyboardOverlapHeight.uikit.kt | 28 +++++++++++ .../ui/uikit/SoftwareKeyboardState.uikit.kt | 50 ------------------- .../ComposeSceneKeyboardOffsetManager.kt | 28 +++-------- 7 files changed, 59 insertions(+), 102 deletions(-) create mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt delete mode 100644 compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/SoftwareKeyboardState.uikit.kt diff --git a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt index 6c453f1ce7f2a..326666dd73c31 100644 --- a/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt +++ b/compose/foundation/foundation-layout/src/uikitMain/kotlin/androidx/compose/foundation/layout/WindowInsets.uikit.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.platform.LocalSafeArea import androidx.compose.ui.platform.PlatformInsets import androidx.compose.ui.uikit.InterfaceOrientation import androidx.compose.ui.uikit.LocalInterfaceOrientation -import androidx.compose.ui.uikit.LocalSoftwareKeyboardState +import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight private val ZeroInsets = WindowInsets(0, 0, 0, 0) @@ -79,7 +79,7 @@ actual val WindowInsets.Companion.displayCutout: WindowInsets actual val WindowInsets.Companion.ime: WindowInsets @Composable @OptIn(InternalComposeApi::class) - get() = WindowInsets(bottom = LocalSoftwareKeyboardState.current.imeBottomInset) + get() = WindowInsets(bottom = LocalKeyboardOverlapHeight.current) /** * These insets represent the space where system gestures have priority over application gestures. diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt index 13b645296fd3a..ce9677a1b451e 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.geometry.takeOrElse import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.ResolvedTextDirection -import androidx.compose.ui.uikit.LocalSoftwareKeyboardState import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize @@ -151,7 +150,6 @@ internal fun Modifier.drawSelectionHandle( } } -@OptIn(InternalComposeApi::class) @Composable internal fun HandlePopup( offset: Offset, @@ -159,13 +157,8 @@ internal fun HandlePopup( handleReferencePoint: HandleReferencePoint, content: @Composable () -> Unit ) { - val cursorOffset = with(LocalDensity.current) { - LocalSoftwareKeyboardState.current.textSelectionHandlersOffset.toPx() - } - val handleOffset = offset.copy(y = offset.y - cursorOffset) - - val popupPositionProvider = remember(handleReferencePoint, positionProvider, handleOffset) { - HandlePositionProvider(handleReferencePoint, positionProvider, handleOffset) + val popupPositionProvider = remember(handleReferencePoint, positionProvider, offset) { + HandlePositionProvider(handleReferencePoint, positionProvider, offset) } Popup( popupPositionProvider = popupPositionProvider, diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt index 171956356a269..99aab182a62a7 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt @@ -20,8 +20,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.uikit.LocalSoftwareKeyboardState -import androidx.compose.ui.uikit.SoftwareKeyboardState +import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight +import androidx.compose.ui.unit.dp /** * Composition local for SafeArea of ComposeUIViewController @@ -41,9 +41,7 @@ private object SafeAreaInsetsConfig : InsetsConfig { @Composable get() = LocalSafeArea.current override val ime: PlatformInsets - @Composable get() = PlatformInsets( - bottom = LocalSoftwareKeyboardState.current.imeBottomInset - ) + @Composable get() = PlatformInsets(bottom = LocalKeyboardOverlapHeight.current) @Composable override fun excludeInsets( @@ -53,11 +51,11 @@ private object SafeAreaInsetsConfig : InsetsConfig { ) { val safeArea = LocalSafeArea.current val layoutMargins = LocalLayoutMargins.current - val softwareKeyboardState = LocalSoftwareKeyboardState.current + val keyboardOverlapHeight = LocalKeyboardOverlapHeight.current CompositionLocalProvider( LocalSafeArea provides if (safeInsets) PlatformInsets() else safeArea, LocalLayoutMargins provides if (safeInsets) layoutMargins.exclude(safeArea) else layoutMargins, - LocalSoftwareKeyboardState provides if (ime) SoftwareKeyboardState.Initial else softwareKeyboardState, + LocalKeyboardOverlapHeight provides if (ime) 0f.dp else keyboardOverlapHeight, content = content ) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 278181d2a9321..b4d4233f5ab1c 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -50,10 +50,10 @@ import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.semantics.SemanticsOwner import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration -import androidx.compose.ui.uikit.LocalSoftwareKeyboardState -import androidx.compose.ui.uikit.SoftwareKeyboardState +import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight import androidx.compose.ui.uikit.systemDensity import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.asCGRect @@ -82,6 +82,7 @@ import org.jetbrains.skiko.SkikoKeyboardEvent import org.jetbrains.skiko.SkikoKeyboardEventKind import platform.CoreGraphics.CGAffineTransformIdentity import platform.CoreGraphics.CGAffineTransformInvert +import platform.CoreGraphics.CGAffineTransformMakeTranslation import platform.CoreGraphics.CGPoint import platform.CoreGraphics.CGRect import platform.CoreGraphics.CGRectMake @@ -212,10 +213,7 @@ internal class ComposeSceneMediator( coroutineContext: CoroutineContext ) -> ComposeScene ) { - @OptIn(InternalComposeApi::class) - private val softwareKeyboardState = mutableStateOf( - SoftwareKeyboardState.Initial - ) + private val keyboardOverlapHeightState: MutableState = mutableStateOf(0f.dp) private var _layout: SceneLayout = SceneLayout.Undefined private var constraints: List = emptyList() set(value) { @@ -235,7 +233,7 @@ internal class ComposeSceneMediator( } } - private val scene: ComposeScene by lazy { + internal val scene: ComposeScene by lazy { composeSceneFactory( ::onComposeSceneInvalidate, IOSPlatformContext(), @@ -330,14 +328,16 @@ internal class ComposeSceneMediator( ) } - @OptIn(InternalComposeApi::class) private val keyboardManager by lazy { ComposeSceneKeyboardOffsetManager( configuration = configuration, - softwareKeyboardState = softwareKeyboardState, + keyboardOverlapHeightState = keyboardOverlapHeightState, viewProvider = { rootView }, - densityProvider = { rootView.systemDensity }, - composeSceneMediatorProvider = { this } + composeSceneMediatorProvider = { this }, + onComposeSceneOffsetChanged = { offset -> + rootView.layer.setAffineTransform(CGAffineTransformMakeTranslation(0.0, -offset)) + scene.invalidatePositionInWindow() + } ) } @@ -495,7 +495,7 @@ internal class ComposeSceneMediator( CompositionLocalProvider( LocalUIKitInteropContext provides interopContext, LocalUIKitInteropContainer provides interopViewContainer, - LocalSoftwareKeyboardState provides softwareKeyboardState.value, + LocalKeyboardOverlapHeight provides keyboardOverlapHeightState.value, LocalSafeArea provides safeAreaState.value, LocalLayoutMargins provides layoutMarginsState.value, content = content @@ -666,10 +666,10 @@ internal class ComposeSceneMediator( override val windowInfo: WindowInfo get() = windowContext.windowInfo override fun calculatePositionInWindow(localPosition: Offset): Offset = - windowContext.calculatePositionInWindow(container, localPosition) + windowContext.calculatePositionInWindow(rootView, localPosition) override fun calculateLocalPosition(positionInWindow: Offset): Offset = - windowContext.calculateLocalPosition(container, positionInWindow) + windowContext.calculateLocalPosition(rootView, positionInWindow) override val measureDrawLayerBounds get() = this@ComposeSceneMediator.measureDrawLayerBounds override val viewConfiguration get() = this@ComposeSceneMediator.viewConfiguration diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt new file mode 100644 index 0000000000000..e888f2a9516b4 --- /dev/null +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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. + */ + +package androidx.compose.ui.uikit + +import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.unit.dp + + +/** + * Composition local for height that is overlapped with keyboard over Compose view. + */ +@InternalComposeApi +val LocalKeyboardOverlapHeight = compositionLocalOf { 0f.dp } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/SoftwareKeyboardState.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/SoftwareKeyboardState.uikit.kt deleted file mode 100644 index 1737ce632c170..0000000000000 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/SoftwareKeyboardState.uikit.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2024 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. - */ - -package androidx.compose.ui.uikit - -import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.ui.unit.Dp - - -@InternalComposeApi -data class SoftwareKeyboardState( - /** - * Height that is overlapped with keyboard over Compose view. - */ - val imeBottomInset: Dp, - - /** - * Selection Handlers vertical offset when keyboard is visible. - * Applied when [OnFocusBehavior.FocusableAboveKeyboard] used and - * [ComposeUIViewControllerConfiguration.platformLayers] are enabled. - */ - val textSelectionHandlersOffset: Dp -) { - companion object { - internal val Initial = SoftwareKeyboardState( - imeBottomInset = Dp(0f), - textSelectionHandlersOffset = Dp(0f) - ) - } -} - -/** - * Composition local for software keyboard state for the current scene. - */ -@InternalComposeApi -val LocalSoftwareKeyboardState = compositionLocalOf { SoftwareKeyboardState.Initial } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt index f4cb36461536e..a4733aec91f63 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt @@ -17,13 +17,12 @@ package androidx.compose.ui.window import androidx.compose.runtime.ExperimentalComposeApi -import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.MutableState import androidx.compose.ui.scene.ComposeSceneMediator import androidx.compose.ui.uikit.ComposeUIViewControllerConfiguration import androidx.compose.ui.uikit.OnFocusBehavior -import androidx.compose.ui.uikit.SoftwareKeyboardState -import androidx.compose.ui.unit.Density +import androidx.compose.ui.uikit.systemDensity +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toDpRect import kotlin.math.max @@ -33,7 +32,6 @@ import kotlinx.cinterop.CValue import kotlinx.cinterop.ObjCAction import kotlinx.cinterop.readValue import kotlinx.cinterop.useContents -import platform.CoreGraphics.CGAffineTransformMakeTranslation import platform.CoreGraphics.CGPointMake import platform.CoreGraphics.CGRect import platform.CoreGraphics.CGRectGetMinY @@ -52,13 +50,12 @@ import platform.darwin.dispatch_async import platform.darwin.dispatch_get_main_queue import platform.darwin.sel_registerName -@OptIn(InternalComposeApi::class) internal class ComposeSceneKeyboardOffsetManager( private val configuration: ComposeUIViewControllerConfiguration, - private val softwareKeyboardState: MutableState, + private val keyboardOverlapHeightState: MutableState, private val viewProvider: () -> UIView, - private val densityProvider: () -> Density, - private val composeSceneMediatorProvider: () -> ComposeSceneMediator? + private val composeSceneMediatorProvider: () -> ComposeSceneMediator?, + private val onComposeSceneOffsetChanged: (Double) -> Unit, ) : KeyboardVisibilityObserver { val view get() = viewProvider() @@ -174,16 +171,7 @@ internal class ComposeSceneKeyboardOffsetManager( val targetBottomOffset = calcFocusedBottomOffsetY(currentOverlapHeight) viewBottomOffset += (targetBottomOffset - viewBottomOffset) * progress - val textSelectionHandlersOffset = if (configuration.platformLayers) { - viewBottomOffset.toFloat() - } else { - 0f - } - - softwareKeyboardState.value = SoftwareKeyboardState( - imeBottomInset = currentOverlapHeight.dp, - textSelectionHandlersOffset = textSelectionHandlersOffset.dp - ) + keyboardOverlapHeightState.value = currentOverlapHeight.dp } fun completeAnimation() { @@ -245,7 +233,7 @@ internal class ComposeSceneKeyboardOffsetManager( } val mediator = composeSceneMediatorProvider() val focusedRect = - mediator?.focusManager?.getFocusRect()?.toDpRect(densityProvider()) ?: return 0.0 + mediator?.focusManager?.getFocusRect()?.toDpRect(view.systemDensity) ?: return 0.0 val viewHeight = view.frame.useContents { size.height } @@ -271,6 +259,6 @@ internal class ComposeSceneKeyboardOffsetManager( private var viewBottomOffset: Double = 0.0 set(newValue) { field = newValue - view.layer.setAffineTransform(CGAffineTransformMakeTranslation(0.0, -newValue)) + onComposeSceneOffsetChanged(newValue) } } From 27cf43530ba3337641d154092ff3c8c03acb2512 Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Tue, 9 Apr 2024 20:44:04 +0200 Subject: [PATCH 07/12] Fix magnifier position --- .../compose/foundation/Magnifier.uikit.kt | 59 +++++++++++-------- .../foundation/PlatformMagnifier.uikit.kt | 7 +-- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt index 024094d786455..11df09afa4c5d 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt @@ -16,7 +16,6 @@ package androidx.compose.foundation -import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -28,7 +27,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.interop.LocalUIViewController import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.GlobalPositionAwareModifierNode @@ -47,18 +46,20 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize +import kotlinx.cinterop.useContents import kotlinx.coroutines.launch import org.jetbrains.skiko.OS import org.jetbrains.skiko.OSVersion import org.jetbrains.skiko.available +import platform.CoreGraphics.CGPointMake import platform.UIKit.UIView /** * A function on elements that are magnified with a [magnifier] modifier that returns the position - * of the center of the magnified content in the coordinate space of the root composable. + * of the center of the magnified content in the coordinate space of the window composable. */ -internal val MagnifierPositionInRoot = - SemanticsPropertyKey<() -> Offset>("MagnifierPositionInRoot") +internal val MagnifierPositionInWindow = + SemanticsPropertyKey<() -> Offset>("MagnifierPositionInWindow") internal fun Modifier.magnifier( sourceCenter: Density.() -> Offset, @@ -195,15 +196,15 @@ internal class MagnifierNode( private var magnifier: PlatformMagnifier? = null /** - * Anchor Composable's position in root layout. + * Anchor Composable's position in window layout. */ - private var anchorPositionInRoot: Offset by mutableStateOf(Offset.Unspecified) + private var anchorPositionInWindow: Offset by mutableStateOf(Offset.Unspecified) /** - * Position where [sourceCenter] is mapped on root layout. This is passed to platform magnifier - * to precisely target the requested location. + * Position where [sourceCenter] is mapped on window layout. This is passed to platform + * magnifier to precisely target the requested location. */ - private var sourceCenterInRoot: Offset = Offset.Unspecified + private var sourceCenterInWindow: Offset = Offset.Unspecified /** * Last reported size to [onSizeChanged]. This is compared to the current size before calling @@ -286,27 +287,37 @@ internal class MagnifierNode( val density = density ?: return val sourceCenterOffset = sourceCenter(density) - sourceCenterInRoot = - if (anchorPositionInRoot.isSpecified && sourceCenterOffset.isSpecified) { - anchorPositionInRoot + sourceCenterOffset + sourceCenterInWindow = + if (anchorPositionInWindow.isSpecified && sourceCenterOffset.isSpecified) { + anchorPositionInWindow + sourceCenterOffset } else { Offset.Unspecified } + val sourceCenterInView = view?.window?.takeIf { + sourceCenterInWindow.isSpecified + }?.let { window -> + view!!.convertPoint( + CGPointMake( + sourceCenterInWindow.x.toDouble() / density.density, + sourceCenterInWindow.y.toDouble() / density.density + ), + fromCoordinateSpace = window.coordinateSpace() + ).useContents { + Offset(x.toFloat() * density.density, y.toFloat() * density.density) + } + } + // Once the position is set, it's never null again, so we don't need to worry // about dismissing the magnifier if this expression changes value. - if (sourceCenterInRoot.isSpecified) { + if (sourceCenterInView != null) { // Calculate magnifier center if it's provided. Only accept if the returned value is - // specified. Then add [anchorPositionInRoot] for relative positioning. - val magnifierCenter = magnifierCenter?.invoke(density) + // specified. Then add [anchorPositionInWindow] for relative positioning. + magnifierCenter?.invoke(density) ?.takeIf { it.isSpecified } - ?.let { anchorPositionInRoot + it } - ?: Offset.Unspecified + ?.let { anchorPositionInWindow + it } - magnifier.update( - sourceCenter = sourceCenterInRoot, - magnifierCenter = magnifierCenter, - ) + magnifier.update(sourceCenter = sourceCenterInView) updateSizeIfNecessary() } else { // Can't place the magnifier at an unspecified location, so just hide it. @@ -338,11 +349,11 @@ internal class MagnifierNode( // The mutable state must store the Offset, not the LocalCoordinates, because the same // LocalCoordinates instance may be sent to this callback multiple times, not implement // equals, or be stable, and so won't invalidate the snapshotFlow. - anchorPositionInRoot = coordinates.positionInRoot() + anchorPositionInWindow = coordinates.positionInWindow() } override fun SemanticsPropertyReceiver.applySemantics() { - this[MagnifierPositionInRoot] = { sourceCenterInRoot } + this[MagnifierPositionInWindow] = { sourceCenterInWindow } } } diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/PlatformMagnifier.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/PlatformMagnifier.uikit.kt index 291ca4636cc46..c0e9db700f374 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/PlatformMagnifier.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/PlatformMagnifier.uikit.kt @@ -69,10 +69,7 @@ internal interface PlatformMagnifier { * Sets the properties on a Magnifier instance that can be updated without recreating the * magnifier. */ - fun update( - sourceCenter: Offset, - magnifierCenter: Offset - ) + fun update(sourceCenter: Offset) fun dismiss() } @@ -136,7 +133,7 @@ internal object PlatformMagnifierFactoryIos17Impl : PlatformMagnifierFactory { // is not required. loupe redraws automatically } - override fun update(sourceCenter: Offset, magnifierCenter: Offset) { + override fun update(sourceCenter: Offset) { if (sourceCenter.isUnspecified) return From 8e3ed2ec85f623ae83d6a877acc03ca03603b7fe Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Fri, 12 Apr 2024 15:22:51 +0200 Subject: [PATCH 08/12] Fix keyboard offset when tracking is not enabled. Fix magnifier and selection handlers location --- .../compose/foundation/Magnifier.uikit.kt | 7 ++++++- .../ui/scene/ComposeSceneMediator.uikit.kt | 17 ++++++++++++----- .../window/KeyboardVisibilityListener.uikit.kt | 8 +++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt index 11df09afa4c5d..aac1a5ebce26a 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/Magnifier.uikit.kt @@ -304,7 +304,12 @@ internal class MagnifierNode( ), fromCoordinateSpace = window.coordinateSpace() ).useContents { - Offset(x.toFloat() * density.density, y.toFloat() * density.density) + // HACK: Applying additional offset to adjust magnifier location + // when platform layers are disabled. + val additionalViewOffsetInWindow = view!!.layer.affineTransform().useContents { + Offset(tx.toFloat(), ty.toFloat()) * density.density + } + Offset(x.toFloat(), y.toFloat()) * density.density + additionalViewOffsetInWindow } } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index c800bf3155dd9..e384fafb59fbe 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -71,6 +71,7 @@ import androidx.compose.ui.window.ComposeSceneKeyboardOffsetManager import androidx.compose.ui.window.FocusStack import androidx.compose.ui.window.InteractionUIView import androidx.compose.ui.window.KeyboardEventHandler +import androidx.compose.ui.window.KeyboardVisibilityListener import androidx.compose.ui.window.RenderingUIView import androidx.compose.ui.window.UITouchesEventPhase import kotlin.coroutines.CoroutineContext @@ -332,10 +333,10 @@ internal class ComposeSceneMediator( ComposeSceneKeyboardOffsetManager( configuration = configuration, keyboardOverlapHeightState = keyboardOverlapHeightState, - viewProvider = { rootView }, + viewProvider = { viewForKeyboardOffsetTransform }, composeSceneMediatorProvider = { this }, onComposeSceneOffsetChanged = { offset -> - rootView.layer.setAffineTransform(CGAffineTransformMakeTranslation(0.0, -offset)) + viewForKeyboardOffsetTransform.layer.setAffineTransform(CGAffineTransformMakeTranslation(0.0, -offset)) scene.invalidatePositionInWindow() } ) @@ -360,7 +361,9 @@ internal class ComposeSceneMediator( viewConfiguration = viewConfiguration, focusStack = focusStack, keyboardEventHandler = keyboardEventHandler - ) + ).also { + KeyboardVisibilityListener.initialize() + } } private val touchesDelegate: InteractionUIView.Delegate by lazy { @@ -662,14 +665,18 @@ internal class ComposeSceneMediator( || scene.sendKeyEvent(keyEvent) || _onKeyEvent(keyEvent) + @OptIn(ExperimentalComposeApi::class) + private var viewForKeyboardOffsetTransform = if (configuration.platformLayers) + rootView else container + private inner class IOSPlatformContext : PlatformContext by PlatformContext.Empty { override val windowInfo: WindowInfo get() = windowContext.windowInfo override fun calculatePositionInWindow(localPosition: Offset): Offset = - windowContext.calculatePositionInWindow(rootView, localPosition) + windowContext.calculatePositionInWindow(viewForKeyboardOffsetTransform, localPosition) override fun calculateLocalPosition(positionInWindow: Offset): Offset = - windowContext.calculateLocalPosition(rootView, positionInWindow) + windowContext.calculateLocalPosition(viewForKeyboardOffsetTransform, positionInWindow) override val measureDrawLayerBounds get() = this@ComposeSceneMediator.measureDrawLayerBounds override val viewConfiguration get() = this@ComposeSceneMediator.viewConfiguration diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt index bd53420ddecb2..cf707d252f15c 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/KeyboardVisibilityListener.uikit.kt @@ -59,6 +59,12 @@ internal interface KeyboardVisibilityObserver { internal object KeyboardVisibilityListener { private val listener = NativeKeyboardVisibilityListener() + private var initOnce = false + fun initialize() { + if (initOnce) { return } + initOnce = true + listener.startKeyboardChangesObserving() + } fun addObserver(observer: KeyboardVisibilityObserver) = listener.observers.add(observer) @@ -70,7 +76,7 @@ internal object KeyboardVisibilityListener { private class NativeKeyboardVisibilityListener : NSObject() { val observers = mutableSetOf() - init { + fun startKeyboardChangesObserving() { NSNotificationCenter.defaultCenter.addObserver( observer = this, selector = NSSelectorFromString(::keyboardWillShow.name + ":"), From 3934ee8811c8242b8c6ecf9f1b9cddf1a3fb642a Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Fri, 12 Apr 2024 15:28:28 +0200 Subject: [PATCH 09/12] Fix MR comments --- .../androidx/compose/ui/platform/PlatformInsets.uikit.kt | 2 +- .../androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt | 4 ++-- .../androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt | 2 +- .../androidx/compose/ui/window/ComposeContainer.uikit.kt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt index 99aab182a62a7..f582b7ae7429e 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/PlatformInsets.uikit.kt @@ -55,7 +55,7 @@ private object SafeAreaInsetsConfig : InsetsConfig { CompositionLocalProvider( LocalSafeArea provides if (safeInsets) PlatformInsets() else safeArea, LocalLayoutMargins provides if (safeInsets) layoutMargins.exclude(safeArea) else layoutMargins, - LocalKeyboardOverlapHeight provides if (ime) 0f.dp else keyboardOverlapHeight, + LocalKeyboardOverlapHeight provides if (ime) 0.dp else keyboardOverlapHeight, content = content ) } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index e384fafb59fbe..7f94d06725cb2 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -214,7 +214,7 @@ internal class ComposeSceneMediator( coroutineContext: CoroutineContext ) -> ComposeScene ) { - private val keyboardOverlapHeightState: MutableState = mutableStateOf(0f.dp) + private val keyboardOverlapHeightState: MutableState = mutableStateOf(0.dp) private var _layout: SceneLayout = SceneLayout.Undefined private var constraints: List = emptyList() set(value) { @@ -234,7 +234,7 @@ internal class ComposeSceneMediator( } } - internal val scene: ComposeScene by lazy { + private val scene: ComposeScene by lazy { composeSceneFactory( ::onComposeSceneInvalidate, IOSPlatformContext(), diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt index e888f2a9516b4..f91a1320a0651 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/uikit/KeyboardOverlapHeight.uikit.kt @@ -25,4 +25,4 @@ import androidx.compose.ui.unit.dp * Composition local for height that is overlapped with keyboard over Compose view. */ @InternalComposeApi -val LocalKeyboardOverlapHeight = compositionLocalOf { 0f.dp } +val LocalKeyboardOverlapHeight = compositionLocalOf { 0.dp } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt index feb1c8c3476ec..f8a1d157bf347 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainer.uikit.kt @@ -338,7 +338,7 @@ internal class ComposeContainer( windowContext = windowContext, coroutineContext = coroutineDispatcher, renderingUIViewFactory = ::createSkikoUIView, - composeSceneFactory = ::createComposeScene + composeSceneFactory = ::createComposeScene, ) mediator.setContent { ProvideContainerCompositionLocals(this, content) From 11df713c41f2794f7eb1f11e7b9321275a1e7c43 Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Fri, 12 Apr 2024 17:17:19 +0200 Subject: [PATCH 10/12] Fix MR comments --- .../foundation/text/selection/SelectionHandles.uikit.kt | 2 -- .../compose/ui/scene/ComposeSceneMediator.uikit.kt | 8 +++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt index ce9677a1b451e..d9b61339bdd29 100644 --- a/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt +++ b/compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/text/selection/SelectionHandles.uikit.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.text.selection.HandleReferencePoint.TopLeft import androidx.compose.foundation.text.selection.HandleReferencePoint.TopMiddle import androidx.compose.foundation.text.selection.HandleReferencePoint.TopRight import androidx.compose.runtime.Composable -import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed @@ -61,7 +60,6 @@ private val RADIUS = 6.dp */ private val THICKNESS = 2.dp -@OptIn(InternalComposeApi::class) @Composable internal actual fun SelectionHandle( offsetProvider: OffsetProvider, diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 546e363fc3a58..e4fe7c0113caa 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -246,7 +246,6 @@ internal class ComposeSceneMediator( set(value) { scene.compositionLocalContext = value } - val focusManager get() = scene.focusManager private val renderingView by lazy { renderingUIViewFactory(interopContext, renderDelegate) @@ -649,8 +648,11 @@ internal class ComposeSceneMediator( || _onKeyEvent(keyEvent) @OptIn(ExperimentalComposeApi::class) - private var viewForKeyboardOffsetTransform = if (configuration.platformLayers) - rootView else container + private var viewForKeyboardOffsetTransform = if (configuration.platformLayers) { + rootView + } else { + container + } private inner class IOSPlatformContext : PlatformContext by PlatformContext.Empty { override val windowInfo: WindowInfo get() = windowContext.windowInfo From 41f058d772bed4b04516a8c2206d3807d2bd50dc Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Mon, 15 Apr 2024 11:09:11 +0200 Subject: [PATCH 11/12] Fix compilation --- .../androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index e4fe7c0113caa..197e12d576624 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -247,6 +247,8 @@ internal class ComposeSceneMediator( scene.compositionLocalContext = value } + val focusManager get() = scene.focusManager + private val renderingView by lazy { renderingUIViewFactory(interopContext, renderDelegate) } From 2f9fddede9721c4eb113502bf8c547499ad87571 Mon Sep 17 00:00:00 2001 From: "Andrei.Salavei" Date: Mon, 15 Apr 2024 13:22:59 +0200 Subject: [PATCH 12/12] Nullify keyboardAnimationListener --- .../ui/window/ComposeSceneKeyboardOffsetManager.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt index a4733aec91f63..28f4271d72bb3 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeSceneKeyboardOffsetManager.kt @@ -174,11 +174,6 @@ internal class ComposeSceneKeyboardOffsetManager( keyboardOverlapHeightState.value = currentOverlapHeight.dp } - fun completeAnimation() { - animationView.removeFromSuperview() - updateAnimationValues(1.0) - } - //attach to root view if needed if (animationView.superview == null) { view.addSubview(animationView) @@ -205,6 +200,15 @@ internal class ComposeSceneKeyboardOffsetManager( selector = sel_registerName("animationDidUpdate") ) keyboardAnimationListener = keyboardDisplayLink + + fun completeAnimation() { + animationView.removeFromSuperview() + if (keyboardAnimationListener == keyboardDisplayLink) { + keyboardAnimationListener = null + } + updateAnimationValues(1.0) + } + UIView.animateWithDuration( duration = duration, delay = 0.0,