react-native-smooth-clip-view 0.1.0 → 0.2.1

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 (113) hide show
  1. package/README.md +171 -43
  2. package/SmoothClipView.podspec +8 -2
  3. package/android/CMakeLists.txt +39 -0
  4. package/android/build.gradle +69 -2
  5. package/android/proguard-rules.pro +6 -0
  6. package/android/src/main/cpp/SmoothClipAndroid.h +59 -0
  7. package/android/src/main/cpp/SmoothClipBindings.cpp +428 -0
  8. package/android/src/main/cpp/SmoothClipRegistry.cpp +945 -0
  9. package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +25 -0
  10. package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +55 -0
  11. package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +128 -0
  12. package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +164 -30
  13. package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +107 -10
  14. package/android/src/main/java/com/smoothclipview/SmoothClipViewPackage.kt +18 -2
  15. package/cpp/SmoothClipRegistry.h +93 -0
  16. package/cpp/SmoothClipSharedGeometry.h +50 -0
  17. package/ios/SmoothClipModuleProvider.h +9 -0
  18. package/ios/SmoothClipModuleProvider.mm +16 -0
  19. package/ios/SmoothClipRegistry.mm +754 -0
  20. package/ios/SmoothClipTurboModule.cpp +290 -0
  21. package/ios/SmoothClipTurboModule.h +132 -0
  22. package/ios/SmoothClipView.mm +976 -85
  23. package/ios/SmoothClipViewRegistry.h +27 -0
  24. package/lib/module/NativeSmoothClipModule.js +5 -0
  25. package/lib/module/NativeSmoothClipModule.js.map +1 -0
  26. package/lib/module/SmoothClipView.android.js +4 -0
  27. package/lib/module/SmoothClipView.android.js.map +1 -0
  28. package/lib/module/SmoothClipView.ios.js +34 -0
  29. package/lib/module/SmoothClipView.ios.js.map +1 -0
  30. package/lib/module/SmoothClipView.js +26 -12
  31. package/lib/module/SmoothClipView.js.map +1 -1
  32. package/lib/module/SmoothClipView.web.js +28 -12
  33. package/lib/module/SmoothClipView.web.js.map +1 -1
  34. package/lib/module/SmoothClipViewNativeComponent.ts +14 -1
  35. package/lib/module/driverState.js +102 -0
  36. package/lib/module/driverState.js.map +1 -0
  37. package/lib/module/driverTypes.js +4 -0
  38. package/lib/module/driverTypes.js.map +1 -0
  39. package/lib/module/drivers.android.js +4 -0
  40. package/lib/module/drivers.android.js.map +1 -0
  41. package/lib/module/drivers.ios.js +380 -0
  42. package/lib/module/drivers.ios.js.map +1 -0
  43. package/lib/module/drivers.js +276 -0
  44. package/lib/module/drivers.js.map +1 -0
  45. package/lib/module/geometry.js +19 -0
  46. package/lib/module/geometry.js.map +1 -1
  47. package/lib/module/index.js +2 -0
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/index.web.js +2 -0
  50. package/lib/module/index.web.js.map +1 -1
  51. package/lib/module/nativeCompletion.js +21 -0
  52. package/lib/module/nativeCompletion.js.map +1 -0
  53. package/lib/module/reactRequests.js +50 -0
  54. package/lib/module/reactRequests.js.map +1 -0
  55. package/lib/module/smoothClipNative.android.js +15 -0
  56. package/lib/module/smoothClipNative.android.js.map +1 -0
  57. package/lib/module/smoothClipNative.js +8 -0
  58. package/lib/module/smoothClipNative.js.map +1 -0
  59. package/lib/typescript/src/NativeSmoothClipModule.d.ts +19 -0
  60. package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -0
  61. package/lib/typescript/src/SmoothClipView.android.d.ts +3 -0
  62. package/lib/typescript/src/SmoothClipView.android.d.ts.map +1 -0
  63. package/lib/typescript/src/SmoothClipView.d.ts +5 -6
  64. package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
  65. package/lib/typescript/src/SmoothClipView.ios.d.ts +9 -0
  66. package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -0
  67. package/lib/typescript/src/SmoothClipView.web.d.ts +5 -7
  68. package/lib/typescript/src/SmoothClipView.web.d.ts.map +1 -1
  69. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +4 -0
  70. package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
  71. package/lib/typescript/src/driverState.d.ts +31 -0
  72. package/lib/typescript/src/driverState.d.ts.map +1 -0
  73. package/lib/typescript/src/driverTypes.d.ts +61 -0
  74. package/lib/typescript/src/driverTypes.d.ts.map +1 -0
  75. package/lib/typescript/src/drivers.android.d.ts +3 -0
  76. package/lib/typescript/src/drivers.android.d.ts.map +1 -0
  77. package/lib/typescript/src/drivers.d.ts +5 -0
  78. package/lib/typescript/src/drivers.d.ts.map +1 -0
  79. package/lib/typescript/src/drivers.ios.d.ts +5 -0
  80. package/lib/typescript/src/drivers.ios.d.ts.map +1 -0
  81. package/lib/typescript/src/geometry.d.ts +8 -0
  82. package/lib/typescript/src/geometry.d.ts.map +1 -1
  83. package/lib/typescript/src/index.d.ts +4 -1
  84. package/lib/typescript/src/index.d.ts.map +1 -1
  85. package/lib/typescript/src/index.web.d.ts +4 -1
  86. package/lib/typescript/src/index.web.d.ts.map +1 -1
  87. package/lib/typescript/src/nativeCompletion.d.ts +7 -0
  88. package/lib/typescript/src/nativeCompletion.d.ts.map +1 -0
  89. package/lib/typescript/src/reactRequests.d.ts +7 -0
  90. package/lib/typescript/src/reactRequests.d.ts.map +1 -0
  91. package/lib/typescript/src/smoothClipNative.android.d.ts +4 -0
  92. package/lib/typescript/src/smoothClipNative.android.d.ts.map +1 -0
  93. package/lib/typescript/src/smoothClipNative.d.ts +3 -0
  94. package/lib/typescript/src/smoothClipNative.d.ts.map +1 -0
  95. package/package.json +12 -7
  96. package/src/NativeSmoothClipModule.ts +104 -0
  97. package/src/SmoothClipView.android.tsx +2 -0
  98. package/src/SmoothClipView.ios.tsx +36 -0
  99. package/src/SmoothClipView.tsx +31 -27
  100. package/src/SmoothClipView.web.tsx +26 -22
  101. package/src/SmoothClipViewNativeComponent.ts +14 -1
  102. package/src/driverState.ts +150 -0
  103. package/src/driverTypes.ts +91 -0
  104. package/src/drivers.android.ts +6 -0
  105. package/src/drivers.ios.ts +665 -0
  106. package/src/drivers.ts +416 -0
  107. package/src/geometry.ts +39 -0
  108. package/src/index.ts +19 -1
  109. package/src/index.web.ts +19 -1
  110. package/src/nativeCompletion.ts +37 -0
  111. package/src/reactRequests.ts +64 -0
  112. package/src/smoothClipNative.android.ts +20 -0
  113. package/src/smoothClipNative.ts +5 -0
