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

Add API to provide container view for platform layers #1689

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,113 @@
/*
* 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.mpp.demo

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.AlertDialog
import androidx.compose.material.TextField
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.UIKitViewController
import androidx.compose.ui.window.ComposeUIViewController
import platform.CoreGraphics.CGRectInset
import platform.UIKit.UIColor
import platform.UIKit.UIView
import platform.UIKit.UIViewController

val CustomLayersContainerExample = Screen.Example("Custom windowContainerView example") {
Box(modifier = Modifier.displayCutoutPadding()) {
UIKitViewController(
factory = ::EmbeddedViewController,
modifier = Modifier.padding(20.dp).fillMaxSize()
)
Text(
text = "Alert should be inside the blue area",
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

private class EmbeddedViewController: UIViewController(nibName = null, bundle = null) {
private val composeController by lazy { EmbeddedComposeViewController(view) }

override fun viewDidLoad() {
super.viewDidLoad()

view.layer.borderWidth = 1.0
view.layer.borderColor = UIColor.blueColor.CGColor

view.addSubview(composeController.view)
}

override fun viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

composeController.view.setFrame(CGRectInset(view.bounds, 30.0, 30.0))
}
}

@OptIn(ExperimentalComposeApi::class)
private fun EmbeddedComposeViewController(view: UIView) = ComposeUIViewController(
configure = {
this.windowContainerView = view
}
) {
MaterialTheme {
val text = rememberTextFieldState()
val showAlert = remember { mutableStateOf(false) }

Column(modifier = Modifier.border(2.dp, Color.Green.copy(alpha = .3f)).fillMaxSize()) {
TextField(text, modifier = Modifier.fillMaxWidth())
Button({ showAlert.value = true }) {
Text("Show Alert")
}

if (showAlert.value) {
AlertDialog(
onDismissRequest = { showAlert.value = false },
confirmButton = {
TextButton({ showAlert.value = false }) {
Text("Submit")
}
},
title = {
Text("Hello Alert")
},
text = {
Text("Alert message")
}
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ val IosSpecificFeatures = Screen.Selection(
AccessibilityLiveRegionExample,
InteropViewAndSemanticsConfigMerge,
InteropExample,
CustomLayersContainerExample,
ReusableMapsExample,
UpdatableInteropPropertiesExample
)
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import kotlin.math.roundToInt
import kotlinx.cinterop.useContents
import platform.UIKit.UIView

private const val LayerFrameKeyPath = "layer.frame"

/**
* Tracking a state of window.
*/
Expand All @@ -45,21 +43,19 @@ internal class PlatformWindowContext {
/**
* A container used for additional layers and as reference for window coordinate space.
*/
private var windowContainer: UIView? = null
var windowContainerView: UIView? = null
set(value) {
field = value
updateWindowContainerSize()
}

var isWindowFocused by _windowInfo::isWindowFocused

fun setWindowContainer(windowContainer: UIView) {
this.windowContainer = windowContainer

updateWindowContainerSize()
}

fun updateWindowContainerSize() {
val windowContainer = windowContainer ?: return
val container = windowContainerView ?: return

val scale = windowContainer.density.density
val size = windowContainer.frame.useContents {
val scale = container.density.density
val size = container.frame.useContents {
IntSize(
width = (size.width * scale).roundToInt(),
height = (size.height * scale).roundToInt()
Expand All @@ -70,7 +66,7 @@ internal class PlatformWindowContext {
}

fun convertLocalToWindowPosition(container: UIView, localPosition: Offset): Offset {
val windowContainer = windowContainer ?: return localPosition
val windowContainer = this.windowContainerView ?: return localPosition
return convertPoint(
point = localPosition,
fromView = container,
Expand All @@ -79,7 +75,7 @@ internal class PlatformWindowContext {
}

fun convertWindowToLocalPosition(container: UIView, positionInWindow: Offset): Offset {
val windowContainer = windowContainer ?: return positionInWindow
val windowContainer = this.windowContainerView ?: return positionInWindow
return convertPoint(
point = positionInWindow,
fromView = windowContainer,
Expand Down Expand Up @@ -125,7 +121,7 @@ internal class PlatformWindowContext {
* Converts the given [boundsInWindow] from the coordinate space of the container window to [toView] space.
*/
fun convertWindowRect(boundsInWindow: Rect, toView: UIView): Rect {
val windowContainer = windowContainer ?: return boundsInWindow
val windowContainer = windowContainerView ?: return boundsInWindow
return convertRect(
rect = boundsInWindow,
fromView = windowContainer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,11 @@ internal class ComposeHostingViewController(
}

private fun onDidMoveToWindow(window: UIWindow?) {
val windowContainer = window ?: return
val windowContainer = configuration.windowContainerView ?: window

updateInterfaceOrientationState()

windowContext.setWindowContainer(windowContainer)
windowContext.windowContainerView = windowContainer
}

private fun updateInterfaceOrientationState() {
Expand Down Expand Up @@ -392,11 +392,12 @@ internal class ComposeHostingViewController(
}

private fun attachLayer(layer: UIKitComposeSceneLayer) {
val window = checkNotNull(view.window) {
"Cannot attach layer if the view is not in the window hierarchy"
val windowContainer = checkNotNull(windowContext.windowContainerView) {
"Cannot attach layer. Either the view is not in the window hierarchy" +
" or the window container view is not provided."
}

layers.attach(window, layer, hasViewAppeared)
layers.attach(windowContainer, layer, hasViewAppeared)
}

private fun detachLayer(layer: UIKitComposeSceneLayer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import androidx.compose.ui.window.GestureEvent
import androidx.compose.ui.window.MetalView
import org.jetbrains.skia.Canvas
import platform.UIKit.UIEvent
import platform.UIKit.UIWindow
import platform.UIKit.UIView

// TODO: add cross-fade orientation transition like in `ComposeHostingViewController`
/**
Expand Down Expand Up @@ -104,7 +104,7 @@ internal class UIKitComposeSceneLayersHolder {
view.removeFromSuperview()
}

fun attach(window: UIWindow, layer: UIKitComposeSceneLayer, hasViewAppeared: Boolean) {
fun attach(containerView: UIView, layer: UIKitComposeSceneLayer, hasViewAppeared: Boolean) {
val isFirstLayer = layers.isEmpty()

layers.add(layer)
Expand All @@ -118,8 +118,8 @@ internal class UIKitComposeSceneLayersHolder {

metalView.setNeedsSynchronousDrawOnNextLayout()

window.embedSubview(view)
window.layoutIfNeeded()
containerView.embedSubview(view)
containerView.layoutIfNeeded()
}

if (hasViewAppeared) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package androidx.compose.ui.uikit

import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.ui.platform.AccessibilitySyncOptions
import androidx.compose.ui.window.ComposeUIViewController
import platform.UIKit.UIStatusBarAnimation
import platform.UIKit.UIStatusBarStyle
import platform.UIKit.UIView
import platform.UIKit.UIViewController

/**
Expand Down Expand Up @@ -66,6 +68,15 @@ class ComposeUIViewControllerConfiguration {
* explanation on how to fix the issue.
*/
var enforceStrictPlistSanityCheck: Boolean = true

/**
* Container view for additional compose layers, used to display full screen overlay interface
* elements such as popups or text selection handlers The container view should bb placed on the
* same UIWindow as the [ComposeUIViewController].
* If not set, the containing UIWindow of the [ComposeUIViewController] is used.
*/
@ExperimentalComposeApi
var windowContainerView: UIView? = null
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, I'm not sure that we really need this. Could you please emphasize reasoning and use cases?

}

/**
Expand Down
Loading