react-native-smooth-clip-view 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +86 -21
  2. package/android/CMakeLists.txt +7 -0
  3. package/android/build.gradle +14 -2
  4. package/android/proguard-rules.pro +3 -3
  5. package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
  6. package/android/src/main/cpp/SmoothClipBindings.cpp +896 -6
  7. package/android/src/main/cpp/SmoothClipRegistry.cpp +1067 -46
  8. package/android/src/main/cpp/SmoothClipTrace.h +25 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +385 -24
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
  14. package/cpp/SmoothClipAnimationCurve.h +151 -122
  15. package/cpp/SmoothClipRegistry.h +102 -4
  16. package/cpp/SmoothClipSharedGeometry.h +146 -7
  17. package/cpp/SmoothClipVelocityTracker.h +13 -9
  18. package/ios/SmoothClipGeometry.h +184 -6
  19. package/ios/SmoothClipRegistry.mm +1033 -25
  20. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  21. package/ios/SmoothClipTurboModule.cpp +857 -2
  22. package/ios/SmoothClipTurboModule.h +310 -0
  23. package/ios/SmoothClipView.mm +714 -183
  24. package/ios/SmoothClipViewRegistry.h +1 -0
  25. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  26. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  27. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  28. package/lib/module/SmoothClipView.ios.js +22 -6
  29. package/lib/module/SmoothClipView.ios.js.map +1 -1
  30. package/lib/module/SmoothClipView.js +1 -46
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
  33. package/lib/module/capabilities.js +4 -0
  34. package/lib/module/capabilities.js.map +1 -0
  35. package/lib/module/capabilities.native.js +36 -0
  36. package/lib/module/capabilities.native.js.map +1 -0
  37. package/lib/module/capabilityTypes.js +11 -0
  38. package/lib/module/capabilityTypes.js.map +1 -0
  39. package/lib/module/driverState.js +0 -28
  40. package/lib/module/driverState.js.map +1 -1
  41. package/lib/module/drivers.js +1 -281
  42. package/lib/module/drivers.js.map +1 -1
  43. package/lib/module/drivers.native.js +340 -75
  44. package/lib/module/drivers.native.js.map +1 -1
  45. package/lib/module/geometry.js +147 -17
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/groupDriverTypes.js +4 -0
  48. package/lib/module/groupDriverTypes.js.map +1 -0
  49. package/lib/module/groupDrivers.android.js +4 -0
  50. package/lib/module/groupDrivers.android.js.map +1 -0
  51. package/lib/module/groupDrivers.ios.js +4 -0
  52. package/lib/module/groupDrivers.ios.js.map +1 -0
  53. package/lib/module/groupDrivers.js +4 -0
  54. package/lib/module/groupDrivers.js.map +1 -0
  55. package/lib/module/groupDrivers.native.js +799 -0
  56. package/lib/module/groupDrivers.native.js.map +1 -0
  57. package/lib/module/index.js +3 -2
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/presentationProtocol.js +15 -0
  60. package/lib/module/presentationProtocol.js.map +1 -0
  61. package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
  62. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  63. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  67. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
  68. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  69. package/lib/typescript/src/capabilities.d.ts +3 -0
  70. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  71. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  72. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  73. package/lib/typescript/src/capabilityTypes.d.ts +10 -0
  74. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/driverState.d.ts +1 -6
  76. package/lib/typescript/src/driverState.d.ts.map +1 -1
  77. package/lib/typescript/src/driverTypes.d.ts +39 -11
  78. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  79. package/lib/typescript/src/drivers.d.ts +2 -4
  80. package/lib/typescript/src/drivers.d.ts.map +1 -1
  81. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  82. package/lib/typescript/src/geometry.d.ts +36 -2
  83. package/lib/typescript/src/geometry.d.ts.map +1 -1
  84. package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
  85. package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
  86. package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
  87. package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
  88. package/lib/typescript/src/groupDrivers.d.ts +2 -0
  89. package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
  90. package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
  91. package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
  92. package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
  93. package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
  94. package/lib/typescript/src/index.d.ts +6 -3
  95. package/lib/typescript/src/index.d.ts.map +1 -1
  96. package/lib/typescript/src/presentationProtocol.d.ts +4 -0
  97. package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
  98. package/package.json +10 -11
  99. package/src/NativeSmoothClipModule.ts +275 -0
  100. package/src/SmoothClipView.ios.tsx +30 -11
  101. package/src/SmoothClipView.tsx +2 -76
  102. package/src/SmoothClipViewNativeComponent.ts +27 -1
  103. package/src/capabilities.native.ts +69 -0
  104. package/src/capabilities.ts +2 -0
  105. package/src/capabilityTypes.ts +17 -0
  106. package/src/driverState.ts +1 -41
  107. package/src/driverTypes.ts +56 -11
  108. package/src/drivers.native.ts +921 -151
  109. package/src/drivers.ts +2 -423
  110. package/src/geometry.ts +231 -27
  111. package/src/groupDriverTypes.ts +117 -0
  112. package/src/groupDrivers.android.ts +1 -0
  113. package/src/groupDrivers.ios.ts +1 -0
  114. package/src/groupDrivers.native.ts +1189 -0
  115. package/src/groupDrivers.ts +1 -0
  116. package/src/index.ts +30 -2
  117. package/src/presentationProtocol.ts +28 -0
  118. package/lib/module/SmoothClipView.web.js +0 -54
  119. package/lib/module/SmoothClipView.web.js.map +0 -1
  120. package/lib/module/index.web.js +0 -8
  121. package/lib/module/index.web.js.map +0 -1
  122. package/lib/module/webClipPath.js +0 -25
  123. package/lib/module/webClipPath.js.map +0 -1
  124. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  125. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  126. package/lib/typescript/src/index.web.d.ts +0 -9
  127. package/lib/typescript/src/index.web.d.ts.map +0 -1
  128. package/lib/typescript/src/webClipPath.d.ts +0 -7
  129. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  130. package/src/SmoothClipView.web.tsx +0 -66
  131. package/src/index.web.ts +0 -23
  132. package/src/webClipPath.ts +0 -30
