react-native-smooth-clip-view 0.2.9 → 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 (130) hide show
  1. package/README.md +60 -21
  2. package/android/build.gradle +6 -2
  3. package/android/proguard-rules.pro +3 -3
  4. package/android/src/main/cpp/SmoothClipAndroid.h +10 -0
  5. package/android/src/main/cpp/SmoothClipBindings.cpp +890 -4
  6. package/android/src/main/cpp/SmoothClipRegistry.cpp +1056 -44
  7. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +265 -0
  8. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +21 -0
  9. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +283 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +371 -18
  11. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +138 -24
  12. package/cpp/SmoothClipAnimationCurve.h +151 -122
  13. package/cpp/SmoothClipRegistry.h +95 -3
  14. package/cpp/SmoothClipSharedGeometry.h +146 -7
  15. package/cpp/SmoothClipVelocityTracker.h +13 -9
  16. package/ios/SmoothClipGeometry.h +184 -6
  17. package/ios/SmoothClipRegistry.mm +1028 -22
  18. package/ios/SmoothClipRegistrySnapshot.h +13 -0
  19. package/ios/SmoothClipTurboModule.cpp +857 -2
  20. package/ios/SmoothClipTurboModule.h +310 -0
  21. package/ios/SmoothClipView.mm +714 -183
  22. package/ios/SmoothClipViewRegistry.h +1 -0
  23. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  24. package/ios/reactnativesmoothclipview.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -1
  26. package/lib/module/SmoothClipView.ios.js +22 -6
  27. package/lib/module/SmoothClipView.ios.js.map +1 -1
  28. package/lib/module/SmoothClipView.js +1 -46
  29. package/lib/module/SmoothClipView.js.map +1 -1
  30. package/lib/module/SmoothClipViewNativeComponent.ts +27 -1
  31. package/lib/module/capabilities.js +4 -0
  32. package/lib/module/capabilities.js.map +1 -0
  33. package/lib/module/capabilities.native.js +36 -0
  34. package/lib/module/capabilities.native.js.map +1 -0
  35. package/lib/module/capabilityTypes.js +11 -0
  36. package/lib/module/capabilityTypes.js.map +1 -0
  37. package/lib/module/driverState.js +0 -28
  38. package/lib/module/driverState.js.map +1 -1
  39. package/lib/module/drivers.js +1 -281
  40. package/lib/module/drivers.js.map +1 -1
  41. package/lib/module/drivers.native.js +263 -63
  42. package/lib/module/drivers.native.js.map +1 -1
  43. package/lib/module/geometry.js +147 -17
  44. package/lib/module/geometry.js.map +1 -1
  45. package/lib/module/groupDriverTypes.js +4 -0
  46. package/lib/module/groupDriverTypes.js.map +1 -0
  47. package/lib/module/groupDrivers.android.js +4 -0
  48. package/lib/module/groupDrivers.android.js.map +1 -0
  49. package/lib/module/groupDrivers.ios.js +4 -0
  50. package/lib/module/groupDrivers.ios.js.map +1 -0
  51. package/lib/module/groupDrivers.js +4 -0
  52. package/lib/module/groupDrivers.js.map +1 -0
  53. package/lib/module/groupDrivers.native.js +799 -0
  54. package/lib/module/groupDrivers.native.js.map +1 -0
  55. package/lib/module/index.js +3 -2
  56. package/lib/module/index.js.map +1 -1
  57. package/lib/module/presentationProtocol.js +15 -0
  58. package/lib/module/presentationProtocol.js.map +1 -0
  59. package/lib/typescript/src/NativeSmoothClipModule.d.ts +25 -0
  60. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
  61. package/lib/typescript/src/SmoothClipView.d.ts +2 -10
  62. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  63. package/lib/typescript/src/SmoothClipView.ios.d.ts +133 -2
  64. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +8 -0
  66. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  67. package/lib/typescript/src/capabilities.d.ts +3 -0
  68. package/lib/typescript/src/capabilities.d.ts.map +1 -0
  69. package/lib/typescript/src/capabilities.native.d.ts +3 -0
  70. package/lib/typescript/src/capabilities.native.d.ts.map +1 -0
  71. package/lib/typescript/src/capabilityTypes.d.ts +10 -0
  72. package/lib/typescript/src/capabilityTypes.d.ts.map +1 -0
  73. package/lib/typescript/src/driverState.d.ts +1 -6
  74. package/lib/typescript/src/driverState.d.ts.map +1 -1
  75. package/lib/typescript/src/driverTypes.d.ts +25 -11
  76. package/lib/typescript/src/driverTypes.d.ts.map +1 -1
  77. package/lib/typescript/src/drivers.d.ts +2 -4
  78. package/lib/typescript/src/drivers.d.ts.map +1 -1
  79. package/lib/typescript/src/drivers.native.d.ts.map +1 -1
  80. package/lib/typescript/src/geometry.d.ts +36 -2
  81. package/lib/typescript/src/geometry.d.ts.map +1 -1
  82. package/lib/typescript/src/groupDriverTypes.d.ts +66 -0
  83. package/lib/typescript/src/groupDriverTypes.d.ts.map +1 -0
  84. package/lib/typescript/src/groupDrivers.android.d.ts +2 -0
  85. package/lib/typescript/src/groupDrivers.android.d.ts.map +1 -0
  86. package/lib/typescript/src/groupDrivers.d.ts +2 -0
  87. package/lib/typescript/src/groupDrivers.d.ts.map +1 -0
  88. package/lib/typescript/src/groupDrivers.ios.d.ts +2 -0
  89. package/lib/typescript/src/groupDrivers.ios.d.ts.map +1 -0
  90. package/lib/typescript/src/groupDrivers.native.d.ts +4 -0
  91. package/lib/typescript/src/groupDrivers.native.d.ts.map +1 -0
  92. package/lib/typescript/src/index.d.ts +6 -3
  93. package/lib/typescript/src/index.d.ts.map +1 -1
  94. package/lib/typescript/src/presentationProtocol.d.ts +4 -0
  95. package/lib/typescript/src/presentationProtocol.d.ts.map +1 -0
  96. package/package.json +10 -11
  97. package/src/NativeSmoothClipModule.ts +275 -0
  98. package/src/SmoothClipView.ios.tsx +30 -11
  99. package/src/SmoothClipView.tsx +2 -76
  100. package/src/SmoothClipViewNativeComponent.ts +27 -1
  101. package/src/capabilities.native.ts +69 -0
  102. package/src/capabilities.ts +2 -0
  103. package/src/capabilityTypes.ts +17 -0
  104. package/src/driverState.ts +1 -41
  105. package/src/driverTypes.ts +42 -11
  106. package/src/drivers.native.ts +824 -124
  107. package/src/drivers.ts +2 -423
  108. package/src/geometry.ts +231 -27
  109. package/src/groupDriverTypes.ts +117 -0
  110. package/src/groupDrivers.android.ts +1 -0
  111. package/src/groupDrivers.ios.ts +1 -0
  112. package/src/groupDrivers.native.ts +1189 -0
  113. package/src/groupDrivers.ts +1 -0
  114. package/src/index.ts +30 -2
  115. package/src/presentationProtocol.ts +28 -0
  116. package/lib/module/SmoothClipView.web.js +0 -54
  117. package/lib/module/SmoothClipView.web.js.map +0 -1
  118. package/lib/module/index.web.js +0 -8
  119. package/lib/module/index.web.js.map +0 -1
  120. package/lib/module/webClipPath.js +0 -25
  121. package/lib/module/webClipPath.js.map +0 -1
  122. package/lib/typescript/src/SmoothClipView.web.d.ts +0 -10
  123. package/lib/typescript/src/SmoothClipView.web.d.ts.map +0 -1
  124. package/lib/typescript/src/index.web.d.ts +0 -9
  125. package/lib/typescript/src/index.web.d.ts.map +0 -1
  126. package/lib/typescript/src/webClipPath.d.ts +0 -7
  127. package/lib/typescript/src/webClipPath.d.ts.map +0 -1
  128. package/src/SmoothClipView.web.tsx +0 -66
  129. package/src/index.web.ts +0 -23
  130. package/src/webClipPath.ts +0 -30
