From 470a7ae1ff4ab8103ddbceb2bf52f44e82779c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 2 Sep 2026 10:24:26 +0200 Subject: [PATCH 1/3] chore: bump rive-android to 11.10.0 and rive-ios to 6.24.0 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 33fd9148..7372c703 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,8 @@ }, "homepage": "https://github.com/rive-app/rive-nitro-react-native#readme", "runtimeVersions": { - "ios": "6.23.1", - "android": "11.9.1" + "ios": "6.24.0", + "android": "11.10.0" }, "publishConfig": { "registry": "https://registry.npmjs.org/" From 95a74f9f04a6ae53adabc1ddf9b839a8cb5d60e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 2 Sep 2026 10:24:26 +0200 Subject: [PATCH 2/3] feat(android): expose editor-authored semantics to TalkBack --- README.md | 2 +- .../com/margelo/nitro/rive/HybridRiveView.kt | 13 +- .../new/java/com/rive/RiveReactNativeView.kt | 154 +++++++++++++++++- .../com/rive/RiveSemanticsTextureView.java | 57 +++++++ example/android/build.gradle | 10 +- example/src/demos/SemanticsExample.tsx | 14 +- src/specs/RiveView.nitro.ts | 6 +- 7 files changed, 231 insertions(+), 25 deletions(-) create mode 100644 android/src/new/java/com/rive/RiveSemanticsTextureView.java diff --git a/README.md b/README.md index 6aaab68a..e4bfe86f 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ The following table compares feature availability with the [previous Rive React | `useRiveFile()` hook | ✅ | Convenient hook to load a Rive file | | `RiveView` error handling | ✅ | Error handler for failed view operations | | `source` .riv file loading | ✅ | Conveniently load .riv files from JS source | -| Accessibility semantics | ⚠️ | Editor-authored semantics → VoiceOver (iOS; Android in progress) | +| Accessibility semantics | ✅ | Editor-authored semantics → VoiceOver / TalkBack (new runtimes) | | Animation selection | ❌ | Animation playback not planned, use state machines | | Renderer options | ❌ | Single renderer option available (Rive) | diff --git a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt index 4d18af5d..89f3477a 100644 --- a/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt +++ b/android/src/new/java/com/margelo/nitro/rive/HybridRiveView.kt @@ -8,7 +8,9 @@ import com.margelo.nitro.core.Promise import com.rive.BindData import com.rive.RiveReactNativeView import com.rive.ViewConfiguration +import app.rive.ExperimentalRiveSemantics import app.rive.Fit as RiveFit +import app.rive.RiveSemanticsMode import app.rive.Alignment as RiveAlignment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -108,9 +110,16 @@ class HybridRiveView(val context: ThemedReactContext) : HybridRiveViewSpec() { ) } - // Accepted for API parity; semantics support is pending in the upstream - // rive-android runtime (iOS-only for now). + @OptIn(ExperimentalRiveSemantics::class) override var semantics: Semantics? = null + set(value) { + field = value + view.semantics = when (value) { + Semantics.ON -> RiveSemanticsMode.On + Semantics.AUTOMATIC -> RiveSemanticsMode.Automatic + Semantics.OFF, null -> RiveSemanticsMode.Off + } + } override var dataBind: Variant_HybridViewModelInstanceSpec_DataBindMode_DataBindByName? = null set(value) { if (field != value) { diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index 020b96a8..f3dc34c3 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -1,6 +1,9 @@ +@file:OptIn(ExperimentalRiveSemantics::class) + package com.rive import android.annotation.SuppressLint +import android.content.Context import android.graphics.SurfaceTexture import android.os.Build import android.util.Log @@ -8,11 +11,15 @@ import android.view.Choreographer import android.view.MotionEvent import android.view.TextureView import android.view.View +import android.view.accessibility.AccessibilityManager import android.widget.FrameLayout +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import com.facebook.react.bridge.UiThreadUtil import app.rive.Artboard +import app.rive.ExperimentalRiveSemantics import app.rive.Fit import app.rive.RiveFile +import app.rive.RiveSemanticsMode import app.rive.ViewModelInstance import app.rive.ViewModelSource import app.rive.core.ArtboardHandle @@ -20,6 +27,7 @@ import app.rive.core.CommandQueue import app.rive.core.RiveSurface import app.rive.core.StateMachineHandle import app.rive.core.SurfaceTextureSurface +import app.rive.semantics.SemanticActionType import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.rive.RiveErrorLogger import com.margelo.nitro.rive.RiveLog @@ -62,6 +70,22 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { // (e.g. every 4th frame at 120Hz for a 30fps cap) instead of drifting past // it and halving the effective rate. private const val CAP_TOLERANCE_NS = 4_000_000L + + // rive-android's TalkBack bridge compiles against androidx.core 1.17; an + // app that forces an older androidx.core (e.g. to stay on compileSdk 35) + // would otherwise crash with NoSuchMethodError on the first accessibility + // query. Probe one of the 1.17-only methods it calls. + private val semanticsSupported: Boolean by lazy { + runCatching { + AccessibilityNodeInfoCompat::class.java + .getMethod("setSupplementalDescription", CharSequence::class.java) + }.isSuccess + } + + private const val SEMANTICS_UNSUPPORTED_MESSAGE = + "Rive semantics need androidx.core 1.17.0 or newer (compileSdk 36), but the app " + + "resolved an older version; check for a forced androidx.core:core version in " + + "build.gradle. Semantics stay disabled." } // Render at most this many frames per second; null = every vsync. @@ -73,6 +97,28 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { var onError: ((String) -> Unit)? = null + var semantics: RiveSemanticsMode = RiveSemanticsMode.Off + set(value) { + if (field == value) return + field = value + resolveSemantics() + } + + private val accessibilityManager = + context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + private val accessibilityStateListener = + AccessibilityManager.AccessibilityStateChangeListener { resolveSemantics() } + private var observingAccessibilityState = false + private var semanticsEnabled = false + private var warnedSemanticsUnsupported = false + private var semanticsSyncJob: Job? = null + private val mainScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) + + // Semantic input (TalkBack actions, accessibility focus) and viewport changes + // must reach the state machine even while it is paused or settled: the next + // frame advances by zero and drains the resulting diff. + private var semanticFrameRequested = false + private var settledJob: Job? = null // rive-runtime's command server emits a settle signal on every advance @@ -121,7 +167,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { private val viewScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - private val textureView = TextureView(context).apply { + private val textureView = RiveSemanticsTextureView.create(context).apply { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) surfaceTextureListener = object : TextureView.SurfaceTextureListener { override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) { @@ -154,6 +200,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { // RiveSurface.resize() is internal to the SDK, so only the artboard // is resized here (same behavior as before the 11.7.2 bump). resizeArtboardIfLayout() + if (semanticsEnabled) requestSemanticFrame() } override fun onSurfaceTextureUpdated(st: SurfaceTexture) {} @@ -202,8 +249,20 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { if (worker != null && art != null && sm != null && rs != null) { try { - if (!paused && !settled) { - worker.advanceStateMachine(sm, deltaTime) + val advanced = when { + !paused && !settled -> { + worker.advanceStateMachine(sm, deltaTime) + true + } + semanticFrameRequested -> { + worker.advanceStateMachine(sm, Duration.ZERO) + true + } + else -> false + } + semanticFrameRequested = false + if (advanced && semanticsEnabled && surfaceWidth > 0 && surfaceHeight > 0) { + worker.drainSemanticsDiff(sm, activeFit, surfaceWidth.toFloat(), surfaceHeight.toFloat()) } worker.draw(art, sm, rs, activeFit) needsRedraw = false @@ -255,6 +314,13 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { } fun configure(config: ViewConfiguration, dataBindingChanged: Boolean, reload: Boolean = false, initialUpdate: Boolean = false) { + // Reported here rather than from the semantics setter: props apply in + // declaration order, so onError is not wired yet when semantics is set. + if (semantics != RiveSemanticsMode.Off && !semanticsSupported && !warnedSemanticsUnsupported) { + warnedSemanticsUnsupported = true + Log.w(TAG, SEMANTICS_UNSUPPORTED_MESSAGE) + onError?.invoke(SEMANTICS_UNSUPPORTED_MESSAGE) + } riveWorker = config.riveWorker activeFit = config.fit needsRedraw = true @@ -266,6 +332,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { if (reload) { RiveErrorLogger.resetReportedErrors() RiveErrorLogger.addListener(errorListener) + if (semanticsEnabled) detachSemantics() stateMachineHandle?.let { old -> runCatching { config.riveWorker.deleteStateMachine(old) } } @@ -289,6 +356,7 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { } stateMachineHandle = newStateMachineHandle observeSettled(config.riveWorker, newStateMachineHandle) + if (semanticsEnabled) attachSemantics() if (surfaceTexture != null && riveSurface == null) { riveSurface = config.riveWorker.createRiveSurface( @@ -467,6 +535,80 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { } } + private fun resolveSemantics() { + val automatic = semantics == RiveSemanticsMode.Automatic + if (automatic && !observingAccessibilityState) { + accessibilityManager.addAccessibilityStateChangeListener(accessibilityStateListener) + observingAccessibilityState = true + } else if (!automatic && observingAccessibilityState) { + accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateListener) + observingAccessibilityState = false + } + val requested = when (semantics) { + RiveSemanticsMode.Off -> false + RiveSemanticsMode.On -> true + RiveSemanticsMode.Automatic -> accessibilityManager.isEnabled + } + val enabled = requested && semanticsSupported + if (enabled == semanticsEnabled) return + semanticsEnabled = enabled + if (enabled) attachSemantics() else detachSemantics() + } + + // Core never stops producing semantics once enabled for a state machine; + // detaching only stops draining diffs and removes the TalkBack nodes. + private fun attachSemantics() { + val worker = riveWorker ?: return + val sm = stateMachineHandle ?: return + try { + worker.enableSemantics(sm) + } catch (e: Exception) { + Log.e(TAG, "Failed to enable semantics", e) + return + } + val tree = worker.semanticTree(sm) + RiveSemanticsTextureView.install(textureView, tree, object : RiveSemanticsTextureView.Listener { + override fun onSemanticAction(nodeId: Int, action: SemanticActionType) = + semanticInput { worker.fireSemanticAction(sm, nodeId, action) } + + override fun onSemanticFocusRequested(nodeId: Int) = + semanticInput { worker.requestSemanticFocus(sm, nodeId) } + + override fun onSemanticFocusCleared() = + semanticInput { worker.clearSemanticFocus(sm) } + }) + semanticsSyncJob?.cancel() + semanticsSyncJob = mainScope.launch { + tree.versionFlow.collect { RiveSemanticsTextureView.synchronize(textureView) } + } + requestSemanticFrame() + } + + private fun detachSemantics() { + semanticsSyncJob?.cancel() + semanticsSyncJob = null + RiveSemanticsTextureView.clear(textureView) + val worker = riveWorker ?: return + val sm = stateMachineHandle ?: return + runCatching { worker.clearSemanticFocus(sm) } + } + + private fun semanticInput(block: () -> Unit) { + try { + block() + } catch (e: Exception) { + Log.e(TAG, "Semantic input failed", e) + return + } + requestSemanticFrame() + } + + private fun requestSemanticFrame() { + semanticFrameRequested = true + needsRedraw = true + settled = false + } + fun play() { paused = false settled = false @@ -526,6 +668,12 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { viewReadyDeferred.complete(false) settledJob?.cancel() viewScope.cancel() + if (semanticsEnabled) detachSemantics() + if (observingAccessibilityState) { + accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateListener) + observingAccessibilityState = false + } + mainScope.cancel() RiveErrorLogger.removeListener(errorListener) stopRenderLoop() // The command queue is FIFO, so deletes enqueued here run after any diff --git a/android/src/new/java/com/rive/RiveSemanticsTextureView.java b/android/src/new/java/com/rive/RiveSemanticsTextureView.java new file mode 100644 index 00000000..a253c7b9 --- /dev/null +++ b/android/src/new/java/com/rive/RiveSemanticsTextureView.java @@ -0,0 +1,57 @@ +package com.rive; + +import android.content.Context; +import android.view.TextureView; + +import app.rive.RiveTextureView; +import app.rive.semantics.SemanticActionType; +import app.rive.semantics.SemanticTreeModel; + +import kotlin.Unit; + +/** + * Bridges to {@code app.rive.RiveTextureView}, the SDK's TalkBack host for a semantic tree. + * It is Kotlin-internal in rive-android 11.10 (only the Compose entry point uses it), which + * Java is not bound by; drop this shim once the SDK exposes it publicly. + */ +final class RiveSemanticsTextureView { + interface Listener { + void onSemanticAction(int nodeId, SemanticActionType action); + + void onSemanticFocusRequested(int nodeId); + + void onSemanticFocusCleared(); + } + + private RiveSemanticsTextureView() {} + + static TextureView create(Context context) { + return new RiveTextureView(context); + } + + static void install(TextureView view, SemanticTreeModel tree, Listener listener) { + ((RiveTextureView) view).installSemantics( + tree, + (nodeId, action) -> { + listener.onSemanticAction(nodeId, action); + return Unit.INSTANCE; + }, + transition -> Unit.INSTANCE, + nodeId -> { + listener.onSemanticFocusRequested(nodeId); + return Unit.INSTANCE; + }, + () -> { + listener.onSemanticFocusCleared(); + return Unit.INSTANCE; + }); + } + + static boolean synchronize(TextureView view) { + return ((RiveTextureView) view).synchronizeSemantics(); + } + + static void clear(TextureView view) { + ((RiveTextureView) view).clearSemantics(); + } +} diff --git a/example/android/build.gradle b/example/android/build.gradle index 046d1836..c80fdf88 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { buildToolsVersion = "35.0.0" minSdkVersion = 24 - compileSdkVersion = 35 + compileSdkVersion = 36 targetSdkVersion = 35 ndkVersion = "27.1.12297006" kotlinVersion = "2.1.20" @@ -21,11 +21,3 @@ buildscript { apply plugin: "com.facebook.react.rootproject" -allprojects { - configurations.all { - resolutionStrategy { - force 'androidx.core:core:1.15.0' - force 'androidx.core:core-ktx:1.15.0' - } - } -} diff --git a/example/src/demos/SemanticsExample.tsx b/example/src/demos/SemanticsExample.tsx index 89b02daf..f7f0e3fc 100644 --- a/example/src/demos/SemanticsExample.tsx +++ b/example/src/demos/SemanticsExample.tsx @@ -4,12 +4,12 @@ import { RiveView, useRiveFile, Semantics } from '@rive-app/react-native'; import type { Metadata } from '../shared/metadata'; /* - Semantics — editor-authored accessibility exposed to VoiceOver. + Semantics — editor-authored accessibility exposed to VoiceOver / TalkBack. tabtest.riv (from rive-runtime's semantics test assets) authors a tab bar with roles, labels and selection state. With semantics enabled, each tab - becomes an accessibility element: VoiceOver reads them and can activate - them. iOS new (default) backend only; see + becomes an accessibility element: the screen reader reads them and can + activate them. New (default) backends only; see https://rive.app/docs/runtimes/apple/semantics */ @@ -36,8 +36,8 @@ export default function SemanticsExample() { return ( - With semantics On (or Automatic + VoiceOver running), the tabs below are - exposed to VoiceOver as selectable accessibility elements. + With semantics On (or Automatic + a screen reader running), the tabs + below are exposed to VoiceOver / TalkBack as selectable elements. {MODES.map(({ label, value }) => ( @@ -71,9 +71,9 @@ export default function SemanticsExample() { } SemanticsExample.metadata = { - name: 'Semantics (VoiceOver)', + name: 'Semantics (VoiceOver / TalkBack)', description: - 'Editor-authored accessibility semantics exposed to VoiceOver (iOS new backend)', + 'Editor-authored accessibility semantics exposed to the screen reader (new backends)', order: 3, } satisfies Metadata; diff --git a/src/specs/RiveView.nitro.ts b/src/specs/RiveView.nitro.ts index f26af654..d9c37f0a 100644 --- a/src/specs/RiveView.nitro.ts +++ b/src/specs/RiveView.nitro.ts @@ -65,10 +65,10 @@ export interface RiveViewProps extends HybridViewProps { frameRate?: number | FrameRateRange; /** * Exposes accessibility semantics authored in the Rive editor to the - * platform screen reader (VoiceOver). Defaults to Semantics.Off. + * platform screen reader (VoiceOver, TalkBack). Defaults to Semantics.Off. * - * Only supported on the new (default) iOS runtime so far; ignored - * elsewhere. Android support is pending the upstream rive-android runtime. + * Only supported on the new (default) runtimes; the legacy backends + * ignore it. * * @see https://rive.app/docs/runtimes/apple/semantics */ From 11b0b0b7e9db5e0c742d464a9063196449a96680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikl=C3=B3s=20Fazekas?= Date: Wed, 2 Sep 2026 10:36:15 +0200 Subject: [PATCH 3/3] refactor(android): simplify semantics shim and trim comments --- .../new/java/com/rive/RiveReactNativeView.kt | 51 +++++++------------ .../com/rive/RiveSemanticsTextureView.java | 45 +++++++--------- 2 files changed, 37 insertions(+), 59 deletions(-) diff --git a/android/src/new/java/com/rive/RiveReactNativeView.kt b/android/src/new/java/com/rive/RiveReactNativeView.kt index f3dc34c3..b27e00ad 100644 --- a/android/src/new/java/com/rive/RiveReactNativeView.kt +++ b/android/src/new/java/com/rive/RiveReactNativeView.kt @@ -27,7 +27,6 @@ import app.rive.core.CommandQueue import app.rive.core.RiveSurface import app.rive.core.StateMachineHandle import app.rive.core.SurfaceTextureSurface -import app.rive.semantics.SemanticActionType import com.facebook.react.uimanager.ThemedReactContext import com.margelo.nitro.rive.RiveErrorLogger import com.margelo.nitro.rive.RiveLog @@ -71,10 +70,8 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { // it and halving the effective rate. private const val CAP_TOLERANCE_NS = 4_000_000L - // rive-android's TalkBack bridge compiles against androidx.core 1.17; an - // app that forces an older androidx.core (e.g. to stay on compileSdk 35) - // would otherwise crash with NoSuchMethodError on the first accessibility - // query. Probe one of the 1.17-only methods it calls. + // rive-android's TalkBack bridge calls androidx.core 1.17 APIs; an app that + // forces an older androidx.core crashes on the first accessibility query. private val semanticsSupported: Boolean by lazy { runCatching { AccessibilityNodeInfoCompat::class.java @@ -114,9 +111,8 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { private var semanticsSyncJob: Job? = null private val mainScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) - // Semantic input (TalkBack actions, accessibility focus) and viewport changes - // must reach the state machine even while it is paused or settled: the next - // frame advances by zero and drains the resulting diff. + // Semantic input and viewport changes must be applied and drained even + // while paused or settled, which needs one zero-delta advance. private var semanticFrameRequested = false private var settledJob: Job? = null @@ -249,16 +245,10 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { if (worker != null && art != null && sm != null && rs != null) { try { - val advanced = when { - !paused && !settled -> { - worker.advanceStateMachine(sm, deltaTime) - true - } - semanticFrameRequested -> { - worker.advanceStateMachine(sm, Duration.ZERO) - true - } - else -> false + val playing = !paused && !settled + val advanced = playing || semanticFrameRequested + if (advanced) { + worker.advanceStateMachine(sm, if (playing) deltaTime else Duration.ZERO) } semanticFrameRequested = false if (advanced && semanticsEnabled && surfaceWidth > 0 && surfaceHeight > 0) { @@ -314,8 +304,8 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { } fun configure(config: ViewConfiguration, dataBindingChanged: Boolean, reload: Boolean = false, initialUpdate: Boolean = false) { - // Reported here rather than from the semantics setter: props apply in - // declaration order, so onError is not wired yet when semantics is set. + // Not reported from the semantics setter: props apply in declaration + // order, so onError is not wired yet when semantics is set. if (semantics != RiveSemanticsMode.Off && !semanticsSupported && !warnedSemanticsUnsupported) { warnedSemanticsUnsupported = true Log.w(TAG, SEMANTICS_UNSUPPORTED_MESSAGE) @@ -555,8 +545,8 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { if (enabled) attachSemantics() else detachSemantics() } - // Core never stops producing semantics once enabled for a state machine; - // detaching only stops draining diffs and removes the TalkBack nodes. + // Core keeps producing semantics once enabled; detaching only stops + // draining them. private fun attachSemantics() { val worker = riveWorker ?: return val sm = stateMachineHandle ?: return @@ -567,16 +557,13 @@ class RiveReactNativeView(context: ThemedReactContext) : FrameLayout(context) { return } val tree = worker.semanticTree(sm) - RiveSemanticsTextureView.install(textureView, tree, object : RiveSemanticsTextureView.Listener { - override fun onSemanticAction(nodeId: Int, action: SemanticActionType) = - semanticInput { worker.fireSemanticAction(sm, nodeId, action) } - - override fun onSemanticFocusRequested(nodeId: Int) = - semanticInput { worker.requestSemanticFocus(sm, nodeId) } - - override fun onSemanticFocusCleared() = - semanticInput { worker.clearSemanticFocus(sm) } - }) + RiveSemanticsTextureView.install( + textureView, + tree, + { nodeId, action -> semanticInput { worker.fireSemanticAction(sm, nodeId, action) } }, + { nodeId -> semanticInput { worker.requestSemanticFocus(sm, nodeId) } }, + { semanticInput { worker.clearSemanticFocus(sm) } }, + ) semanticsSyncJob?.cancel() semanticsSyncJob = mainScope.launch { tree.versionFlow.collect { RiveSemanticsTextureView.synchronize(textureView) } diff --git a/android/src/new/java/com/rive/RiveSemanticsTextureView.java b/android/src/new/java/com/rive/RiveSemanticsTextureView.java index a253c7b9..0aee4674 100644 --- a/android/src/new/java/com/rive/RiveSemanticsTextureView.java +++ b/android/src/new/java/com/rive/RiveSemanticsTextureView.java @@ -8,43 +8,34 @@ import app.rive.semantics.SemanticTreeModel; import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; /** - * Bridges to {@code app.rive.RiveTextureView}, the SDK's TalkBack host for a semantic tree. - * It is Kotlin-internal in rive-android 11.10 (only the Compose entry point uses it), which - * Java is not bound by; drop this shim once the SDK exposes it publicly. + * Reaches {@code app.rive.RiveTextureView}, the SDK's TalkBack host, which is Kotlin-internal + * in rive-android 11.10 (only its Compose entry point uses it); Java is not bound by that. */ final class RiveSemanticsTextureView { - interface Listener { - void onSemanticAction(int nodeId, SemanticActionType action); - - void onSemanticFocusRequested(int nodeId); - - void onSemanticFocusCleared(); - } - private RiveSemanticsTextureView() {} static TextureView create(Context context) { return new RiveTextureView(context); } - static void install(TextureView view, SemanticTreeModel tree, Listener listener) { - ((RiveTextureView) view).installSemantics( - tree, - (nodeId, action) -> { - listener.onSemanticAction(nodeId, action); - return Unit.INSTANCE; - }, - transition -> Unit.INSTANCE, - nodeId -> { - listener.onSemanticFocusRequested(nodeId); - return Unit.INSTANCE; - }, - () -> { - listener.onSemanticFocusCleared(); - return Unit.INSTANCE; - }); + static void install( + TextureView view, + SemanticTreeModel tree, + Function2 onSemanticAction, + Function1 onSemanticFocusRequested, + Function0 onSemanticFocusCleared) { + ((RiveTextureView) view) + .installSemantics( + tree, + onSemanticAction, + transition -> Unit.INSTANCE, + onSemanticFocusRequested, + onSemanticFocusCleared); } static boolean synchronize(TextureView view) {