@@ -2,6 +2,8 @@ package com.smoothclipview
2
2
 
3
3
  import android.content.res.Configuration
4
4
  import android.graphics.Outline
5
+ import android.graphics.Path
6
+ import android.os.Trace
5
7
  import android.view.MotionEvent
6
8
  import android.view.View
7
9
  import android.view.ViewOutlineProvider
@@ -17,13 +19,28 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
17
19
  private var requestedWidth = 0f
18
20
  private var requestedHeight = 0f
19
21
  private var requestedRadius = 0f
22
+ private var requestedTopLeftRadius = 0f
23
+ private var requestedTopRightRadius = 0f
24
+ private var requestedBottomRightRadius = 0f
25
+ private var requestedBottomLeftRadius = 0f
26
+ private var requestedCurveCode = CLIP_CURVE_CIRCULAR
20
27
  private var requestedContentTranslateX = 0f
21
28
  private var requestedContentTranslateY = 0f
29
+ private var requestedContentScale = 1f
30
+ private var requestedUsesV2Geometry = false
22
31
  private var clipLeft = 0f
23
32
  private var clipTop = 0f
24
33
  private var clipRight = 0f
25
34
  private var clipBottom = 0f
26
35
  private var clipRadius = 0f
36
+ private var clipTopLeftRadius = 0f
37
+ private var clipTopRightRadius = 0f
38
+ private var clipBottomRightRadius = 0f
39
+ private var clipBottomLeftRadius = 0f
40
+ private var clipCurveCode = CLIP_CURVE_CIRCULAR
41
+ private val clipPath = Path()
42
+ private var outlineUsesPath = false
43
+ private var outlineUsesFloatRoundRect = false
27
44
  // Rounded edges actually emitted to the outline provider. Sub-pixel changes
28
45
  // that round to the same integers must not invalidate the outline.
29
46
  private var outlineLeft = 0
@@ -56,13 +73,23 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
56
73
  return
57
74
  }
58
75
 