@@ -3,6 +3,7 @@ package com.smoothclipview
3
3
  import android.view.View
4
4
  import kotlin.math.max
5
5
  import kotlin.math.min
6
+ import kotlin.math.roundToInt
6
7
 
7
8
  /**
8
9
  * Normalizes clip geometry without allocating a result object. The callback is
@@ -53,6 +54,30 @@ internal inline fun normalizeClipGeometryPx(
53
54
  internal fun clipVisibility(isEmpty: Boolean): Int =
54
55
  if (isEmpty) View.INVISIBLE else View.VISIBLE
55
56
 
57
+ /**
58
+ * Outline dedupe on the rounded integer physical-pixel edges actually emitted
59
+ * to the outline provider. Sub-pixel motion that rounds to the cached integers
60
+ * must not invalidate the outline; the radius stays a float comparison because
61
+ * it is emitted to the outline as a float.
62
+ */
63
+ internal fun outlineChanged(
64
+ left: Float,
65
+ top: Float,
66
+ right: Float,
67
+ bottom: Float,
68
+ radius: Float,
69
+ cachedLeft: Int,
70
+ cachedTop: Int,
71
+ cachedRight: Int,
72
+ cachedBottom: Int,
73
+ cachedRadius: Float,
74
+ ): Boolean =
75
+ left.roundToInt() != cachedLeft ||
76
+ top.roundToInt() != cachedTop ||
77
+ right.roundToInt() != cachedRight ||
78
+ bottom.roundToInt() != cachedBottom ||
79
+ radius != cachedRadius
80
+
56
81
  internal fun clipAccessibility(
57
82
  isEmpty: Boolean,
58
83
  requestedAccessibility: Int,
@@ -0,0 +1,55 @@
1
+ package com.smoothclipview
2
+
3
+ import com.facebook.proguard.annotations.DoNotStrip
4
+ import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
5
+
6
+ /**
7
+ * Bridge to the shared C++ SmoothClip registry. All driver state, animation and
8
+ * geometry live in native C++ (see android/src/main/cpp); this object only loads
9
+ * the library, exposes the worklet-callable JSI bindings installer, and forwards
10
+ * Fabric view lifecycle to the registry.
11
+ */
12
+ @DoNotStrip
13
+ internal object SmoothClipBindings {
14
+ init {
15
+ System.loadLibrary("smoothclipview")
16
+ }
17
+
18
+ /** Installs `global.__SmoothClipView` and wires completion delivery. */
19
+ external fun getBindingsInstaller(): BindingsInstallerHolder
20
+
21
+ external fun nativeRegisterView(
22
+ driverId: Double,
23
+ view: SmoothClipView,
24
+ x: Double,
25
+ y: Double,
26
+ width: Double,
27
+ height: Double,
28
+ radius: Double,
29
+ contentTranslateX: Double,
30
+ contentTranslateY: Double,
31
+ density: Double,
32
+ widthPx: Double,
33
+ heightPx: Double,
34
+ )
35
+
36
+ /**
37
+ * Pushes a registered view's density and pixel host size so driver
38
+ * deliveries are pre-normalized in C++; redelivers the visible value.
39
+ */
40
+ external fun nativeSetViewHostGeometry(
41
+ driverId: Double,
42
+ view: SmoothClipView,
43
+ density: Double,
44
+ widthPx: Double,
45
+ heightPx: Double,
46
+ )
47
+
48
+ external fun nativeUnregisterView(driverId: Double, view: SmoothClipView)
49
+
50
+ /**
51
+ * Releases runtime-bound listener state during host teardown; must run
52
+ * while the JS runtime is still alive (module invalidate).
53
+ */
54
+ external fun nativeInvalidate()
55
+ }
@@ -0,0 +1,128 @@
1
+ package com.smoothclipview
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactApplicationContext
5
+ import com.facebook.react.bridge.ReadableArray
6
+ import com.facebook.react.bridge.WritableArray
7
+ import com.facebook.react.module.annotations.ReactModule
8
+ import com.facebook.react.turbomodule.core.interfaces.BindingsInstallerHolder
9
+ import com.facebook.react.turbomodule.core.interfaces.TurboModuleWithJSIBindings
10
+
11
+ /**
12
+ * The Android driver runs entirely through worklet-callable C++ JSI bindings
13
+ * (installed via [getBindingsInstaller]); JS resolves `global.__SmoothClipView`
14
+ * instead of these TurboModule methods. The spec methods below are required by
15
+ * codegen but are never invoked on Android.
16
+ */
17
+ @ReactModule(name = NativeSmoothClipModuleSpec.NAME)
18
+ class SmoothClipModule(context: ReactApplicationContext) :
19
+ NativeSmoothClipModuleSpec(context), TurboModuleWithJSIBindings {
20
+
21
+ override fun getBindingsInstaller(): BindingsInstallerHolder =
22
+ SmoothClipBindings.getBindingsInstaller()
23
+
24
+ override fun invalidate() {
25
+ super.invalidate()
26
+ // Completions must stop and runtime-bound listener functions must be
27
+ // released before the JS runtime is torn down; otherwise a later host
28
+ // would deliver completions into a destroyed runtime.
29
+ SmoothClipBindings.nativeInvalidate()
30
+ }
31
+
32
+ override fun setClipPresentation(
33
+ driverId: Double,
34
+ x: Double,
35
+ y: Double,
36
+ width: Double,
37
+ height: Double,
38
+ radius: Double,
39
+ contentTranslateX: Double,
40
+ contentTranslateY: Double,
41
+ takeOwnership: Boolean,
42
+ ) = Unit
43
+
44
+ override fun beginInteraction(driverId: Double): WritableArray =
45
+ Arguments.createArray()
46
+
47
+ override fun animateTiming(
48
+ driverId: Double,
49
+ hasInteractiveStart: Boolean,
50
+ startX: Double,
51
+ startY: Double,
52
+ startWidth: Double,
53
+ startHeight: Double,
54
+ startRadius: Double,
55
+ startContentTranslateX: Double,
56
+ startContentTranslateY: Double,
57
+ x: Double,
58
+ y: Double,
59
+ width: Double,
60
+ height: Double,
61
+ radius: Double,
62
+ contentTranslateX: Double,
63
+ contentTranslateY: Double,
64
+ durationMs: Double,
65
+ controlPoint1X: Double,
66
+ controlPoint1Y: Double,
67
+ controlPoint2X: Double,
68
+ controlPoint2Y: Double,
69
+ reduceMotion: Double,
70
+ ): Double = 0.0
71
+
72
+ override fun animateSpring(
73
+ driverId: Double,
74
+ hasInteractiveStart: Boolean,
75
+ startX: Double,
76
+ startY: Double,
77
+ startWidth: Double,
78
+ startHeight: Double,
79
+ startRadius: Double,
80
+ startContentTranslateX: Double,
81
+ startContentTranslateY: Double,
82
+ x: Double,
83
+ y: Double,
84
+ width: Double,
85
+ height: Double,
86
+ radius: Double,
87
+ contentTranslateX: Double,
88
+ contentTranslateY: Double,
89
+ mass: Double,
90
+ stiffness: Double,
91
+ damping: Double,
92
+ initialVelocity: Double,
93
+ inheritVelocity: Boolean,
94
+ reduceMotion: Double,
95
+ ): Double = 0.0
96
+
97
+ override fun animateKeyframes(
98
+ driverId: Double,
99
+ hasInteractiveStart: Boolean,
100
+ startX: Double,
101
+ startY: Double,
102
+ startWidth: Double,
103
+ startHeight: Double,
104
+ startRadius: Double,
105
+ startContentTranslateX: Double,
106
+ startContentTranslateY: Double,
107
+ x: Double,
108
+ y: Double,
109
+ width: Double,
110
+ height: Double,
111
+ radius: Double,
112
+ contentTranslateX: Double,
113
+ contentTranslateY: Double,
114
+ durationMs: Double,
115
+ frames: ReadableArray,
116
+ reduceMotion: Double,
117
+ ): Double = 0.0
118
+
119
+ override fun rejectAnimation(driverId: Double): Double = 0.0
120
+
121
+ override fun cancelAnimation(
122
+ driverId: Double,
123
+ animationId: Double,
124
+ behavior: Double,
125
+ ): WritableArray = Arguments.createArray()
126
+
127
+ override fun destroyDriver(driverId: Double) = Unit
128
+ }
@@ -1,29 +1,46 @@
1
1
  package com.smoothclipview
2
2
 
3
+ import android.content.res.Configuration
3
4
  import android.graphics.Outline
4
5
  import android.view.MotionEvent
5
6
  import android.view.View
6
7
  import android.view.ViewOutlineProvider
8
+ import com.facebook.proguard.annotations.DoNotStrip
7
9
  import com.facebook.react.uimanager.PixelUtil
8
10
  import com.facebook.react.uimanager.ThemedReactContext
9
11
  import com.facebook.react.views.view.ReactViewGroup
10
12
  import kotlin.math.roundToInt
11
13
 
12
14
  class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
15
+ internal val contentContainer = ReactViewGroup(context)
13
16
  private var requestedX = 0f
14
17
  private var requestedY = 0f
15
18
  private var requestedWidth = 0f
16
19
  private var requestedHeight = 0f
17
20
  private var requestedRadius = 0f
21
+ private var requestedContentTranslateX = 0f
22
+ private var requestedContentTranslateY = 0f
18
23
  private var clipLeft = 0f
19
24
  private var clipTop = 0f
20
25
  private var clipRight = 0f
21
26
  private var clipBottom = 0f
22
27
  private var clipRadius = 0f
28
+ // Rounded edges actually emitted to the outline provider. Sub-pixel changes
29
+ // that round to the same integers must not invalidate the outline.
30
+ private var outlineLeft = 0
31
+ private var outlineTop = 0
32
+ private var outlineRight = 0
33
+ private var outlineBottom = 0
23
34
  private var clipIsEmpty = true
24
35
  private var requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
25
36
  private var acceptingTouchStream = false
26
37
 
38
+ /** Driver this view is registered with in the native registry (0 = none). */
39
+ internal var boundDriverId: Double = 0.0
40
+
41
+ /** Set once a legacy command arrives; later initialClip* props are ignored. */
42
+ internal var commandIsAuthoritative = false
43
+
27
44
  private val clipOutlineProvider = object : ViewOutlineProvider() {
28
45
  override fun getOutline(view: View, outline: Outline) {
29
46
  if (clipIsEmpty) {
@@ -32,16 +49,17 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
32
49
  }
33
50
 
34
51
  outline.setRoundRect(
35
- clipLeft.roundToInt(),
36
- clipTop.roundToInt(),
37
- clipRight.roundToInt(),
38
- clipBottom.roundToInt(),
52
+ outlineLeft,
53
+ outlineTop,
54
+ outlineRight,
55
+ outlineBottom,
39
56
  clipRadius,
40
57
  )
41
58
  }
42
59
  }
43
60
 
44
61
  init {
62
+ super.addView(contentContainer)
45
63
  outlineProvider = clipOutlineProvider
46
64
  clipToOutline = true
47
65
  visibility = INVISIBLE
@@ -54,9 +72,21 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
54
72
  width: Double,
55
73
  height: Double,
56
74
  radius: Double,
75
+ ) = setClipPresentationDip(x, y, width, height, radius, 0.0, 0.0)
76
+
77
+ @DoNotStrip
78
+ fun setClipPresentationDip(
79
+ x: Double,
80
+ y: Double,
81
+ width: Double,
82
+ height: Double,
83
+ radius: Double,
84
+ contentTranslateX: Double,
85
+ contentTranslateY: Double,
57
86
  ) {
58
87
  if (!x.isFinite() || !y.isFinite() || !width.isFinite() ||
59
- !height.isFinite() || !radius.isFinite()
88
+ !height.isFinite() || !radius.isFinite() ||
89
+ !contentTranslateX.isFinite() || !contentTranslateY.isFinite()
60
90
  ) {
61
91
  return
62
92
  }
@@ -66,8 +96,11 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
66
96
  val nextWidth = PixelUtil.toPixelFromDIP(width).toFloat()
67
97
  val nextHeight = PixelUtil.toPixelFromDIP(height).toFloat()
68
98
  val nextRadius = PixelUtil.toPixelFromDIP(radius).toFloat()
99
+ val nextContentTranslateX = PixelUtil.toPixelFromDIP(contentTranslateX).toFloat()
100
+ val nextContentTranslateY = PixelUtil.toPixelFromDIP(contentTranslateY).toFloat()
69
101
  if (!nextX.isFinite() || !nextY.isFinite() || !nextWidth.isFinite() ||
70
- !nextHeight.isFinite() || !nextRadius.isFinite()
102
+ !nextHeight.isFinite() || !nextRadius.isFinite() ||
103
+ !nextContentTranslateX.isFinite() || !nextContentTranslateY.isFinite()
71
104
  ) {
72
105
  return
73
106
  }
@@ -77,9 +110,43 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
77
110
  requestedWidth = nextWidth
78
111
  requestedHeight = nextHeight
79
112
  requestedRadius = nextRadius
113
+ if (requestedContentTranslateX != nextContentTranslateX) {
114
+ requestedContentTranslateX = nextContentTranslateX
115
+ contentContainer.translationX = nextContentTranslateX
116
+ }
117
+ if (requestedContentTranslateY != nextContentTranslateY) {
118
+ requestedContentTranslateY = nextContentTranslateY
119
+ contentContainer.translationY = nextContentTranslateY
120
+ }
80
121
  applyRequestedGeometry()
81
122
  }
82
123
 
124
+ /**
125
+ * Driver hot path: called from the C++ registry with final pixel-space,
126
+ * pre-normalized values (see deliverToView in SmoothClipRegistry.cpp).
127
+ * No validation, scaling or normalization happens here.
128
+ */
129
+ @DoNotStrip
130
+ fun setClipPresentationPx(
131
+ left: Float,
132
+ top: Float,
133
+ right: Float,
134
+ bottom: Float,
135
+ radius: Float,
136
+ contentTranslateX: Float,
137
+ contentTranslateY: Float,
138
+ ) {
139
+ if (requestedContentTranslateX != contentTranslateX) {
140
+ requestedContentTranslateX = contentTranslateX
141
+ contentContainer.translationX = contentTranslateX
142
+ }
143
+ if (requestedContentTranslateY != contentTranslateY) {
144
+ requestedContentTranslateY = contentTranslateY
145
+ contentContainer.translationY = contentTranslateY
146
+ }
147
+ applyNormalizedClipPx(left, top, right, bottom, radius)
148
+ }
149
+
83
150
  private fun applyRequestedGeometry() {
84
151
  normalizeClipGeometryPx(
85
152
  requestedX,
@@ -90,40 +157,98 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
90
157
  width.toFloat(),
91
158
  height.toFloat(),
92
159
  ) { left, top, right, bottom, radius ->
93
- val isEmpty = right <= left || bottom <= top
94
- val geometryChanged =
95
- left != clipLeft || top != clipTop || right != clipRight ||
96
- bottom != clipBottom || radius != clipRadius
160
+ applyNormalizedClipPx(left, top, right, bottom, radius)
161
+ }
162
+ }
97
163
 
98
- if (!geometryChanged && isEmpty == clipIsEmpty) {
99
- return@normalizeClipGeometryPx
100
- }
164
+ private fun applyNormalizedClipPx(
165
+ left: Float,
166
+ top: Float,
167
+ right: Float,
168
+ bottom: Float,
169
+ radius: Float,
170
+ ) {
171
+ val isEmpty = right <= left || bottom <= top
172
+ val outlineGeometryChanged = outlineChanged(
173
+ left,
174
+ top,
175
+ right,
176
+ bottom,
177
+ radius,
178
+ outlineLeft,
179
+ outlineTop,
180
+ outlineRight,
181
+ outlineBottom,
182
+ clipRadius,
183
+ )
101
184
 
102
- clipLeft = left
103
- clipTop = top
104
- clipRight = right
105
- clipBottom = bottom
106
- clipRadius = radius
107
-
108
- if (isEmpty != clipIsEmpty) {
109
- clipIsEmpty = isEmpty
110
- visibility = clipVisibility(isEmpty)
111
- importantForAccessibility = clipAccessibility(
112
- isEmpty,
113
- requestedImportantForAccessibility,
114
- )
115
- if (isEmpty) acceptingTouchStream = false
116
- }
185
+ if (!outlineGeometryChanged && isEmpty == clipIsEmpty) {
186
+ return
187
+ }
188
+
189
+ clipLeft = left
190
+ clipTop = top
191
+ clipRight = right
192
+ clipBottom = bottom
193
+ clipRadius = radius
194
+ outlineLeft = left.roundToInt()
195
+ outlineTop = top.roundToInt()
196
+ outlineRight = right.roundToInt()
197
+ outlineBottom = bottom.roundToInt()
117
198
 
118
- invalidateOutline()
199
+ if (isEmpty != clipIsEmpty) {
200
+ clipIsEmpty = isEmpty
201
+ visibility = clipVisibility(isEmpty)
202
+ importantForAccessibility = clipAccessibility(
203
+ isEmpty,
204
+ requestedImportantForAccessibility,
205
+ )
206
+ if (isEmpty) acceptingTouchStream = false
119
207
  }
208
+
209
+ invalidateOutline()
120
210
  }
121
211
 
122
212
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
123
213
  super.onSizeChanged(w, h, oldw, oldh)
124
- applyRequestedGeometry()
214
+ contentContainer.layout(0, 0, w, h)
215
+ if (boundDriverId != 0.0) {
216
+ // Driver deliveries are pre-normalized in C++ against the pushed
217
+ // host metrics; refresh them and let the registry redeliver the
218
+ // visible value synchronously.
219
+ SmoothClipBindings.nativeSetViewHostGeometry(
220
+ boundDriverId,
221
+ this,
222
+ densityScale(),
223
+ w.toDouble(),
224
+ h.toDouble(),
225
+ )
226
+ }
227
+ if (boundDriverId == 0.0 || commandIsAuthoritative) {
228
+ // Command geometry lives in the requested* DIP fields. Reapplying
229
+ // after the driver redelivery keeps the last-writer semantics: an
230
+ // authoritative command wins over the driver value on resize.
231
+ applyRequestedGeometry()
232
+ }
125
233
  }
