Commit 1e8e182
fix(hit-testing): ignore views with non-invertible transforms (scale 0) (#56586)
Summary:
Fixes #50797. A view with a non-invertible transform (e.g. `transform: [{scaleY: 0}]` or `{scaleX: 0}`) still received touches on both Android and iOS. On Android the view appeared to "inherit" a hit region from another view in the hierarchy; on iOS the view registered taps along the collapsed axis. Both symptoms collapse to the same native behaviour: when a transform can't be inverted, the platform APIs silently fall back to stale or degenerate data during hit testing.
### Root cause
**Android.** `TouchTargetHelper.getChildPoint` calls `Matrix.invert(inverseMatrix)` without checking its return value. `Matrix.invert` returns `false` for a non-invertible matrix and **leaves its destination unchanged**. `inverseMatrix` is a class-level field reused across every child-point conversion, so on failure we apply *the previous view's* inverse to the current view's touch point: the exact "inherits from another view" behavior. It is also why shrinking a view from `scaleY: 0.9` to `0.0` leaves a 90 % hit region: the 0.9 frame populated the cache.
**iOS.** `CGAffineTransformInvert` is documented to return the original matrix when it can't invert. `-[UIView convertPoint:fromView:]` uses that "inverse" during hit testing, so the touch point gets collapsed onto the degenerate axis (e.g. y = 0 for `scaleY: 0`) and the inherited `pointInside:` still reports a hit against a visually invisible view.
### Fix
- **Android** (`TouchTargetHelper.kt`): `getChildPoint` now returns `Boolean`. It returns `false` when `Matrix.invert` fails; the child iteration loop skips such children. One behaviour change, no epsilon tuning, no shared state leaks. Resolves both symptom variants via the same code path.
- **iOS** (`RCTViewComponentView.mm`, Fabric): add a small `RCTLayerTransformCollapsesAxis` helper that tests the determinant of the 2 × 2 XY projection of `layer.transform` and return `NO` from the existing `pointInside:` override when it collapses.
- **Tests.** `TouchTargetHelperTest` (new, Robolectric): initial `scaleX`/`scaleY: 0`, zero-scale parent, the "inherits from sibling" regression, the 0.9 to 0.0 transition, and the touch-path accumulator. `RCTViewComponentViewTests` gets four parallel iOS cases. All pass locally.
- **RNTester.** New Transforms entry "Zero-scale hit test (regression for #50797)". Reproduces both variants and exposes a scale input so you can step through the transition interactively.
### Why not #53769?
PR #53769 solved the Android side with a `hasZeroScale(view)` early-return plus an epsilon comparison on individual matrix entries, and handled the `invert` failure in `getChildPoint` by *falling back to untransformed coordinates*. That still lets the degenerate view be hit with its pre-transform bounds; the early-return then has to catch the miss. This branch fixes the root cause once: if the matrix can't be inverted we don't descend into the child. The iOS fix is the matching change on the Fabric side.
## Changelog:
[GENERAL] [FIXED] - Views with a non-invertible transform (e.g. `scaleX: 0` or `scaleY: 0`) no longer receive touches on Android or iOS.
Pull Request resolved: #56586
Test Plan:
### Unit tests
- `./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests 'com.facebook.react.uimanager.TouchTargetHelperTest*'`: 7/7 pass.
- iOS build-for-testing of `RNTesterUnitTests` scheme succeeds; the four new `RCTViewComponentViewTests` cases live next to the existing cases in `React/Tests/Mounting/`.
- `./gradlew ktfmtCheck`, `clang-format --dry-run -Werror`, `prettier --check`, `flow`, and `eslint` all pass.
### Manual verification (Android)
RNTester → Components → Transforms → "Zero-scale hit test (regression for #50797)".
**Variant 1: tap the `scaleY: 0` row directly.** "Last tapped:" must stay `(none)`.
| Before | After |
| --- | --- |
| <img src="https://raw.githubusercontent.com/qflen/react-native/6f3799dbb597630a91f43e460437d636796dbbe7/assets/50797/before-tap-zero-scale.png" width="260"> | <img src="https://raw.githubusercontent.com/qflen/react-native/6f3799dbb597630a91f43e460437d636796dbbe7/assets/50797/after-tap-zero-scale.png" width="260"> |
| `Last tapped: zero-scale` (bug) | `Last tapped: (none)` |
**Variant 2: tap the `scaleY: 0.5` row (①), then tap the `scaleY: 0` row (②).** Without the fix, the shared `inverseMatrix` cache populated by tap ① leaks into the zero-scale hit test on tap ②. After the fix, tap ② is correctly ignored and "Last tapped" still reflects tap ①.
| Before | After |
| --- | --- |
| <img src="https://raw.githubusercontent.com/qflen/react-native/6f3799dbb597630a91f43e460437d636796dbbe7/assets/50797/before-tap-zero-after-05.png" width="260"> | <img src="https://raw.githubusercontent.com/qflen/react-native/6f3799dbb597630a91f43e460437d636796dbbe7/assets/50797/after-tap-zero-after-05.png" width="260"> |
| `Last tapped: zero-scale` (stale cache) | `Last tapped: variable (scaleY=0.5)` |
iOS behaves similarly. I tried the example on iPhone 17 Pro (Simulator, iOS 26.1) and the zero-scale pressable is correctly untouchable; driving synthetic taps on the simulator wasn't scripted here, so the iOS signal is the four XCTest cases plus manual verification.
Reviewed By: fabriziocucci
Differential Revision: D102590386
Pulled By: javache
fbshipit-source-id: 863a653f17db72ff85135fd8eb344ccd1528eda71 parent 0c153e2 commit 1e8e182
5 files changed
Lines changed: 330 additions & 5 deletions
File tree
- packages
- react-native
- ReactAndroid/src
- main/java/com/facebook/react/uimanager
- test/java/com/facebook/react/uimanager
- React
- Fabric/Mounting/ComponentViews/View
- Tests/Mounting
- rn-tester/js/examples/Transform
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
100 | 114 | | |
101 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
102 | 119 | | |
103 | 120 | | |
104 | 121 | | |
| |||
Lines changed: 41 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
142 | 142 | | |
143 | 143 | | |
144 | 144 | | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
145 | 186 | | |
packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/TouchTargetHelper.kt
Lines changed: 18 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
215 | | - | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
216 | 220 | | |
217 | 221 | | |
218 | 222 | | |
| |||
277 | 281 | | |
278 | 282 | | |
279 | 283 | | |
280 | | - | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
281 | 291 | | |
282 | 292 | | |
283 | 293 | | |
284 | 294 | | |
285 | 295 | | |
286 | 296 | | |
287 | 297 | | |
288 | | - | |
| 298 | + | |
289 | 299 | | |
290 | 300 | | |
291 | 301 | | |
292 | 302 | | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
293 | 307 | | |
294 | 308 | | |
295 | 309 | | |
296 | | - | |
297 | | - | |
298 | 310 | | |
299 | 311 | | |
300 | 312 | | |
301 | 313 | | |
302 | 314 | | |
| 315 | + | |
303 | 316 | | |
304 | 317 | | |
305 | 318 | | |
| |||
Lines changed: 169 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
0 commit comments