59
- outline.setRoundRect(
60
- outlineLeft,
61
- outlineTop,
62
- outlineRight,
63
- outlineBottom,
64
- clipRadius,
65
- )
76
+ if (outlineUsesPath) {
77
+ outline.setPath(clipPath)
78
+ } else if (outlineUsesFloatRoundRect) {
79
+ // Android's public Outline.setRoundRect overload accepts only
80
+ // integer edges. Path.addRoundRect is the platform float
81
+ // round-rect primitive; Outline.setPath preserves those edges
82
+ // without falling back to V1's integer/residual placement.
83
+ outline.setPath(clipPath)
84
+ } else {
85
+ outline.setRoundRect(
86
+ outlineLeft,
87
+ outlineTop,
88
+ outlineRight,
89
+ outlineBottom,
90
+ clipRadius,
91
+ )
92
+ }
66
93
  }
67
94
  }
68
95
 
@@ -118,8 +145,80 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
118
145
  requestedWidth = nextWidth
119
146
  requestedHeight = nextHeight
120
147
  requestedRadius = nextRadius
148
+ requestedTopLeftRadius = nextRadius
149
+ requestedTopRightRadius = nextRadius
150
+ requestedBottomRightRadius = nextRadius
151
+ requestedBottomLeftRadius = nextRadius
152
+ requestedCurveCode = CLIP_CURVE_CIRCULAR
121
153
  requestedContentTranslateX = nextContentTranslateX
122
154
  requestedContentTranslateY = nextContentTranslateY
155
+ requestedContentScale = 1f
156
+ requestedUsesV2Geometry = false
157
+ applyRequestedGeometry()
158
+ }
159
+
160
+ /** V2 DIP fallback used until a registered view has pushed host metrics. */
161
+ @DoNotStrip
162
+ fun setClipPresentationV2Dip(
163
+ x: Double,
164
+ y: Double,
165
+ width: Double,
166
+ height: Double,
167
+ topLeftRadius: Double,
168
+ topRightRadius: Double,
169
+ bottomRightRadius: Double,
170
+ bottomLeftRadius: Double,
171
+ curveCode: Int,
172
+ contentTranslateX: Double,
173
+ contentTranslateY: Double,
174
+ contentScale: Double,
175
+ ) {
176
+ if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
177
+ !height.isFinite() || !topLeftRadius.isFinite() ||
178
+ !topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
179
+ !bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
180
+ !contentTranslateY.isFinite() || !contentScale.isFinite() ||
181
+ contentScale <= 0.0 ||
182
+ (curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
183
+ ) {
184
+ return
185
+ }
186
+
187
+ val nextX = PixelUtil.toPixelFromDIP(x).toFloat()
188
+ val nextY = PixelUtil.toPixelFromDIP(y).toFloat()
189
+ val nextWidth = PixelUtil.toPixelFromDIP(width).toFloat()
190
+ val nextHeight = PixelUtil.toPixelFromDIP(height).toFloat()
191
+ val nextTopLeftRadius = PixelUtil.toPixelFromDIP(topLeftRadius).toFloat()
192
+ val nextTopRightRadius = PixelUtil.toPixelFromDIP(topRightRadius).toFloat()
193
+ val nextBottomRightRadius = PixelUtil.toPixelFromDIP(bottomRightRadius).toFloat()
194
+ val nextBottomLeftRadius = PixelUtil.toPixelFromDIP(bottomLeftRadius).toFloat()
195
+ val nextContentTranslateX = PixelUtil.toPixelFromDIP(contentTranslateX).toFloat()
196
+ val nextContentTranslateY = PixelUtil.toPixelFromDIP(contentTranslateY).toFloat()
197
+ val nextContentScale = contentScale.toFloat()
198
+ if (!nextX.isFinite() || !nextY.isFinite() || !nextWidth.isFinite() ||
199
+ !nextHeight.isFinite() || !nextTopLeftRadius.isFinite() ||
200
+ !nextTopRightRadius.isFinite() || !nextBottomRightRadius.isFinite() ||
201
+ !nextBottomLeftRadius.isFinite() || !nextContentTranslateX.isFinite() ||
202
+ !nextContentTranslateY.isFinite() || !nextContentScale.isFinite() ||
203
+ nextContentScale <= 0f
204
+ ) {
205
+ return
206
+ }
207
+
208
+ requestedX = nextX
209
+ requestedY = nextY
210
+ requestedWidth = nextWidth
211
+ requestedHeight = nextHeight
212
+ requestedRadius = 0f
213
+ requestedTopLeftRadius = nextTopLeftRadius
214
+ requestedTopRightRadius = nextTopRightRadius
215
+ requestedBottomRightRadius = nextBottomRightRadius
216
+ requestedBottomLeftRadius = nextBottomLeftRadius
217
+ requestedCurveCode = curveCode
218
+ requestedContentTranslateX = nextContentTranslateX
219
+ requestedContentTranslateY = nextContentTranslateY
220
+ requestedContentScale = nextContentScale
221
+ requestedUsesV2Geometry = true
123
222
  applyRequestedGeometry()
124
223
  }
