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
package/README.md CHANGED
@@ -1,16 +1,21 @@
1
1
  # react-native-smooth-clip-view
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/react-native-smooth-clip-view.svg)](https://www.npmjs.com/package/react-native-smooth-clip-view)
4
+
3
5
  Layout-free animated rounded clipping for React Native Fabric.
4
6
 
5
- `SmoothClipView` keeps a fixed maximum Yoga footprint while Reanimated sends
6
- one atomic clip geometry update to the native view on the UI runtime. It is
7
- useful for expanding cards, sheets, maps, media, and other reveals where
8
- animating layout width and height would reflow descendants.
7
+ `SmoothClipView` keeps a fixed maximum Yoga footprint while a reusable driver
8
+ updates only the native clipping layer. It is
9
+ useful for expanding cards, sheets, maps, media, zoom transitions, and other reveals where
10
+ animating layout width and height cause lag because of yoga/fabric calculating layout every frame.
11
+
12
+ In other words, it makes animating width height on Reanimated a cheap operation.
9
13
 
10
14
  ## Requirements
11
15
 
12
- - React Native 0.85 or newer with the New Architecture enabled
13
- - React Native Reanimated 4.3 or newer
16
+ - React Native 0.86 or newer with the New Architecture enabled
17
+ - React Native Reanimated 4.5 or newer
18
+ - React Native Worklets 0.10 or newer
14
19
  - iOS 16.4 or newer
15
20
  - Android API 26 or newer
16
21
 
@@ -19,6 +24,8 @@ legacy Paper implementation.
19
24
 
20
25
  ## Installation
21
26
 
27
+ Available from [npm](https://www.npmjs.com/package/react-native-smooth-clip-view):
28
+
22
29
  ```sh
23
30
  npm install react-native-smooth-clip-view react-native-reanimated react-native-worklets
24
31
  ```
@@ -40,13 +47,9 @@ import { Button, StyleSheet, View } from 'react-native';
40
47
  import {
41
48
  type ClipGeometry,
42
49
  SmoothClipView,
50
+ createClipPresentation,
51
+ useSmoothClipDriver,
43
52
  } from 'react-native-smooth-clip-view';
44
- import {
45
- interpolate,
46
- useDerivedValue,
47
- useSharedValue,
48
- withTiming,
49
- } from 'react-native-reanimated';
50
53
 
51
54
  const initialClip: ClipGeometry = {
52
55
  x: 120,
@@ -58,28 +61,29 @@ const initialClip: ClipGeometry = {
58
61
 
59
62
  export function ClipExample() {
60
63
  const [expanded, setExpanded] = useState(false);
61
- const progress = useSharedValue(0);
62
- const animatedClip = useDerivedValue<ClipGeometry>(() => ({
63
- x: interpolate(progress.value, [0, 1], [120, 0]),
64
- y: interpolate(progress.value, [0, 1], [180, 0]),
65
- width: interpolate(progress.value, [0, 1], [80, 320]),
66
- height: interpolate(progress.value, [0, 1], [80, 480]),
67
- radius: interpolate(progress.value, [0, 1], [40, 24]),
68
- }));
64
+ const driver = useSmoothClipDriver(
65
+ createClipPresentation(initialClip, -initialClip.x, -initialClip.y)
66
+ );
69
67
 
70
68
  const toggle = () => {
71
69
  const next = !expanded;
72
70
  setExpanded(next);
73
- progress.value = withTiming(next ? 1 : 0);
71
+ const clip = next
72
+ ? { x: 0, y: 0, width: 320, height: 480, radius: 24 }
73
+ : initialClip;
74
+ void driver.react.animateTo(
75
+ createClipPresentation(clip, -clip.x, -clip.y),
76
+ {
77
+ type: 'timing',
78
+ duration: 450,
79
+ controlPoints: [0.42, 0, 0.58, 1],
80
+ }
81
+ );
74
82
  };
75
83
 
76
84
  return (
77
85
  <View>
78
- <SmoothClipView
79
- initialClip={initialClip}
80
- animatedClip={animatedClip}
81
- style={styles.host}
82
- >
86
+ <SmoothClipView driver={driver} style={styles.host}>
83
87
  <View style={styles.content} />
84
88
  </SmoothClipView>
85
89
  <Button title="Toggle clip" onPress={toggle} />
@@ -93,9 +97,57 @@ const styles = StyleSheet.create({
93
97
  });
94
98
  ```
95
99
 
96
- `initialClip` must equal `animatedClip.value` when the component mounts. This
97
- lets native rendering start with the correct clip before the first UI-runtime
98
- command is dispatched.
100
+ The same driver can be passed to multiple hosts. One native registry update
101
+ fans the seven-scalar presentation out to all mounted hosts. Interactive
102
+ updates avoid Yoga and ShadowTree commits.
103
+
104
+ For transitions whose endpoint is known, let Core Animation interpolate on the
105
+ render server:
106
+
107
+ ```tsx
108
+ const expandFromReact = () =>
109
+ driver.react.animateTo(
110
+ createClipPresentation(
111
+ { x: 0, y: 0, width: 320, height: 480, radius: 24 },
112
+ 0,
113
+ 0
114
+ ),
115
+ {
116
+ type: 'timing',
117
+ duration: 450,
118
+ controlPoints: [0.42, 0, 0.58, 1],
119
+ }
120
+ );
121
+ ```
122
+
123
+ Native timing, spring, and keyframed transitions have no app callback between
124
+ setup and completion on iOS. Core Animation interpolates the clip and content
125
+ translation as one presentation. Android runs one C++ frame-loop driven by the
126
+ vsync clock with registry fanout; outline calculation and invalidation still
127
+ run on its UI thread. Springs are physically integrated per geometry channel
128
+ and finish when they settle below sub-pixel thresholds on both platforms.
129
+ Retargeting starts from visible state.
130
+ The same driver can be grabbed by a gesture without a visual jump:
131
+
132
+ ```tsx
133
+ const gesture = Gesture.Pan()
134
+ .onStart(() => {
135
+ const visible = driver.ui.beginInteraction();
136
+ dragStart.value = visible.clip.height;
137
+ })
138
+ .onUpdate((event) => {
139
+ const clip = geometryForDrag(dragStart.value, event.translationY);
140
+ // Per-frame hot path: writes straight to native without SharedValue
141
+ // bookkeeping. Assigning driver.presentation.value also works.
142
+ driver.ui.setScalars(clip.x, clip.y, clip.width, clip.height, clip.radius, 0, 0);
143
+ })
144
+ .onEnd(() => {
145
+ driver.ui.animateTo(createClipPresentation(expandedClip), {
146
+ type: 'spring',
147
+ initialVelocity: 'inherit',
148
+ });
149
+ });
150
+ ```
99
151
 
100
152
  ## API
101
153
 
@@ -103,11 +155,78 @@ command is dispatched.
103
155
 
104
156
  `SmoothClipViewProps` extends React Native `ViewProps` and adds:
105
157
 
106
- | Prop | Type | Description |
107
- | --- | --- | --- |
108
- | `initialClip` | `ClipGeometry` | Clip rendered synchronously at mount. |
109
- | `animatedClip` | `SharedValue<ClipGeometry>` | Geometry observed on the Reanimated UI runtime. |
110
- | `children` | `ReactNode` | Content rendered inside the fixed host. |
158
+ | Prop | Type | Description |
159
+ | ---------- | ------------------ | --------------------------------------- |
160
+ | `driver` | `SmoothClipDriver` | Reusable hybrid clip driver. |
161
+ | `children` | `ReactNode` | Content rendered inside the fixed host. |
162
+
163
+ ### Driver
164
+
165
+ - `useSmoothClipDriver(initialPresentation, options)` returns one hybrid driver
166
+ whose writable `presentation` SharedValue contains clip geometry and content
167
+ translation. Passing a `ClipGeometry` remains supported and initializes both
168
+ translations to zero.
169
+ - `driver.ui` is the synchronous UI-worklet interface. `beginInteraction()`
170
+ freezes a transition at visible presentation state; `set()`, `animateTo()`,
171
+ and `cancel()` change ownership atomically.
172
+ - `driver.ui.setScalars(x, y, width, height, radius, tx, ty)` is the per-frame
173
+ hot path: it writes geometry straight to native without touching
174
+ `driver.presentation`, skipping SharedValue bookkeeping for high-frequency
175
+ streams such as gestures. `driver.presentation.value` is stale after hot
176
+ writes by design; `beginInteraction()` remains the source of truth for
177
+ visible geometry, and `animateTo()` after hot writes starts from the native
178
+ registry's latest value rather than the stale SharedValue. Do not interleave
179
+ `setScalars` with `presentation.value` writes on the same driver.
180
+ - Spring `initialVelocity` is one normalized scalar along the current-to-target
181
+ trajectory, in units of the remaining distance per second (`1` covers the
182
+ remaining distance in one second). Every geometry channel continues with the
183
+ same normalized rate, so grab/release preserves the felt direction and
184
+ speed. `'inherit'` (the default) estimates the scalar from the last two
185
+ interactive samples on iOS and Android; samples older than 100 ms — and the
186
+ web fallback — fall back to zero.
187
+ - `driver.react` exposes `beginInteraction`, `set`, `animateTo`, and `cancel`
188
+ as Promises (`setScalars` is UI-worklet-only). React code never blocks
189
+ waiting for main/UI-thread work. An immediate animation request resolves
190
+ before its completion callback is delivered.
191
+ - `animateTo()` transfers ownership to native animation. Timing uses
192
+ cubic Bézier control points; springs accept mass, stiffness, damping, and an
193
+ explicit normalized velocity or `'inherit'` (the default). Keyframes accept
194
+ validated, monotonically increasing offsets from zero through one.
195
+ - `cancel()` freezes visible presentation by default. Pass `'target'` as its
196
+ behavior to jump to the requested endpoint.
197
+ - `options.onAnimationComplete` fires once per animation with its ID and
198
+ `finished` state, including cancellation, replacement, participant unmount,
199
+ and native-side rejection (`animateTo` then returns `0` and one
200
+ `finished: false` completion is delivered).
201
+ - An `animateTo` issued before any host view has registered (for example from
202
+ an effect in the same commit that mounts the host, or inside a modal route
203
+ that attaches late) is held pending and starts with its full duration when
204
+ the first view registers. If no view ever registers, the pending animation
205
+ survives until it is replaced, cancelled, overridden by a take-ownership
206
+ write, or the driver is destroyed — at which point its single
207
+ `finished: false` completion is delivered.
208
+
209
+ Do not call `driver.ui` from React code — it throws on the React runtime; use
210
+ `driver.react` there. During a native transition, `driver.presentation.value`
211
+ is the requested target, not a per-frame mirror of the native presentation
212
+ layer. `beginInteraction()` returns geometry normalized against the host
213
+ bounds, so a clip that extended beyond the host comes back clamped. Start
214
+ gestures with `beginInteraction()`: interactive writes issued while native
215
+ still owns rendering are dropped.
216
+
217
+ ### `SmoothClipPresentation`
218
+
219
+ ```ts
220
+ type SmoothClipPresentation = Readonly<{
221
+ clip: ClipGeometry;
222
+ contentTranslateX: number;
223
+ contentTranslateY: number;
224
+ }>;
225
+ ```
226
+
227
+ Use the content translation fields when a clipped viewport must reveal a
228
+ fixed-size child without a separate Reanimated transform. Native transitions
229
+ animate clip position, bounds, radius, and content translation together.
111
230
 
112
231
  ### `ClipGeometry`
113
232
 
@@ -131,6 +250,15 @@ negative sizes, and clamps `radius` to half of the visible shortest edge.
131
250
  contract for tests and non-native calculations. Native bounds remain
132
251
  authoritative at render time.
133
252
 
253
+ ## Migrating from 0.0.x
254
+
255
+ Version 0.1.0 removes the `initialClip` and `animatedClip` props in favor of
256
+ the driver API: create a driver with `useSmoothClipDriver(initialPresentation)`,
257
+ pass it as the `driver` prop, and move per-frame updates from the
258
+ `animatedClip` SharedValue to `driver.presentation.value` (or
259
+ `driver.ui.setScalars`). Autonomous transitions move from Reanimated
260
+ `withTiming`/`withSpring` to `driver.ui.animateTo` / `driver.react.animateTo`.
261
+
134
262
  ## Layout and styling contract
135
263
 
136
264
  - Give the host its fixed maximum `width` and `height`; clipping never changes
@@ -149,20 +277,20 @@ The [`example`](./example) workspace is an Expo SDK 57 app that exercises the
149
277
  package through its public import on iOS, Android, and web.
150
278
 
151
279
  ```sh
152
- yarn
153
- yarn example ios
154
- yarn example android
155
- yarn example web
280
+ npm install
281
+ npm run example -- ios
282
+ npm run example -- android
283
+ npm run example -- web
156
284
  ```
157
285
 
158
286
  Run the repository checks with:
159
287
 
160
288
  ```sh
161
- yarn lint
162
- yarn typecheck
163
- yarn test
164
- yarn prepare
165
- yarn example build:web
289
+ npm run lint
290
+ npm run typecheck
291
+ npm test
292
+ npm run prepare
293
+ npm run example -- build:web
166
294
  ```
167
295
 
168
296
  ## License
@@ -12,8 +12,14 @@ Pod::Spec.new do |s|
12
12
 
13
13
  s.platforms = { :ios => "16.4" }
14
14
  s.source = { :git => "https://github.com/kbrattli/react-native-smooth-clip-view.git", :tag => "v#{s.version}" }
15
- s.source_files = "ios/*.{h,m,mm,swift,cpp}"
16
- s.private_header_files = "ios/*.h"
15
+ s.source_files = ["ios/*.{h,m,mm,swift,cpp}", "cpp/*.{h,cpp}"]
16
+ s.private_header_files = ["ios/*.h", "cpp/*.h"]
17
+ s.pod_target_xcconfig = {
18
+ "HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/cpp\" \"$(PODS_TARGET_SRCROOT)/ios\"",
19
+ # Signpost instrumentation is compiled into Debug only; Release interactive
20
+ # frames must not generate signpost IDs or intervals.
21
+ "GCC_PREPROCESSOR_DEFINITIONS[config=Debug]" => "$(inherited) SMOOTH_CLIP_ENABLE_SIGNPOSTS=1"
22
+ }
17
23
 
18
24
  install_modules_dependencies(s)
19
25
 
@@ -0,0 +1,39 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(smoothclipview)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+
7
+ find_package(fbjni REQUIRED CONFIG)
8
+ find_package(ReactAndroid REQUIRED CONFIG)
9
+
10
+ # Folly compile flags used across the React Native C++ surface.
11
+ include("${REACT_NATIVE_DIR}/ReactAndroid/cmake-utils/folly-flags.cmake")
12
+ add_compile_options(${folly_FLAGS})
13
+
14
+ file(GLOB smoothclip_SOURCES CONFIGURE_DEPENDS
15
+ "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/*.cpp")
16
+
17
+ add_library(smoothclipview SHARED ${smoothclip_SOURCES})
18
+
19
+ # C++20, RTTI, exceptions and React Native macros.
20
+ include("${REACT_NATIVE_DIR}/ReactCommon/cmake-utils/react-native-flags.cmake")
21
+ target_compile_reactnative_options(smoothclipview PUBLIC)
22
+
23
+ target_include_directories(
24
+ smoothclipview
25
+ PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp"
26
+ "${CMAKE_CURRENT_SOURCE_DIR}/../cpp"
27
+ "${REACT_NATIVE_DIR}/ReactCommon"
28
+ "${REACT_NATIVE_DIR}/ReactCommon/jsi"
29
+ "${REACT_NATIVE_DIR}/ReactCommon/callinvoker"
30
+ "${REACT_NATIVE_DIR}/ReactCommon/react/nativemodule/core/ReactCommon"
31
+ "${REACT_NATIVE_DIR}/ReactAndroid/src/main/jni/react/turbomodule")
32
+
33
+ target_link_libraries(
34
+ smoothclipview
35
+ android
36
+ log
37
+ fbjni::fbjni
38
+ ReactAndroid::jsi
39
+ ReactAndroid::reactnative)
@@ -28,17 +28,74 @@ apply plugin: "com.android.library"
28
28
  apply plugin: "kotlin-android"
29
29
  apply plugin: "com.facebook.react"
30
30
 
31
+ def resolveReactNativeDirectory() {
32
+ def reactNativeLocation = rootProject.ext.has("REACT_NATIVE_NODE_MODULES_DIR")
33
+ ? rootProject.ext.get("REACT_NATIVE_NODE_MODULES_DIR")
34
+ : null
35
+ if (reactNativeLocation != null) {
36
+ return file(reactNativeLocation)
37
+ }
38
+
39
+ // Fallback to node resolver for custom directory structures like monorepos.
40
+ def reactNativePackage = file(
41
+ providers.exec {
42
+ workingDir(rootDir)
43
+ commandLine("node", "--print", "require.resolve('react-native/package.json')")
44
+ }.standardOutput.asText.get().trim()
45
+ )
46
+ if (reactNativePackage.exists()) {
47
+ return reactNativePackage.parentFile
48
+ }
49
+
50
+ throw new GradleException("[react-native-smooth-clip-view] Unable to resolve react-native in node_modules.")
51
+ }
52
+
53
+ def reactNativeRootDir = resolveReactNativeDirectory()
54
+
55
+ def reactNativeArchitectures() {
56
+ def value = rootProject.findProperty("reactNativeArchitectures")
57
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
58
+ }
59
+
31
60
  android {
32
61
  namespace "com.smoothclipview"
33
62
  compileSdkVersion getExtOrDefault("compileSdkVersion")
34
63
 
64
+ if (rootProject.hasProperty("ndkPath")) {
65
+ ndkPath rootProject.ext.ndkPath
66
+ }
67
+ if (rootProject.hasProperty("ndkVersion")) {
68
+ ndkVersion rootProject.ext.ndkVersion
69
+ } else {
70
+ ndkVersion "27.1.12297006"
71
+ }
72
+
73
+ buildFeatures {
74
+ buildConfig true
75
+ prefab true
76
+ }
77
+
35
78
  defaultConfig {
36
79
  minSdkVersion getExtOrDefault("minSdkVersion")
37
80
  targetSdkVersion getExtOrDefault("targetSdkVersion")
81
+ // C++ resolves SmoothClipView by name and calls setClipPresentationDip via
82
+ // a cached jmethodID; consumer minification must not rename or strip them.
83
+ consumerProguardFiles "proguard-rules.pro"
84
+
85
+ externalNativeBuild {
86
+ cmake {
87
+ cppFlags "-O2", "-frtti", "-fexceptions", "-Wall", "-std=c++20"
88
+ arguments "-DANDROID_STL=c++_shared",
89
+ "-DREACT_NATIVE_DIR=${reactNativeRootDir}"
90
+ abiFilters(*reactNativeArchitectures())
91
+ }
92
+ }
38
93
  }
39
94
 
40
- buildFeatures {
41
- buildConfig true
95
+ externalNativeBuild {
96
+ cmake {
97
+ path "CMakeLists.txt"
98
+ }
42
99
  }
43
100
 
44
101
  buildTypes {
@@ -55,6 +112,16 @@ android {
55
112
  sourceCompatibility JavaVersion.VERSION_1_8
56
113
  targetCompatibility JavaVersion.VERSION_1_8
57
114
  }
115
+
116
+ packagingOptions {
117
+ // Provided by the host application at runtime.
118
+ excludes += [
119
+ "**/libc++_shared.so",
120
+ "**/libfbjni.so",
121
+ "**/libjsi.so",
122
+ "**/libreactnative.so",
123
+ ]
124
+ }
58
125
  }
59
126
 
60
127
  dependencies {
@@ -0,0 +1,6 @@
1
+ # SmoothClipView is resolved from C++ by name (fbjni findClassStatic) and its
2
+ # setClipPresentationDip / setClipPresentationPx are invoked through cached
3
+ # jmethodIDs; SmoothClipBindings registers native methods by name. None of them
4
+ # may be renamed or stripped in consumer release builds.
5
+ -keep class com.smoothclipview.SmoothClipView { *; }
6
+ -keep class com.smoothclipview.SmoothClipBindings { *; }
@@ -0,0 +1,59 @@
1
+ #pragma once
2
+
3
+ #include <ReactCommon/CallInvoker.h>
4
+ #include <fbjni/fbjni.h>
5
+ #include <jsi/jsi.h>
6
+
7
+ #include <memory>
8
+
9
+ #include "SmoothClipRegistry.h"
10
+ #include "SmoothClipSharedGeometry.h"
11
+
12
+ namespace smoothclip {
13
+
14
+ // fbjni wrapper around com.smoothclipview.SmoothClipView. Only the primitive
15
+ // clip setter is invoked from native, so the Kotlin view stays a thin outline
16
+ // clipper while all driver state lives in the shared C++ registry.
17
+ struct JSmoothClipView : facebook::jni::JavaClass<JSmoothClipView> {
18
+ static constexpr auto kJavaDescriptor = "Lcom/smoothclipview/SmoothClipView;";
19
+
20
+ void applyClip(const Presentation &presentation) const;
21
+ void applyClipPx(
22
+ const NormalizedClip &clip,
23
+ double contentTranslateXPx,
24
+ double contentTranslateYPx) const;
25
+ };
26
+
27
+ // Called from Kotlin (SmoothClipViewManager) on the UI thread when a Fabric
28
+ // view mounts/unmounts. The registry retains a global ref for the view.
29
+ void registerViewAndroid(
30
+ uint64_t driverId,
31
+ facebook::jni::alias_ref<JSmoothClipView> view,
32
+ Presentation initialPresentation,
33
+ double density,
34
+ double hostWidthPx,
35
+ double hostHeightPx);
36
+ // Refreshes a registered view's density / host size (px) and synchronously
37
+ // redelivers the driver's visible value pre-normalized against them.
38
+ void setViewHostGeometryAndroid(
39
+ uint64_t driverId,
40
+ facebook::jni::alias_ref<JSmoothClipView> view,
41
+ double density,
42
+ double hostWidthPx,
43
+ double hostHeightPx);
44
+ void unregisterViewAndroid(
45
+ uint64_t driverId,
46
+ facebook::jni::alias_ref<JSmoothClipView> view);
47
+
48
+ // Installs `global.__SmoothClipView` (worklet-callable host functions) into the
49
+ // JS runtime and wires native animation completion delivery through the
50
+ // CallInvoker. Invoked by the TurboModule BindingsInstaller.
51
+ void installBindings(
52
+ facebook::jsi::Runtime &runtime,
53
+ const std::shared_ptr<facebook::react::CallInvoker> &callInvoker);
54
+
55
+ // Releases the listener functions and completion sink while the JS runtime is
56
+ // still alive. Called from SmoothClipModule.invalidate() during host teardown.
57
+ void invalidateBindings();
58
+
59
+ } // namespace smoothclip