Skip to content

Commit

Permalink
Revamp interop API to align it with Android and make it reusable (#1489)
Browse files Browse the repository at this point in the history
Refactor interop API on iOS and Desktop to make it aligned with
`AndroidView`

- Make an `InteropViewHolder` a type responsible for configuring and
emitting a `LayoutNode` associated with interop view.
- Introduce a `TypedInteropViewHolder` that allows type-bound
user-provided callbacks to run and be correctly updated in the scope of
`InteropViewHolder`.
- Introduce an `InteropView` composable function as an entry point for
all interop implementations.
- Merge `SwingInteropViewHolder` and `SwingInteropViewHolder2` and wire
them with `TypedInteropViewHolder`.
- Delete `InteropViewUpdater` and use interop container resident
`SnapshotObserver` to track updates read by `InteropViewHolder`.
- Change Swing update scheduling mechanics to batch them and then
synchronously execute along with rendering sequence.
- Rewire iOS implementation to use of `InteropView` with
`UIKitInteropViewControllerHolder`, `UIKitInteropViewHolder`, and
`UIKitInteropElementHolder` containing common parts of former two.
- Tie `UIViewController` containment calls with `place`/`unplace`.

## Release notes

### iOS - Breaking changes

- Actual of expected `InteropView` on iOS is `UIResponder` now instead
of `UIView`. It's the first common ancestor for `UIViewController` and
`UIView`, both of which can be integrated using iOS interop APIs.

---------

Co-authored-by: Ivan Matkov <ivan.matkov@jetbrains.com>
  • Loading branch information
2 people authored and mazunin-v-jb committed Aug 14, 2024
1 parent 4ec5441 commit 3b7bd8d
Show file tree
Hide file tree
Showing 23 changed files with 1,374 additions and 737 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.interop.UIKitView
import androidx.compose.ui.interop.UIKitViewController
import androidx.compose.ui.layout.findRootCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import kotlinx.cinterop.ObjCAction
import kotlinx.cinterop.objcPtr
import kotlinx.cinterop.readValue
import platform.CoreGraphics.CGRectMake
import platform.CoreGraphics.CGRectZero
Expand Down Expand Up @@ -71,24 +75,37 @@ private class TouchReactingView: UIView(frame = CGRectZero.readValue()) {

val UIKitInteropExample = Screen.Example("UIKitInterop") {
var text by remember { mutableStateOf("Type something") }
var updatedValue by remember { mutableStateOf(null as Offset?) }

LazyColumn(Modifier.fillMaxSize()) {
item {
UIKitView(
factory = {
MKMapView()
},
modifier = Modifier.fillMaxWidth().height(200.dp)
modifier = Modifier.fillMaxWidth().height(200.dp),
update = {
println("MKMapView updated")
}
)
}

item {
UIKitViewController(
factory = {
object : UIViewController(nibName = null, bundle = null) {
val label = UILabel()

override fun loadView() {
setView(label)
}

override fun viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = UIColor.blueColor
label.textAlignment = NSTextAlignmentCenter
label.textColor = UIColor.whiteColor
label.backgroundColor = UIColor.blueColor
}

override fun viewWillAppear(animated: Boolean) {
Expand Down Expand Up @@ -116,7 +133,20 @@ val UIKitInteropExample = Screen.Example("UIKitInterop") {
}
}
},
modifier = Modifier.fillMaxWidth().height(100.dp),
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.onGloballyPositioned { coordinates ->
val rootCoordinates = coordinates.findRootCoordinates()
val box = coordinates.localBoundingBoxOf(rootCoordinates, clipBounds = false)
updatedValue = box.topLeft
},
update = { viewController ->
updatedValue?.let {
viewController.label.text = "${it.x}, ${it.y}"
}
},
interactive = false
)
}
items(100) { index ->
Expand All @@ -136,7 +166,7 @@ val UIKitInteropExample = Screen.Example("UIKitInterop") {
3 -> ComposeUITextField(text, onValueChange = { text = it }, Modifier.fillMaxWidth().height(40.dp))
4 -> UIKitView(
factory = { TouchReactingView() },
modifier = Modifier.fillMaxWidth().height(40.dp)
modifier = Modifier.fillMaxWidth().height(40.dp),
)
}
}
Expand Down Expand Up @@ -171,6 +201,7 @@ private fun ComposeUITextField(value: String, onValueChange: (String) -> Unit, m
},
modifier = modifier,
update = { textField ->
println("Update called for UITextField(0x${textField.objcPtr().toLong().toString(16)}, value = $value")
textField.text = value
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* 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.awt

import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.viewinterop.InteropViewGroup
import java.awt.event.FocusEvent

internal class InteropFocusSwitcher(
private val group: InteropViewGroup,
private val focusManager: FocusManager,
) {
private val backwardTracker = Tracker {
val component = group.focusTraversalPolicy.getFirstComponent(group)
if (component != null) {
component.requestFocus(FocusEvent.Cause.TRAVERSAL_FORWARD)
} else {
moveForward()
}
}

private val forwardTracker = Tracker {
val component = group.focusTraversalPolicy.getLastComponent(group)
if (component != null) {
component.requestFocus(FocusEvent.Cause.TRAVERSAL_BACKWARD)
} else {
moveBackward()
}
}

val backwardTrackerModifier: Modifier
get() = backwardTracker.modifier

val forwardTrackerModifier: Modifier
get() = forwardTracker.modifier

fun moveBackward() {
backwardTracker.requestFocusWithoutEvent()
focusManager.moveFocus(FocusDirection.Previous)
}

fun moveForward() {
forwardTracker.requestFocusWithoutEvent()
focusManager.moveFocus(FocusDirection.Next)
}

/**
* A helper class that can help:
* - to prevent recursive focus events
* (a case when we focus the same element inside `onFocusEvent`)
* - to prevent triggering `onFocusEvent` while requesting focus somewhere else
*/
private class Tracker(
private val onNonRecursiveFocused: () -> Unit
) {
private val requester = FocusRequester()

private var isRequestingFocus = false
private var isHandlingFocus = false

fun requestFocusWithoutEvent() {
try {
isRequestingFocus = true
requester.requestFocus()
} finally {
isRequestingFocus = false
}
}

val modifier = Modifier
.focusRequester(requester)
.onFocusEvent {
if (!isRequestingFocus && !isHandlingFocus && it.isFocused) {
try {
isHandlingFocus = true
onNonRecursiveFocused()
} finally {
isHandlingFocus = false
}
}
}
.focusTarget()
}
}
Loading

0 comments on commit 3b7bd8d

Please sign in to comment.