125
224
 
@@ -138,25 +237,112 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
138
237
  contentTranslateX: Float,
139
238
  contentTranslateY: Float,
140
239
  ) {
141
- // Stored only; applyNormalizedClipPx computes the clip residual and
142
- // applyClipPlacement writes both translations together, because the
143
- // content's final offset depends on the residual as well.
144
- requestedContentTranslateX = contentTranslateX
145
- requestedContentTranslateY = contentTranslateY
146
- applyNormalizedClipPx(left, top, right, bottom, radius)
240
+ // Debug-only Perfetto slice for the Kotlin share of the C++
241
+ // SmoothClip.setPresentation section (see SmoothClipTrace.h).
242
+ if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyPx")
243
+ try {
244
+ // Stored only; applyNormalizedClipPx computes the clip residual and
245
+ // applyClipPlacement writes both translations together, because the
246
+ // content's final offset depends on the residual as well.
247
+ requestedContentTranslateX = contentTranslateX
248
+ requestedContentTranslateY = contentTranslateY
249
+ requestedContentScale = 1f
250
+ applyNormalizedClipPx(left, top, right, bottom, radius)
251
+ } finally {
252
+ if (BuildConfig.DEBUG) Trace.endSection()
253
+ }
254
+ }
255
+
256
+ /**
257
+ * V2 driver hot path. Geometry is already intersected with the host and
258
+ * its four radii have already had the CSS overlap factor applied in C++.
259
+ */
260
+ @DoNotStrip
261
+ fun setClipPresentationV2Px(
262
+ left: Float,
263
+ top: Float,
264
+ right: Float,
265
+ bottom: Float,
266
+ topLeftRadius: Float,
267
+ topRightRadius: Float,
268
+ bottomRightRadius: Float,
269
+ bottomLeftRadius: Float,
270
+ curveCode: Int,
271
+ contentTranslateX: Float,
272
+ contentTranslateY: Float,
273
+ contentScale: Float,
274
+ ) {
275
+ if (!left.isFinite() || !top.isFinite() || !right.isFinite() ||
276
+ !bottom.isFinite() || !topLeftRadius.isFinite() ||
277
+ !topRightRadius.isFinite() || !bottomRightRadius.isFinite() ||
278
+ !bottomLeftRadius.isFinite() || !contentTranslateX.isFinite() ||
279
+ !contentTranslateY.isFinite() || !contentScale.isFinite() ||
280
+ contentScale <= 0f ||
281
+ (curveCode != CLIP_CURVE_CIRCULAR && curveCode != CLIP_CURVE_CONTINUOUS)
282
+ ) {
283
+ return
284
+ }
285
+
286
+ if (BuildConfig.DEBUG) Trace.beginSection("SmoothClip.applyV2Px")
287
+ try {
288
+ requestedContentTranslateX = contentTranslateX
289
+ requestedContentTranslateY = contentTranslateY
290
+ requestedContentScale = contentScale
291
+ applyNormalizedClipV2Px(
292
+ left,
293
+ top,
294
+ right,
295
+ bottom,
296
+ topLeftRadius,
297
+ topRightRadius,
298
+ bottomRightRadius,
299
+ bottomLeftRadius,
300
+ curveCode,
301
+ )
302
+ } finally {
303
+ if (BuildConfig.DEBUG) Trace.endSection()
304
+ }
147
305
  }