@@ -2,6 +2,7 @@ package com.smoothclipview
2
2
 
3
3
  import android.content.res.Configuration
4
4
  import android.graphics.Outline
5
+ import android.graphics.Path
5
6
  import android.os.Trace
6
7
  import android.view.MotionEvent
7
8
  import android.view.View
@@ -18,13 +19,28 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
18
19
  private var requestedWidth = 0f
19
20
  private var requestedHeight = 0f
20
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
21
27
  private var requestedContentTranslateX = 0f
22
28
  private var requestedContentTranslateY = 0f
29
+ private var requestedContentScale = 1f
30
+ private var requestedUsesV2Geometry = false
23
31
  private var clipLeft = 0f
24
32
  private var clipTop = 0f
25
33
  private var clipRight = 0f
26
34
  private var clipBottom = 0f
27
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
28
44
  // Rounded edges actually emitted to the outline provider. Sub-pixel changes
29
45
  // that round to the same integers must not invalidate the outline.
30
46
  private var outlineLeft = 0
@@ -57,13 +73,23 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
57
73
  return
58
74
  }
59
75
 
60
- outline.setRoundRect(
61
- outlineLeft,
62
- outlineTop,
63
- outlineRight,
64
- outlineBottom,
65
- clipRadius,
66
- )
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
+ }
67
93
  }
