Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -18,6 +18,7 @@ package androidx.compose.ui.platform

import androidx.collection.MutableIntSet
import androidx.compose.runtime.BroadcastFrameClock
import androidx.compose.ui.animation.durationScale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.node.HitTestResult
Expand Down Expand Up @@ -78,6 +79,7 @@ import kotlinx.cinterop.readValue
import kotlinx.cinterop.useContents
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
Expand Down Expand Up @@ -1253,7 +1255,8 @@ internal class AccessibilityMediator(
// Will exit on CancellationException from within await on `invalidationChannel.receive()`
// when [job] is cancelled
while (true) {
hasPendingInvalidations = false
@OptIn(ExperimentalCoroutinesApi::class)
hasPendingInvalidations = !invalidationChannel.isEmpty
invalidationChannel.receive()
hasPendingInvalidations = true

Expand All @@ -1269,24 +1272,24 @@ internal class AccessibilityMediator(
sync()
}
accessibilityDebugLogger?.log("AccessibilityMediator.sync took $time")

if (keyboardFocusedElementKey != null) {
// Do nothing.
// When full keyboard access is enabled, the selection rectangle can be updated
// on every frame. To improve the user experience, we should update the
// accessibility tree as quickly as possible.
} else {
// Estimated delay between the iOS Accessibility Engine sync intervals.
// There is no reason to post change notifications more frequently because the
// iOS Accessibility Engine will ignore them.
delay((100.0 * coroutineContext.durationScale()).milliseconds)
}
}
} else if (root.element != null) {
refocusKeyboardElementIfNeeded()
root.element = null
AccessibilityNotification(UIAccessibilityLayoutChangedNotification).postNotification()
}