148
306
 
149
307
  private fun applyRequestedGeometry() {
150
- normalizeClipGeometryPx(
151
- requestedX,
152
- requestedY,
153
- requestedWidth,
154
- requestedHeight,
155
- requestedRadius,
156
- width.toFloat(),
157
- height.toFloat(),
158
- ) { left, top, right, bottom, radius ->
159
- applyNormalizedClipPx(left, top, right, bottom, radius)
308
+ if (requestedUsesV2Geometry) {
309
+ normalizeClipGeometryV2Px(
310
+ requestedX,
311
+ requestedY,
312
+ requestedWidth,
313
+ requestedHeight,
314
+ requestedTopLeftRadius,
315
+ requestedTopRightRadius,
316
+ requestedBottomRightRadius,
317
+ requestedBottomLeftRadius,
318
+ requestedCurveCode,
319
+ width.toFloat(),
320
+ height.toFloat(),
321
+ ) { left, top, right, bottom, topLeft, topRight, bottomRight, bottomLeft, curve ->
322
+ applyNormalizedClipV2Px(
323
+ left,
324
+ top,
325
+ right,
326
+ bottom,
327
+ topLeft,
328
+ topRight,
329
+ bottomRight,
330
+ bottomLeft,
331
+ curve,
332
+ )
333
+ }
334
+ } else {
335
+ normalizeClipGeometryPx(
336
+ requestedX,
337
+ requestedY,
338
+ requestedWidth,
339
+ requestedHeight,
340
+ requestedRadius,
341
+ width.toFloat(),
342
+ height.toFloat(),
343
+ ) { left, top, right, bottom, radius ->
344
+ applyNormalizedClipPx(left, top, right, bottom, radius)
345
+ }
160
346
  }
161
347
  }
162
348
 
@@ -184,7 +370,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
184
370
  nextOutlineRight,
185
371
  nextOutlineBottom,
186
372
  )
