Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix IME window insets and view offset when keyboard appears #1199

Merged
merged 16 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ 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.unit.dp
import androidx.compose.ui.uikit.InterfaceOrientation
import androidx.compose.ui.uikit.LocalInterfaceOrientation
import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight

private val ZeroInsets = WindowInsets(0, 0, 0, 0)

Expand Down Expand Up @@ -78,7 +79,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 = LocalKeyboardOverlapHeight.current)

/**
* These insets represent the space where system gestures have priority over application gestures.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -286,27 +287,42 @@ 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 {
// 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
}
}

// 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.
Expand Down Expand Up @@ -338,11 +354,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 }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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
Expand Down
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,6 +61,7 @@ private val RADIUS = 6.dp
*/
private val THICKNESS = 2.dp

@OptIn(InternalComposeApi::class)
MatkovIvan marked this conversation as resolved.
Show resolved Hide resolved
@Composable
internal actual fun SelectionHandle(
offsetProvider: OffsetProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private object SafeAreaInsetsConfig : InsetsConfig {
@Composable get() = LocalSafeArea.current

override val ime: PlatformInsets
@Composable get() = PlatformInsets(bottom = LocalKeyboardOverlapHeight.current.dp)
@Composable get() = PlatformInsets(bottom = LocalKeyboardOverlapHeight.current)

@Composable
override fun excludeInsets(
Expand All @@ -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 else keyboardOverlapHeight,
LocalKeyboardOverlapHeight provides if (ime) 0.dp else keyboardOverlapHeight,
content = content
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ internal class UIKitTextInputService(

textUIView?.removeFromSuperview()
textUIView = IntermediateTextInputUIView(
keyboardEventHandler = keyboardEventHandler,
viewConfiguration = viewConfiguration
).also {
it.keyboardEventHandler = keyboardEventHandler
rootView.addSubview(it)
it.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints(
Expand All @@ -137,8 +137,10 @@ internal class UIKitTextInputService(

textUIView?.inputTraits = EmptyInputTraits
textUIView?.input = null
textUIView?.keyboardEventHandler = null
textUIView?.let { view ->
mainScope.launch {
view.resignFirstResponder()
view.removeFromSuperview()
}
}
Expand Down
Loading