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

Support RTL accessibility gestures #1663

Open
wants to merge 3 commits into
base: jb-main
Choose a base branch
from
Open
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 @@ -34,6 +34,7 @@ import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.uikit.utils.CMPAccessibilityContainer
import androidx.compose.ui.uikit.utils.CMPAccessibilityElement
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.viewinterop.InteropWrappingView
import androidx.compose.ui.viewinterop.NativeAccessibilityViewSemanticsKey
Expand Down Expand Up @@ -413,47 +414,51 @@ private class AccessibilityElement(
)
)

fun Float.invertIfNeeded() = if (isRTL) -this else this
// TODO: consider safe areas?
// TODO: is RTL working properly?
if (unclippedRect.top < scrollableAncestorRect.top) {
// The element is above the screen, scroll up
parent?.scrollByIfPossible(
0f,
unclippedRect.top - scrollableAncestorRect.top - scrollableAncestor.size.height / 2
unclippedRect.top - scrollableAncestorRect.top -
(scrollableAncestor.size.height - unclippedRect.size.height) / 2
)
} else if (unclippedRect.bottom > scrollableAncestorRect.bottom) {
// The element is below the screen, scroll down
parent?.scrollByIfPossible(
0f,
unclippedRect.bottom - scrollableAncestorRect.bottom + scrollableAncestor.size.height / 2
unclippedRect.bottom - scrollableAncestorRect.bottom +
(scrollableAncestor.size.height - unclippedRect.size.height) / 2
)
} else if (unclippedRect.left < scrollableAncestorRect.left) {
// The element is to the left of the screen, scroll left
parent?.scrollByIfPossible(
unclippedRect.left - scrollableAncestorRect.left - scrollableAncestor.size.width / 2,
(unclippedRect.left - scrollableAncestorRect.left -
(scrollableAncestor.size.width - unclippedRect.size.width) / 2).invertIfNeeded(),
0f
)
} else if (unclippedRect.right > scrollableAncestorRect.right) {
// The element is to the right of the screen, scroll right
parent?.scrollByIfPossible(
unclippedRect.right - scrollableAncestorRect.right + scrollableAncestor.size.width / 2,
(unclippedRect.right - scrollableAncestorRect.right +
(scrollableAncestor.size.width - unclippedRect.size.width) / 2).invertIfNeeded(),
0f
)
}
}

private fun scrollByIfPossible(dx: Float, dy: Float) {
private fun scrollByIfPossible(dx: Float, dy: Float): Boolean {
if (!isAlive) {
return
return false
}

// if it has scrollBy action, invoke it, otherwise try to scroll the parent
val action = cachedConfig.getOrNull(SemanticsActions.ScrollBy)?.action

if (action != null) {
return if (action != null) {
action(dx, dy)
} else {
parent?.scrollByIfPossible(dx, dy)
parent?.scrollByIfPossible(dx, dy) ?: false
}
}

Expand All @@ -472,18 +477,33 @@ private class AccessibilityElement(
SemanticsProperties.VerticalScrollAxisRange
}

// TODO: is RTL working properly?
val axisRange = config.getOrNull(rangeProperty)
val normalisedDirection = if (axisRange?.reverseScrolling == true) {
when (direction) {
UIAccessibilityScrollDirectionUp -> UIAccessibilityScrollDirectionDown
UIAccessibilityScrollDirectionDown -> UIAccessibilityScrollDirectionUp
UIAccessibilityScrollDirectionRight -> UIAccessibilityScrollDirectionLeft
UIAccessibilityScrollDirectionLeft -> UIAccessibilityScrollDirectionRight
else -> return null
val normalisedDirection = when (direction) {
UIAccessibilityScrollDirectionUp -> if (axisRange?.reverseScrolling == true) {
UIAccessibilityScrollDirectionDown
} else {
UIAccessibilityScrollDirectionUp
}
} else {
direction

UIAccessibilityScrollDirectionDown -> if (axisRange?.reverseScrolling == true) {
UIAccessibilityScrollDirectionUp
} else {
UIAccessibilityScrollDirectionDown
}

UIAccessibilityScrollDirectionRight -> if (isRTL) {
UIAccessibilityScrollDirectionLeft
} else {
UIAccessibilityScrollDirectionRight
}

UIAccessibilityScrollDirectionLeft -> if (isRTL) {
UIAccessibilityScrollDirectionRight
} else {
UIAccessibilityScrollDirectionLeft
}

else -> return null
}

when (normalisedDirection) {
Expand Down Expand Up @@ -822,6 +842,8 @@ private class AccessibilityElement(
}
return this.takeIf { containsPoint }
}

private val isRTL get() = semanticsNode.isRTL
}

/**
Expand Down Expand Up @@ -860,6 +882,7 @@ private class AccessibilityElement(
* https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/SemanticsObject.h
*
*/
@OptIn(BetaInteropApi::class)
@ExportObjCClass
private class AccessibilityContainer(
/**
Expand Down Expand Up @@ -1255,7 +1278,7 @@ internal class AccessibilityMediator(
.filter {
it.isValid
}
.sortedByAccessibilityOrder()
.sortedByAccessibilityOrder(node.isRTL)

for (childNode in childSemanticsNodesInAccessibilityOrder) {
val childElement = traverseSemanticsNode(childNode)
Expand Down Expand Up @@ -1454,19 +1477,21 @@ private fun debugContainmentChain(accessibilityObject: Any): String {
/**
* Sort the elements in their visual order using their bounds:
* - from top to bottom,
* - from left to right // TODO: consider RTL layout
* - from left to right or from right to left, depending on language direction
*
* The sort is needed because [SemanticsNode.replacedChildren] order doesn't match the
* expected order of the children in the accessibility tree.
*
* TODO: investigate if it's a bug, or some assumptions about the order are wrong.
*/
private fun List<SemanticsNode>.sortedByAccessibilityOrder(): List<SemanticsNode> {
private fun List<SemanticsNode>.sortedByAccessibilityOrder(isRTL: Boolean): List<SemanticsNode> {
return sortedWith { lhs, rhs ->
val result = lhs.boundsInWindow.topLeft.y.compareTo(rhs.boundsInWindow.topLeft.y)

if (result == 0) {
lhs.boundsInWindow.topLeft.x.compareTo(rhs.boundsInWindow.topLeft.x)
lhs.boundsInWindow.topLeft.x.compareTo(rhs.boundsInWindow.topLeft.x).let {
if (isRTL) -it else it
}
} else {
result
}
Expand All @@ -1482,6 +1507,9 @@ private val SemanticsNode.unclippedBoundsInWindow: Rect
private val SemanticsNode.isValid: Boolean
get() = layoutNode.isPlaced && layoutNode.isAttached

private val SemanticsNode.isRTL: Boolean
get() = layoutInfo.layoutDirection == LayoutDirection.Rtl

/**
* Closest ancestor that has [SemanticsActions.ScrollBy] action
*/
Expand Down
Loading