68
94
  }
69
95
 
@@ -119,8 +145,80 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
119
145
  requestedWidth = nextWidth
120
146
  requestedHeight = nextHeight
121
147
  requestedRadius = nextRadius
148
+ requestedTopLeftRadius = nextRadius
149
+ requestedTopRightRadius = nextRadius
150
+ requestedBottomRightRadius = nextRadius
151
+ requestedBottomLeftRadius = nextRadius
152
+ requestedCurveCode = CLIP_CURVE_CIRCULAR
122
153
  requestedContentTranslateX = nextContentTranslateX
123
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
124
222
  applyRequestedGeometry()
125
223
  }
126
224
 
@@ -148,23 +246,103 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
148
246
  // content's final offset depends on the residual as well.
149
247
  requestedContentTranslateX = contentTranslateX
150
248
  requestedContentTranslateY = contentTranslateY
249
+ requestedContentScale = 1f
151
250
  applyNormalizedClipPx(left, top, right, bottom, radius)
152
251
  } finally {
153
252
  if (BuildConfig.DEBUG) Trace.endSection()
154
253
  }
155
254
  }
156
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
+ }
305
+ }
306
+
157
307
  private fun applyRequestedGeometry() {
158
- normalizeClipGeometryPx(
159
- requestedX,
160
- requestedY,
161
- requestedWidth,
162
- requestedHeight,
163
- requestedRadius,
164
- width.toFloat(),
165
- height.toFloat(),
166
- ) { left, top, right, bottom, radius ->
167
- 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
+ }
168
346
  }
169
347
  }
170
348
 
@@ -192,7 +370,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
192
370
  nextOutlineRight,
193
371
  nextOutlineBottom,
194
372
  )
