Skip to content

Commit eabcbcd

Browse files
authored
Use screenDensity to layout interop views (#3037)
Fixes incorrect scaling and positioning of interop `UIKitView` / `UIKitViewController` when `LocalDensity` is modified Fixes [CMP-9154](https://youtrack.jetbrains.com/issue/CMP-9154) [iOS] UIKitView interop incorrect position with modified density ## Testing Tests added to `LocalDensityTest.kt` ## Release Notes ### Fixes - iOS - Fix incorrect scaling and positioning of interop `UIKitView` / `UIKitViewController` element when `LocalDensity` is modified. This change does not affect scaling of `factory` content: `UIView` / `UIViewController`.
1 parent 910d0b2 commit eabcbcd

2 files changed

Lines changed: 140 additions & 5 deletions

File tree

compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/viewinterop/UIKitInteropElementHolder.ios.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.compose.ui.input.pointer.PointerEvent
2525
import androidx.compose.ui.layout.LayoutCoordinates
2626
import androidx.compose.ui.layout.MeasurePolicy
2727
import androidx.compose.ui.layout.findRootCoordinates
28+
import androidx.compose.ui.uikit.density
2829
import androidx.compose.ui.unit.IntRect
2930
import androidx.compose.ui.unit.toCGRect
3031
import androidx.compose.ui.unit.roundToIntRect
@@ -69,11 +70,9 @@ internal abstract class UIKitInteropElementHolder<T : InteropView>(
6970
* The UIView to be embedded in the wrapping view.
7071
*/
7172
protected abstract val userComponentView: UIView
72-
7373
private var currentUnclippedRect: IntRect? = null
7474
private var currentClippedRect: IntRect? = null
7575
private var currentUserComponentRect: IntRect? = null
76-
7776
private val layout = UIKitInteropElementLayout(group = group, userComponent = userComponentView)
7877
override val measurePolicy: MeasurePolicy get() = layout.measurePolicy
7978

@@ -94,6 +93,7 @@ internal abstract class UIKitInteropElementHolder<T : InteropView>(
9493

9594
override fun layoutAccordingTo(layoutCoordinates: LayoutCoordinates) {
9695
val rootCoordinates = layoutCoordinates.findRootCoordinates()
96+
val screenDensity = container.root.density
9797

9898
val unclippedRect = rootCoordinates
9999
.localBoundingBoxOf(
@@ -116,11 +116,11 @@ internal abstract class UIKitInteropElementHolder<T : InteropView>(
116116
if (clippedRect != currentClippedRect) {
117117
val groupFrame = clippedRect
118118
.toRect()
119-
.toDpRect(density)
119+
.toDpRect(screenDensity)
120120
.toCGRect()
121121
val groupAccessibilityFrame = unclippedRect
122122
.toRect()
123-
.toDpRect(density)
123+
.toDpRect(screenDensity)
124124
.toCGRect()
125125

126126
container.scheduleUpdate {
@@ -147,7 +147,7 @@ internal abstract class UIKitInteropElementHolder<T : InteropView>(
147147
if (userComponentRect != currentUserComponentRect) {
148148
val userComponentCGRect = userComponentRect
149149
.toRect()
150-
.toDpRect(density)
150+
.toDpRect(screenDensity)
151151
.toCGRect()
152152

153153
container.scheduleUpdate {

compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/layers/LocalDensityTest.kt

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,46 @@
1616

1717
package androidx.compose.ui.layers
1818

19+
import androidx.compose.foundation.layout.Box
1920
import androidx.compose.foundation.layout.Column
21+
import androidx.compose.foundation.layout.fillMaxSize
2022
import androidx.compose.foundation.layout.fillMaxWidth
23+
import androidx.compose.foundation.layout.height
24+
import androidx.compose.foundation.layout.padding
25+
import androidx.compose.foundation.layout.size
2126
import androidx.compose.material.Button
2227
import androidx.compose.runtime.CompositionLocalProvider
28+
import androidx.compose.runtime.getValue
29+
import androidx.compose.runtime.mutableFloatStateOf
30+
import androidx.compose.runtime.setValue
31+
import androidx.compose.ui.Alignment
2332
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.layout.boundsInWindow
34+
import androidx.compose.ui.layout.onGloballyPositioned
2435
import androidx.compose.ui.platform.LocalDensity
2536
import androidx.compose.ui.platform.testTag
2637
import androidx.compose.ui.test.findNodeWithTag
2738
import androidx.compose.ui.test.runUIKitInstrumentedTest
39+
import androidx.compose.ui.test.utils.DpRectZero
2840
import androidx.compose.ui.unit.Density
41+
import androidx.compose.ui.unit.DpOffset
42+
import androidx.compose.ui.unit.DpRect
43+
import androidx.compose.ui.unit.DpSize
44+
import androidx.compose.ui.unit.dp
45+
import androidx.compose.ui.unit.toDpRect
46+
import androidx.compose.ui.unit.toDpSize
47+
import androidx.compose.ui.viewinterop.UIKitView
2948
import androidx.compose.ui.window.Dialog
3049
import androidx.compose.ui.window.Popup
3150
import kotlin.test.Test
3251
import kotlin.test.assertEquals
3352
import kotlin.test.assertNotEquals
53+
import kotlinx.cinterop.ExperimentalForeignApi
54+
import kotlinx.cinterop.useContents
55+
import platform.UIKit.UIButton
56+
import platform.UIKit.UIColor
57+
import platform.UIKit.UIControlStateNormal
58+
import platform.UIKit.UIView
3459

3560
class LocalDensityTest {
3661
@Test
@@ -228,4 +253,114 @@ class LocalDensityTest {
228253

229254
assertEquals(targetButtonIndex, tappedButtonIndex)
230255
}
256+
257+
@OptIn(ExperimentalForeignApi::class)
258+
@Test
259+
fun testInteropViewSizeScaledCustomDensity() = runUIKitInstrumentedTest {
260+
val interopSize = DpSize(20.dp, 20.dp)
261+
var densityScale by mutableFloatStateOf(1f)
262+
val interopView = UIView()
263+
264+
setContent {
265+
Box(
266+
modifier = Modifier.fillMaxSize(),
267+
contentAlignment = Alignment.Center
268+
) {
269+
val density = LocalDensity.current.density
270+
CompositionLocalProvider(LocalDensity provides Density(densityScale * density)) {
271+
UIKitView(
272+
factory = { interopView },
273+
modifier = Modifier.size(interopSize)
274+
)
275+
}
276+
}
277+
}
278+
279+
assertEquals(
280+
expected = interopSize,
281+
actual = interopView.frame.useContents { size.toDpSize() }
282+
)
283+
284+
densityScale = 2f
285+
waitForIdle()
286+
287+
assertEquals(
288+
expected = (interopSize * densityScale),
289+
actual = interopView.frame.useContents { size.toDpSize() }
290+
)
291+
}
292+
293+
@OptIn(ExperimentalForeignApi::class)
294+
@Test
295+
fun testInteropViewPositionForCustomDensity() = runUIKitInstrumentedTest {
296+
val interopHeight = 100.dp
297+
val padding = 10.dp
298+
var densityScale by mutableFloatStateOf(1f)
299+
var interopViewRect = DpRectZero()
300+
val interopView = UIButton().also {
301+
it.setTitle("TAP", forState = UIControlStateNormal)
302+
it.backgroundColor = UIColor.redColor
303+
}
304+
305+
setContent {
306+
Box(
307+
modifier = Modifier.fillMaxSize(),
308+
contentAlignment = Alignment.TopStart
309+
) {
310+
val density = LocalDensity.current
311+
CompositionLocalProvider(LocalDensity provides Density(densityScale * density.density)) {
312+
val currentDensity = LocalDensity.current
313+
UIKitView(
314+
factory = { interopView },
315+
modifier = Modifier
316+
.padding(padding)
317+
.fillMaxWidth()
318+
.height(interopHeight)
319+
.onGloballyPositioned { interopViewRect = it.boundsInWindow().toDpRect(currentDensity) }
320+
)
321+
}
322+
}
323+
}
324+
325+
assertEquals(
326+
expected = DpSize(
327+
width = screenSize.width - padding * 2 * densityScale,
328+
height = interopHeight * densityScale
329+
),
330+
actual = interopView.frame.useContents { size.toDpSize() },
331+
)
332+
333+
assertEquals(
334+
expected = DpRect(
335+
origin = DpOffset(padding, padding),
336+
size = DpSize(
337+
width = (screenSize.width - padding.times(2f)).times(1f / densityScale),
338+
height = interopHeight
339+
)
340+
),
341+
actual = interopViewRect,
342+
)
343+
344+
densityScale = 2f
345+
waitForIdle()
346+
347+
assertEquals(
348+
expected = DpSize(
349+
width = screenSize.width - padding * 2 * densityScale,
350+
height = interopHeight * densityScale
351+
),
352+
actual = interopView.frame.useContents { size.toDpSize() },
353+
)
354+
355+
assertEquals(
356+
expected = DpRect(
357+
origin = DpOffset(padding, padding),
358+
size = DpSize(
359+
width = (screenSize.width - padding * 2 * densityScale).times(1f / densityScale),
360+
height = interopHeight
361+
)
362+
),
363+
actual = interopViewRect
364+
)
365+
}
231366
}

0 commit comments

Comments
 (0)