if (keyboardFocusedElementKey != null) {
// Do nothing.
// When full keyboard access is enabled, the selection rectangle can be updated
// on every frame. To improve the user experience, we should update the
// accessibility tree as quickly as possible.
} else {
// Estimated delay between the iOS Accessibility Engine sync intervals.
// There is no reason to post change notifications more frequently because the
// iOS Accessibility Engine will ignore them.
delay(100.milliseconds)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ internal class ComposeContainer(
}

fun hasInvalidations(): Boolean {
return mediator?.hasInvalidations == true || layersHolder?.layersViewController?.hasInvalidations == true
return mediator?.hasInvalidations == true ||
layersHolder?.layersViewController?.hasInvalidations == true ||
focusedViewsList.hasScheduledTasks
}

private val currentInterfaceOrientation: InterfaceOrientation?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@ internal class ComposeSceneMediator(
keyboardManager.hasPendingWork ||
isLayoutTransitionAnimating ||
semanticsOwnerListener.hasInvalidations ||
textInputService.hasInvalidations
textInputService.hasInvalidations ||
interopContainer.hasPendingUpdates
}

init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,12 @@ internal class InteropMutableTransaction(
/**
* Schedules a user-provided `UIKitView.update` or `UIKitViewController.update` callback.
*/
fun scheduleViewUpdate(holder: InteropViewHolder) {
fun scheduleViewUpdate(holder: InteropViewHolder, completion: () -> Unit) {
if (holdersWithPendingViewUpdates.add(holder)) {
actions.add { holder.update() }
actions.add {
holder.update()
completion()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ internal class IosInteropContainer(

private val interopViews = mutableMapOf<InteropView, InteropViewHolder>()
private var transaction = InteropMutableTransaction(isInteropActive = false)
private var scheduledUpdatesCount = 0

val hasInteropViews: Boolean get() = interopViews.isNotEmpty()
val hasPendingUpdates: Boolean get() = transaction.hasPendingActions || scheduledUpdatesCount > 0
val hasPendingViewUpdatesOnly: Boolean get() = transaction.hasPendingViewUpdatesOnly

// TODO: Android reuses `owner.snapshotObserver`. We should probably do the same with RootNodeOwner.
Expand Down Expand Up @@ -140,14 +142,21 @@ internal class IosInteropContainer(
}

override fun scheduleUpdate(action: () -> Unit) {
scheduledUpdatesCount++
// Add lambda to a list of commands which will be executed later
// in the same [CATransaction], when the next rendered Compose frame is presented.
transaction.scheduleFrameSynchronizedAction(action)
transaction.scheduleFrameSynchronizedAction {
action()
scheduledUpdatesCount--
}
requestRedraw()
}

override fun scheduleUpdate(holder: InteropViewHolder) {
transaction.scheduleViewUpdate(holder)
scheduledUpdatesCount++
transaction.scheduleViewUpdate(holder) {
scheduledUpdatesCount--
}
requestRedraw()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package androidx.compose.ui.window
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachReversed
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand All @@ -38,6 +39,12 @@ internal class FocusedViewsList {
private var parent: FocusedViewsList? = null
private val children = mutableListOf<FocusedViewsList>()

private var scheduledTasksCount = 0

val hasScheduledTasks: Boolean get() {
return scheduledTasksCount > 0 || children.any { it.hasScheduledTasks }
}

/**
* Add new view to list and focus on it.
*/
Expand Down Expand Up @@ -87,27 +94,39 @@ internal class FocusedViewsList {

resignedViews += activeViews
activeViews = emptyList()
scheduledTasksCount++
mainScope.launch {
resignScheduledViews()
scheduledTasksCount--
}
}

private fun onListHierarchyChanged(delay: Duration?) {
fun refocusOnLastViewInHierarchy() {
val viewToFocus = lastViewToFocus()
if (viewToFocus != null) {
scheduledTasksCount++
viewToFocus.becomeFirstResponder()
viewToFocus.window?.makeKeyWindow()
mainScope.launch {
scheduledTasksCount--
}
} else {
resignScheduledViews()
}
}
if (delay == null) {
refocusOnLastViewInHierarchy()
} else {
scheduledTasksCount++
mainScope.launch {
delay(delay)
refocusOnLastViewInHierarchy()

// With lots of show/hide keyboard requests, iOS postpones next operations for a while,
// which makes some tests fail. Adding a small delay to fix this issue.
delay(50.milliseconds)
scheduledTasksCount--
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package androidx.compose.ui.animation

import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.MotionDurationScale
import kotlin.coroutines.CoroutineContext
import kotlin.math.min
import kotlin.time.Duration
import kotlin.time.Duration.Companion.nanoseconds
Expand Down Expand Up @@ -53,3 +55,7 @@ internal suspend fun withAnimationProgress(
}
}
}

internal fun CoroutineContext.durationScale(): Float {
return this[MotionDurationScale]?.scaleFactor ?: 1f
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.ComposeUiFlags
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.MotionDurationScale
import androidx.compose.ui.animation.durationScale
import androidx.compose.ui.animation.easeOutTimingFunction
import androidx.compose.ui.animation.withAnimationProgress
import androidx.compose.ui.draw.drawBehind
Expand Down Expand Up @@ -64,7 +64,6 @@ import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.center
import androidx.navigationevent.compose.LocalNavigationEventDispatcherOwner
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.currentCoroutineContext
Expand Down Expand Up @@ -416,7 +415,3 @@ internal fun getDialogScrimBlendMode(isWindowTransparent: Boolean) =
} else {
BlendMode.SrcOver
}

private fun CoroutineContext.durationScale(): Float {
return this[MotionDurationScale]?.scaleFactor ?: 1f
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class ComposeSceneMediatorTest {
tap(screenSize.center)

waitForIdle()
// Should not crash
}

@OptIn(ExperimentalForeignApi::class)
Expand All @@ -45,5 +46,6 @@ class ComposeSceneMediatorTest {
viewController.view.layoutIfNeeded()

waitForIdle()
// Should not crash
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import androidx.compose.ui.test.assertVisibleInContainer
import androidx.compose.ui.test.findNodeWithLabel
import androidx.compose.ui.test.findNodeWithLabelOrNull
import androidx.compose.ui.test.findNodeWithTag
import androidx.compose.ui.test.isContextMenuVisible
import androidx.compose.ui.test.runUIKitInstrumentedTest
import androidx.compose.ui.test.tapContextMenuButton
import androidx.compose.ui.test.utils.BasicTextFieldType
Expand Down Expand Up @@ -751,13 +752,16 @@ class TextFieldEditMenuTest {

private fun UIKitInstrumentedTest.longPressNodeWithTagAndAwaitContextMenu(textFieldTag: String) {
val touch = findNodeWithTag(textFieldTag).touchDown()
waitUntil {
findFirstDescendant { it.isLoupeView } != null
waitUntil("Awaiting context menu or loupe") {
isContextMenuVisible || isLoupeVisible
}
touch.up()
waitForContextMenu()
}

private val UIKitInstrumentedTest.isLoupeVisible: Boolean get() =
findFirstDescendant { it.isLoupeView } != null

private fun UIKitInstrumentedTest.setTextFieldContent(
textFieldKind: BasicTextFieldType,
initialValue: TextFieldValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ internal abstract class KeyboardInsetsTest(
var focusManager: FocusManager? = null
val focusRequester = FocusRequester()

animationSpeed = UIKitInstrumentedTest.DefaultAnimationSpeed

setContent({
onFocusBehavior = OnFocusBehavior.FocusableAboveKeyboard
}) {
Expand Down Expand Up @@ -188,6 +190,8 @@ internal abstract class KeyboardInsetsTest(
var focusManager: FocusManager? = null
val focusRequester = FocusRequester()

animationSpeed = UIKitInstrumentedTest.DefaultAnimationSpeed

setContent({
onFocusBehavior = OnFocusBehavior.DoNothing
}) {
Expand Down Expand Up @@ -590,6 +594,8 @@ internal abstract class KeyboardInsetsTest(
val drawnTextFieldFrames = mutableListOf<Pair<Int, Int>>()
val focusRequester = FocusRequester()

animationSpeed = UIKitInstrumentedTest.DefaultAnimationSpeed

setContent({
onFocusBehavior = OnFocusBehavior.FocusableAboveKeyboard
}) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.preferredFrameRate
import androidx.compose.ui.test.UIKitInstrumentedTest
import androidx.compose.ui.test.findNodeWithTag
import androidx.compose.ui.test.runUIKitInstrumentedTest
import kotlin.test.Test
Expand All @@ -43,6 +44,8 @@ internal class FrameRateTest {
fun testLowFrameRates() = runUIKitInstrumentedTest {
val frameRates = listOf(5f, 10f, 30f, 60f)

animationSpeed = UIKitInstrumentedTest.DefaultAnimationSpeed

setContent {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(frameRates.size) { index ->
Expand All @@ -67,6 +70,8 @@ internal class FrameRateTest {
fun testPreferredFrameRates() = runUIKitInstrumentedTest {
val frameRates = listOf(5f, 10f, 30f, 60f, 80f, 120f)

animationSpeed = UIKitInstrumentedTest.DefaultAnimationSpeed

setContent {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(frameRates.size) { index ->
Expand Down
Loading
Loading