187
- val outlineGeometryChanged = outlineChanged(
373
+ val outlineGeometryChanged = outlineUsesPath || outlineUsesFloatRoundRect || outlineChanged(
188
374
  nextOutlineLeft,
189
375
  nextOutlineTop,
190
376
  nextOutlineRight,
@@ -202,6 +388,11 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
202
388
  clipRight = right
203
389
  clipBottom = bottom
204
390
  clipRadius = radius
391
+ clipTopLeftRadius = radius
392
+ clipTopRightRadius = radius
393
+ clipBottomRightRadius = radius
394
+ clipBottomLeftRadius = radius
395
+ clipCurveCode = CLIP_CURVE_CIRCULAR
205
396
  // Whatever integer rounding threw away, carried on this view's own
206
397
  // translation so the clip edge still lands where the driver asked
207
398
  // instead of snapping to the pixel grid while the content it clips
@@ -226,6 +417,9 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
226
417
  // edge, so the existing edge/radius key cannot skip invalidateOutline.
227
418
  if (!outlineGeometryChanged) return
228
419
 
420
+ outlineUsesPath = false
421
+ outlineUsesFloatRoundRect = false
422
+ clipPath.reset()
229
423
  outlineLeft = nextOutlineLeft
230
424
  outlineTop = nextOutlineTop
231
425
  outlineRight = nextOutlineRight
@@ -239,6 +433,148 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
239
433
  invalidateOutline()
240
434
  }
241
435
 
436
+ private fun applyNormalizedClipV2Px(
437
+ left: Float,
438
+ top: Float,
439
+ right: Float,
440
+ bottom: Float,
441
+ topLeftRadius: Float,
442
+ topRightRadius: Float,
443
+ bottomRightRadius: Float,
444
+ bottomLeftRadius: Float,
445
+ curveCode: Int,
446
+ ) {
447
+ val radiiAreUniform = topLeftRadius == topRightRadius &&
448
+ topLeftRadius == bottomRightRadius &&
449
+ topLeftRadius == bottomLeftRadius
450
+ val needsPath = curveCode == CLIP_CURVE_CONTINUOUS || !radiiAreUniform
451
+ if (!needsPath) {
452
+ applyNormalizedFloatRoundRectV2Px(
453
+ left,
454
+ top,
455
+ right,
456
+ bottom,
457
+ topLeftRadius,
458
+ )
459
+ return
460
+ }
461
+
462
+ val isEmpty = right <= left || bottom <= top
463
+ val outlineGeometryChanged = !outlineUsesPath ||
464
+ left != clipLeft || top != clipTop || right != clipRight ||
465
+ bottom != clipBottom || topLeftRadius != clipTopLeftRadius ||
466
+ topRightRadius != clipTopRightRadius ||
467
+ bottomRightRadius != clipBottomRightRadius ||
468
+ bottomLeftRadius != clipBottomLeftRadius || curveCode != clipCurveCode
469
+
470
+ clipLeft = left
471
+ clipTop = top
472
+ clipRight = right
473
+ clipBottom = bottom
474
+ clipRadius = if (radiiAreUniform) topLeftRadius else 0f
475
+ clipTopLeftRadius = topLeftRadius
476
+ clipTopRightRadius = topRightRadius
477
+ clipBottomRightRadius = bottomRightRadius
478
+ clipBottomLeftRadius = bottomLeftRadius
479
+ clipCurveCode = curveCode
480
+ // Path outlines retain physical-pixel floats, so unlike the legacy
481
+ // integer round rect they need no residual translation compensation.
482
+ clipResidualX = 0f
483
+ clipResidualY = 0f
484
+ applyClipPlacement()
485
+
486
+ if (isEmpty != clipIsEmpty) {
487
+ if (isEmpty) cancelAcceptedTouchStream()
488
+ clipIsEmpty = isEmpty
489
+ visibility = clipVisibility(isEmpty)
490
+ importantForAccessibility = clipAccessibility(
491
+ isEmpty,
492
+ requestedImportantForAccessibility,
493
+ )
494
+ }
495
+
496
+ if (!outlineGeometryChanged) return
497
+
498
+ outlineUsesPath = true
499
+ outlineUsesFloatRoundRect = false
500
+ clipPath.reset()
501
+ if (!isEmpty) {
502
+ appendRoundedRectPath(
503
+ clipPath,
504
+ left,
505
+ top,
506
+ right,
507
+ bottom,
508
+ topLeftRadius,
509
+ topRightRadius,
510
+ bottomRightRadius,
511
+ bottomLeftRadius,
512
+ curveCode,
513
+ )
514
+ }
515
+ invalidateOutline()
516
+ }
517
+
518
+ /**
519
+ * Protocol V2 uniform circular geometry stays entirely in float physical
520
+ * pixels. V1 intentionally continues through applyNormalizedClipPx so its
521
+ * integer outline and residual placement remain byte-for-byte compatible.
522
+ */
523
+ private fun applyNormalizedFloatRoundRectV2Px(
524
+ left: Float,
525
+ top: Float,
526
+ right: Float,
527
+ bottom: Float,
528
+ radius: Float,
529
+ ) {
530
+ val isEmpty = right <= left || bottom <= top
531
+ val outlineGeometryChanged = outlineUsesPath ||
532
+ !outlineUsesFloatRoundRect ||
533
+ left != clipLeft || top != clipTop || right != clipRight ||
534
+ bottom != clipBottom || radius != clipRadius
535
+
536
+ clipLeft = left
537
+ clipTop = top
538
+ clipRight = right
539
+ clipBottom = bottom
540
+ clipRadius = radius
541
+ clipTopLeftRadius = radius
542
+ clipTopRightRadius = radius
543
+ clipBottomRightRadius = radius
544
+ clipBottomLeftRadius = radius
545
+ clipCurveCode = CLIP_CURVE_CIRCULAR
546
+ clipResidualX = 0f
547
+ clipResidualY = 0f
548
+ applyClipPlacement()
549
+
550
+ if (isEmpty != clipIsEmpty) {
551
+ if (isEmpty) cancelAcceptedTouchStream()
552
+ clipIsEmpty = isEmpty
553
+ visibility = clipVisibility(isEmpty)
554
+ importantForAccessibility = clipAccessibility(
555
+ isEmpty,
556
+ requestedImportantForAccessibility,
557
+ )
558
+ }
559
+
560
+ if (!outlineGeometryChanged) return
561
+ outlineUsesPath = false
562
+ outlineUsesFloatRoundRect = true
563
+ clipPath.reset()
564
+ if (!isEmpty) {
565
+ clipPath.addRoundRect(
566
+ left,
567
+ top,
568
+ right,
569
+ bottom,
570
+ radius,
571
+ radius,
572
+ Path.Direction.CW,
573
+ )
574
+ }
575
+ invalidateOutline()
576
+ }
577
+
242
578
  /**
243
579
  * Single writer for both translations. The view carries the sub-pixel
244
580
  * remainder of the clip origin; the content container subtracts it back out
@@ -251,6 +587,14 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
251
587
  super.setTranslationY(userTranslationY + clipResidualY)
252
588
  contentContainer.translationX = requestedContentTranslateX - clipResidualX
253
589
  contentContainer.translationY = requestedContentTranslateY - clipResidualY
590
+ // Scale is intentionally centered and lives on the content only. View
591
+ // translation properties are applied independently of scale, so a
592
+ // caller's tx/ty remains a physical-pixel offset rather than scaling
593
+ // around the origin with the content.
594
+ contentContainer.pivotX = contentContainer.width / 2f
595
+ contentContainer.pivotY = contentContainer.height / 2f
596
+ contentContainer.scaleX = requestedContentScale
597
+ contentContainer.scaleY = requestedContentScale
254
598
  }
255
599
 
256
600
  // The clip's sub-pixel placement and the consumer's `transform` prop share
@@ -278,6 +622,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
278
622
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
279
623
  super.onSizeChanged(w, h, oldw, oldh)
280
624
  contentContainer.layout(0, 0, w, h)
625
+ applyClipPlacement()
281
626
  if (boundDriverId != 0.0) {
282
627
  // Driver deliveries are pre-normalized in C++ against the pushed
283
628
  // host metrics; refresh them and let the registry redeliver the
@@ -355,6 +700,12 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
355
700
  // back maps the point into the space clipLeft/clipTop are expressed in,
356
701
  // so hit testing stays against the geometry the driver delivered — the
357
702
  // same test as before the residual existed.
703
+ if (outlineUsesPath) {
704
+ // Reuse the exact cubic path supplied to Outline.setPath: hit
705
+ // testing must follow the rendered aperture, including its
706
+ // portable continuous-curve approximation.
707
+ return !clipIsEmpty && containsPathPoint(clipPath, x, y)
708
+ }
358
709
  return containsRoundedPointPx(
359
710
  x + clipResidualX,
360
711
  y + clipResidualY,
@@ -457,11 +808,21 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
457
808
  requestedWidth = 0f
458
809
  requestedHeight = 0f
459
810
  requestedRadius = 0f
811
+ requestedTopLeftRadius = 0f
812
+ requestedTopRightRadius = 0f
813
+ requestedBottomRightRadius = 0f
814
+ requestedBottomLeftRadius = 0f
815
+ requestedCurveCode = CLIP_CURVE_CIRCULAR
460
816
  requestedContentTranslateX = 0f
461
817
  requestedContentTranslateY = 0f
818
+ requestedContentScale = 1f
819
+ requestedUsesV2Geometry = false
462
820
  clipResidualX = 0f
463
821
  clipResidualY = 0f
464
822
  applyClipPlacement()
823
+ outlineUsesPath = false
824
+ outlineUsesFloatRoundRect = false
825
+ clipPath.reset()
465
826
  outlineLeft = 0
466
827
  outlineTop = 0
467
828
  outlineRight = 0