126
234
 
235
+ override fun onConfigurationChanged(newConfig: Configuration) {
236
+ super.onConfigurationChanged(newConfig)
237
+ if (boundDriverId != 0.0) {
238
+ // Density can change without a size change (display switch); the
239
+ // registry scales DIP deliveries by the pushed density.
240
+ SmoothClipBindings.nativeSetViewHostGeometry(
241
+ boundDriverId,
242
+ this,
243
+ densityScale(),
244
+ width.toDouble(),
245
+ height.toDouble(),
246
+ )
247
+ }
248
+ }
249
+
250
+ internal fun densityScale(): Double = PixelUtil.toPixelFromDIP(1f).toDouble()
251
+
127
252
  private fun containsRoundedPoint(x: Float, y: Float): Boolean {
128
253
  return containsRoundedPointPx(
129
254
  x,
@@ -179,11 +304,20 @@ class SmoothClipView(context: ThemedReactContext) : ReactViewGroup(context) {
179
304
 
180
305
  fun resetClipState() {
181
306
  acceptingTouchStream = false
307
+ commandIsAuthoritative = false
182
308
  requestedX = 0f
183
309
  requestedY = 0f
184
310
  requestedWidth = 0f
185
311
  requestedHeight = 0f
186
312
  requestedRadius = 0f
313
+ requestedContentTranslateX = 0f
314
+ requestedContentTranslateY = 0f
315
+ contentContainer.translationX = 0f
316
+ contentContainer.translationY = 0f
317
+ outlineLeft = 0
318
+ outlineTop = 0
319
+ outlineRight = 0
320
+ outlineBottom = 0
187
321
  requestedImportantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_AUTO
188
322
  applyRequestedGeometry()
189
323
  }
@@ -1,5 +1,6 @@
1
1
  package com.smoothclipview
2
2
 
3
+ import android.view.View
3
4
  import com.facebook.react.module.annotations.ReactModule
4
5
  import com.facebook.react.uimanager.ThemedReactContext
5
6
  import com.facebook.react.uimanager.ViewGroupManager
@@ -20,10 +21,21 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
20
21
  var width: Double = 0.0,
21
22
  var height: Double = 0.0,
22
23
  var radius: Double = 0.0,
24
+ var contentTranslateX: Double = 0.0,
25
+ var contentTranslateY: Double = 0.0,
26
+ var driverId: Double = 0.0,
27
+ // Whether any prop setter ran in the current update transaction.
28
+ var dirty: Boolean = false,
23
29
  )
24
30
 
31
+ // Sticky per-view prop state: Fabric only delivers changed props, so a
32
+ // transaction touching one initialClip* prop must keep every other value
33
+ // — including driverId — from earlier transactions.
25
34
  private val pendingInitialClips = java.util.WeakHashMap<SmoothClipView, PendingInitialClip>()
26
35
 
36
+ private fun pending(view: SmoothClipView): PendingInitialClip =
37
+ pendingInitialClips.getOrPut(view) { PendingInitialClip() }.also { it.dirty = true }
38
+
27
39
  override fun getName(): String = NAME
28
40
 
29
41
  override fun getDelegate(): ViewManagerDelegate<SmoothClipView> = delegate
@@ -31,6 +43,25 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
31
43
  override fun createViewInstance(context: ThemedReactContext): SmoothClipView =
32
44
  SmoothClipView(context).also { pendingInitialClips[it] = PendingInitialClip() }
33
45
 
46
+ @ReactProp(name = "driverId")
47
+ override fun setDriverId(view: SmoothClipView, value: Double) {
48
+ pending(view).driverId = value
49
+ }
50
+
51
+ override fun addView(parent: SmoothClipView, child: View, index: Int) {
52
+ parent.contentContainer.addView(child, index)
53
+ }
54
+
55
+ override fun getChildCount(parent: SmoothClipView): Int =
56
+ parent.contentContainer.childCount
57
+
58
+ override fun getChildAt(parent: SmoothClipView, index: Int): View =
59
+ parent.contentContainer.getChildAt(index)
60
+
61
+ override fun removeViewAt(parent: SmoothClipView, index: Int) {
62
+ parent.contentContainer.removeViewAt(index)
63
+ }
64
+
34
65
  override fun setImportantForAccessibility(
35
66
  view: SmoothClipView,
36
67
  importantForAccessibility: String?,
@@ -41,27 +72,37 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
41
72
 
42
73
  @ReactProp(name = "initialClipX")
43
74
  override fun setInitialClipX(view: SmoothClipView, value: Double) {
44
- pendingInitialClips.getOrPut(view) { PendingInitialClip() }.x = value
75
+ pending(view).x = value
45
76
  }
46
77
 
47
78
  @ReactProp(name = "initialClipY")
48
79
  override fun setInitialClipY(view: SmoothClipView, value: Double) {
49
- pendingInitialClips.getOrPut(view) { PendingInitialClip() }.y = value
80
+ pending(view).y = value
50
81
  }
51
82
 
52
83
  @ReactProp(name = "initialClipWidth")
53
84
  override fun setInitialClipWidth(view: SmoothClipView, value: Double) {
54
- pendingInitialClips.getOrPut(view) { PendingInitialClip() }.width = value
85
+ pending(view).width = value
55
86
  }
56
87
 
57
88
  @ReactProp(name = "initialClipHeight")
58
89
  override fun setInitialClipHeight(view: SmoothClipView, value: Double) {
59
- pendingInitialClips.getOrPut(view) { PendingInitialClip() }.height = value
90
+ pending(view).height = value
60
91
  }
61
92
 
62
93
  @ReactProp(name = "initialClipRadius")
63
94
  override fun setInitialClipRadius(view: SmoothClipView, value: Double) {
64
- pendingInitialClips.getOrPut(view) { PendingInitialClip() }.radius = value
95
+ pending(view).radius = value
96
+ }
97
+
98
+ @ReactProp(name = "initialContentTranslateX")
99
+ override fun setInitialContentTranslateX(view: SmoothClipView, value: Double) {
100
+ pending(view).contentTranslateX = value
101
+ }
102
+
103
+ @ReactProp(name = "initialContentTranslateY")
104
+ override fun setInitialContentTranslateY(view: SmoothClipView, value: Double) {
105
+ pending(view).contentTranslateY = value
65
106
  }
66
107
 
67
108
  override fun setClipGeometry(
@@ -72,28 +113,84 @@ class SmoothClipViewManager : ViewGroupManager<SmoothClipView>(),
72
113
  height: Double,
73
114
  radius: Double,
74
115
  ) {
75
- pendingInitialClips.remove(view)
116
+ view.commandIsAuthoritative = true
76
117
  view.setClipGeometryDip(x, y, width, height, radius)
77
118
  }
78
119
 
120
+ override fun setClipPresentation(
121
+ view: SmoothClipView,
122
+ x: Double,
123
+ y: Double,
124
+ width: Double,
125
+ height: Double,
126
+ radius: Double,
127
+ contentTranslateX: Double,
128
+ contentTranslateY: Double,
129
+ ) {
130
+ view.commandIsAuthoritative = true
131
+ view.setClipPresentationDip(
132
+ x,
133
+ y,
134
+ width,
135
+ height,
136
+ radius,
137
+ contentTranslateX,
138
+ contentTranslateY,
139
+ )
140
+ }
141
+
79
142
  override fun onAfterUpdateTransaction(view: SmoothClipView) {
80
143
  super.onAfterUpdateTransaction(view)
81
- val initial = pendingInitialClips.remove(view)
82
- if (initial == null) {
144
+ val initial = pendingInitialClips[view]
145
+ if (initial == null || !initial.dirty) {
83
146
  view.reapplyClipPresentation()
84
- } else {
85
- view.setClipGeometryDip(
147
+ return
148
+ }
149
+ initial.dirty = false
150
+ if (view.boundDriverId != 0.0 && view.boundDriverId != initial.driverId) {
151
+ // The view moved to a different driver: leave the old driver's
152
+ // fanout before joining the new one.
153
+ SmoothClipBindings.nativeUnregisterView(view.boundDriverId, view)
154
+ }
155
+ view.boundDriverId = initial.driverId
156
+ if (initial.driverId != 0.0) {
157
+ SmoothClipBindings.nativeRegisterView(
158
+ initial.driverId,
159
+ view,
86
160
  initial.x,
87
161
  initial.y,
88
162
  initial.width,
89
163
  initial.height,
90
164
  initial.radius,
165
+ initial.contentTranslateX,
166
+ initial.contentTranslateY,
167
+ view.densityScale(),
168
+ view.width.toDouble(),
169
+ view.height.toDouble(),
91
170
  )
171
+ } else if (!view.commandIsAuthoritative) {
172
+ // Command-driven mode (driverId 0): the initial props are the only
173
+ // source of the first clip, so apply them directly.
174
+ view.setClipPresentationDip(
175
+ initial.x,
176
+ initial.y,
177
+ initial.width,
178
+ initial.height,
179
+ initial.radius,
180
+ initial.contentTranslateX,
181
+ initial.contentTranslateY,
182
+ )
183
+ } else {
184
+ view.reapplyClipPresentation()
92
185
  }
93
186
  }
94
187
 
95
188
  override fun onDropViewInstance(view: SmoothClipView) {
96
189
  pendingInitialClips.remove(view)
190
+ if (view.boundDriverId != 0.0) {
191
+ SmoothClipBindings.nativeUnregisterView(view.boundDriverId, view)
192
+ view.boundDriverId = 0.0
193
+ }
97
194
  view.resetClipState()
98
195
  super.onDropViewInstance(view)
99
196
  }