195
- val outlineGeometryChanged = outlineChanged(
373
+ val outlineGeometryChanged = outlineUsesPath || outlineUsesFloatRoundRect || outlineChanged(
196
374
  nextOutlineLeft,
197
375
  nextOutlineTop,
198
376
  nextOutlineRight,
@@ -210,6 +388,11 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
210
388
  clipRight = right
211
389
  clipBottom = bottom
212
390
  clipRadius = radius
391
+ clipTopLeftRadius = radius
392
+ clipTopRightRadius = radius
393
+ clipBottomRightRadius = radius
394
+ clipBottomLeftRadius = radius
395
+ clipCurveCode = CLIP_CURVE_CIRCULAR
213
396
  // Whatever integer rounding threw away, carried on this view's own
214
397
  // translation so the clip edge still lands where the driver asked
215
398
  // instead of snapping to the pixel grid while the content it clips
@@ -234,6 +417,9 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
234
417
  // edge, so the existing edge/radius key cannot skip invalidateOutline.
235
418
  if (!outlineGeometryChanged) return
236
419
 
420
+ outlineUsesPath = false
421
+ outlineUsesFloatRoundRect = false
422
+ clipPath.reset()
237
423
  outlineLeft = nextOutlineLeft
238
424
  outlineTop = nextOutlineTop
239
425
  outlineRight = nextOutlineRight
@@ -247,6 +433,148 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
247
433
  invalidateOutline()
248
434
  }
249
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
+
250
578
  /**
251
579
  * Single writer for both translations. The view carries the sub-pixel
252
580
  * remainder of the clip origin; the content container subtracts it back out
@@ -259,6 +587,14 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
259
587
  super.setTranslationY(userTranslationY + clipResidualY)
260
588
  contentContainer.translationX = requestedContentTranslateX - clipResidualX
261
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
262
598
  }
263
599
 
264
600
  // The clip's sub-pixel placement and the consumer's `transform` prop share
@@ -286,6 +622,7 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
286
622
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
287
623
  super.onSizeChanged(w, h, oldw, oldh)
288
624
  contentContainer.layout(0, 0, w, h)
625
+ applyClipPlacement()
289
626
  if (boundDriverId != 0.0) {
290
627
  // Driver deliveries are pre-normalized in C++ against the pushed
291
628
  // host metrics; refresh them and let the registry redeliver the
@@ -363,6 +700,12 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
363
700
  // back maps the point into the space clipLeft/clipTop are expressed in,
364
701
  // so hit testing stays against the geometry the driver delivered — the
365
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
+ }
366
709
  return containsRoundedPointPx(
367
710
  x + clipResidualX,
368
711
  y + clipResidualY,
@@ -465,11 +808,21 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
465
808
  requestedWidth = 0f
466
809
  requestedHeight = 0f
467
810
  requestedRadius = 0f
811
+ requestedTopLeftRadius = 0f
812
+ requestedTopRightRadius = 0f
813
+ requestedBottomRightRadius = 0f
814
+ requestedBottomLeftRadius = 0f
815
+ requestedCurveCode = CLIP_CURVE_CIRCULAR
468
816
  requestedContentTranslateX = 0f
469
817
  requestedContentTranslateY = 0f
818
+ requestedContentScale = 1f
819
+ requestedUsesV2Geometry = false
470
820
  clipResidualX = 0f
471
821
  clipResidualY = 0f
472
822
  applyClipPlacement()
823
+ outlineUsesPath = false
824
+ outlineUsesFloatRoundRect = false
825
+ clipPath.reset()
473
826
  outlineLeft = 0
474
827
  outlineTop = 0
475
828
  outlineRight = 0
@@ -21,8 +21,15 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
21
21
  var width: Double = 0.0,
22
22
  var height: Double = 0.0,
23
23
  var radius: Double = 0.0,
24
+ var presentationVersion: Int = 1,
25
+ var topLeftRadius: Double = 0.0,
26
+ var topRightRadius: Double = 0.0,
27
+ var bottomRightRadius: Double = 0.0,
28
+ var bottomLeftRadius: Double = 0.0,
29
+ var curveCode: Int = CLIP_CURVE_CIRCULAR,
24
30
  var contentTranslateX: Double = 0.0,
25
31
  var contentTranslateY: Double = 0.0,
32
+ var contentScale: Double = 1.0,
26
33
  var driverId: Double = 0.0,
27
34
  // Whether any prop setter ran in the current update transaction.
28
35
  var dirty: Boolean = false,
@@ -95,6 +102,36 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
95
102
  pending(view).radius = value
96
103
  }
97
104
 
105
+ @ReactProp(name = "presentationVersion")
106
+ override fun setPresentationVersion(view: SmoothClipView, value: Int) {
107
+ pending(view).presentationVersion = value
108
+ }
109
+
110
+ @ReactProp(name = "initialClipTopLeftRadius")
111
+ override fun setInitialClipTopLeftRadius(view: SmoothClipView, value: Double) {
112
+ pending(view).topLeftRadius = value
113
+ }
114
+
115
+ @ReactProp(name = "initialClipTopRightRadius")
116
+ override fun setInitialClipTopRightRadius(view: SmoothClipView, value: Double) {
117
+ pending(view).topRightRadius = value
118
+ }
119
+
120
+ @ReactProp(name = "initialClipBottomRightRadius")
121
+ override fun setInitialClipBottomRightRadius(view: SmoothClipView, value: Double) {
122
+ pending(view).bottomRightRadius = value
123
+ }
124
+
125
+ @ReactProp(name = "initialClipBottomLeftRadius")
126
+ override fun setInitialClipBottomLeftRadius(view: SmoothClipView, value: Double) {
127
+ pending(view).bottomLeftRadius = value
128
+ }
129
+
130
+ @ReactProp(name = "initialClipCurve")
131
+ override fun setInitialClipCurve(view: SmoothClipView, value: Int) {
132
+ pending(view).curveCode = value
133
+ }
134
+
98
135
  @ReactProp(name = "initialContentTranslateX")
99
136
  override fun setInitialContentTranslateX(view: SmoothClipView, value: Double) {
100
137
  pending(view).contentTranslateX = value
@@ -105,6 +142,11 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
105
142
  pending(view).contentTranslateY = value
106
143
  }
107
144
 
145
+ @ReactProp(name = "initialContentScale")
146
+ override fun setInitialContentScale(view: SmoothClipView, value: Double) {
147
+ pending(view).contentScale = value
148
+ }
149
+
108
150
  override fun setClipGeometry(
109
151
  view: SmoothClipView,
110
152
  x: Double,
@@ -139,6 +181,38 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
139
181
  )
140
182
  }
141
183
 
184
+ override fun setClipPresentationV2(
185
+ view: SmoothClipView,
186
+ x: Double,
187
+ y: Double,
188
+ width: Double,
189
+ height: Double,
190
+ topLeftRadius: Double,
191
+ topRightRadius: Double,
192
+ bottomRightRadius: Double,
193
+ bottomLeftRadius: Double,
194
+ curveCode: Int,
195
+ contentTranslateX: Double,
196
+ contentTranslateY: Double,
197
+ contentScale: Double,
198
+ ) {
199
+ view.commandIsAuthoritative = true
200
+ view.setClipPresentationV2Dip(
201
+ x,
202
+ y,
203
+ width,
204
+ height,
205
+ topLeftRadius,
206
+ topRightRadius,
207
+ bottomRightRadius,
208
+ bottomLeftRadius,
209
+ curveCode,
210
+ contentTranslateX,
211
+ contentTranslateY,
212
+ contentScale,
213
+ )
214
+ }
215
+
142
216
  override fun onAfterUpdateTransaction(view: SmoothClipView) {
143
217
  super.onAfterUpdateTransaction(view)
144
218
  val initial = pendingInitialClips[view]
@@ -154,33 +228,73 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
154
228
  }
155
229
  view.boundDriverId = initial.driverId
156
230
  if (initial.driverId != 0.0) {
157
- SmoothClipBindings.nativeRegisterView(
158
- initial.driverId,
159
- view,
160
- initial.x,
161
- initial.y,
162
- initial.width,
163
- initial.height,
164
- initial.radius,
165
- initial.contentTranslateX,
166
- initial.contentTranslateY,
167
- view.densityScale(),
168
- view.width.toDouble(),
169
- view.height.toDouble(),
170
- view.isHostLifecycleVisible(),
171
- )
231
+ if (initial.presentationVersion >= 2) {
232
+ SmoothClipBindings.nativeRegisterViewV2(
233
+ initial.driverId,
234
+ view,
235
+ initial.x,
236
+ initial.y,
237
+ initial.width,
238
+ initial.height,
239
+ initial.topLeftRadius,
240
+ initial.topRightRadius,
241
+ initial.bottomRightRadius,
242
+ initial.bottomLeftRadius,
243
+ initial.curveCode,
244
+ initial.contentTranslateX,
245
+ initial.contentTranslateY,
246
+ initial.contentScale,
247
+ view.densityScale(),
248
+ view.width.toDouble(),
249
+ view.height.toDouble(),
250
+ view.isHostLifecycleVisible(),
251
+ )
252
+ } else {
253
+ SmoothClipBindings.nativeRegisterView(
254
+ initial.driverId,
255
+ view,
256
+ initial.x,
257
+ initial.y,
258
+ initial.width,
259
+ initial.height,
260
+ initial.radius,
261
+ initial.contentTranslateX,
262
+ initial.contentTranslateY,
263
+ view.densityScale(),
264
+ view.width.toDouble(),
265
+ view.height.toDouble(),
266
+ view.isHostLifecycleVisible(),
267
+ )
268
+ }
172
269
  } else if (!view.commandIsAuthoritative) {
173
270
  // Command-driven mode (driverId 0): the initial props are the only
174
271
  // source of the first clip, so apply them directly.
175
- view.setClipPresentationDip(
176
- initial.x,
177
- initial.y,
178
- initial.width,
179
- initial.height,
180
- initial.radius,
181
- initial.contentTranslateX,
182
- initial.contentTranslateY,
183
- )
272
+ if (initial.presentationVersion >= 2) {
273
+ view.setClipPresentationV2Dip(
274
+ initial.x,
275
+ initial.y,
276
+ initial.width,
277
+ initial.height,
278
+ initial.topLeftRadius,
279
+ initial.topRightRadius,
280
+ initial.bottomRightRadius,
281
+ initial.bottomLeftRadius,
282
+ initial.curveCode,
283
+ initial.contentTranslateX,
284
+ initial.contentTranslateY,
285
+ initial.contentScale,
286
+ )
287
+ } else {
288
+ view.setClipPresentationDip(
289
+ initial.x,
290
+ initial.y,
291
+ initial.width,
292
+ initial.height,
293
+ initial.radius,
294
+ initial.contentTranslateX,
295
+ initial.contentTranslateY,
296
+ )
297
+ }
184
298
  } else {
185
299
  view.reapplyClipPresentation()
186
300
  }