Skip to content

RivePointerInputMode.PassThrough — clickables underneath Rive's layout bounds don't fire onClick #454

@sebastian-gallese-speak

Description

Summary

With Rive() in PassThrough mode as an overlay above sibling composables, any Button / IconButton whose position overlaps Rive's layout bounds doesn't receive onClick, even though the kdoc says PassThrough is the right mode for "an overlay with transparent sections that should allow pointer events through."

Versions

  • rive-android 11.4.1, androidx.compose.ui 1.10.5
  • Samsung Galaxy S25 (SM-S931U1)

Repro

Box(Modifier.fillMaxSize()) {
    // Underlay with two buttons at different positions
    Column(Modifier.fillMaxSize()) {
        Button(
            onClick = { Log.d("test", "top fired") },
            modifier = Modifier.align(Alignment.Start),
        ) { Text("Top button") }

        Spacer(Modifier.weight(1f))

        Button(
            onClick = { Log.d("test", "bottom fired") },
        ) { Text("Bottom button") }
    }

    // Rive overlay with bounds only covering the bottom half
    Rive(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight(0.5f)
            .align(Alignment.BottomCenter),
        pointerInputMode = RivePointerInputMode.PassThrough,
        // ...
    )
}
  • Top button (outside Rive's bounds): fires ✓
  • Bottom button (inside Rive's bounds): does NOT fire

Per the PassThrough kdoc and Compose's PointerInputFilter.shareWithSiblings kdoc, the bottom button should fire. It doesn't.

Additional diagnostic: applying graphicsLayer { scaleX = 0.5f; scaleY = 0.5f; clip = true; shape = CircleShape } to Rive's modifier does shrink the failing region, but it follows the scaled rectangular layout bounds — not the CircleShape. Useful for pinpointing the problem but doesn't rescue the overlay use case.

Suggested fix

Add a mode that skips the PointerInputFilter entirely — the Compose equivalent of .allowsHitTesting(false) that rive-ios's own DrillLessonMascotView.swift uses:

enum class RivePointerInputMode { Consume, Observe, PassThrough, None }

// In Rive():
val finalModifier = when (pointerInputMode) {
    RivePointerInputMode.None -> modifier
    else -> modifier.then(passThroughInputModifier)
}

With None, Rive is a pure drawing layer — Compose hit-testing walks normally and sibling-underneath clickables fire. Matches the iOS pattern.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions