react-native-smooth-clip-view 0.3.0 → 0.4.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.
- package/README.md +135 -386
- package/android/build.gradle +4 -7
- package/android/proguard-rules.pro +2 -2
- package/android/src/main/cpp/SmoothClipAndroid.h +9 -12
- package/android/src/main/cpp/SmoothClipBindings.cpp +300 -704
- package/android/src/main/cpp/SmoothClipRegistry.cpp +348 -594
- package/android/src/main/java/com/smoothclipview/ClipGeometryNormalizer.kt +36 -227
- package/android/src/main/java/com/smoothclipview/SmoothClipBindings.kt +16 -19
- package/android/src/main/java/com/smoothclipview/SmoothClipModule.kt +36 -294
- package/android/src/main/java/com/smoothclipview/SmoothClipView.kt +369 -486
- package/android/src/main/java/com/smoothclipview/SmoothClipViewManager.kt +75 -77
- package/cpp/SmoothClipAnimationCurve.h +237 -346
- package/cpp/SmoothClipRegistry.h +24 -52
- package/cpp/SmoothClipSharedGeometry.h +64 -94
- package/cpp/SmoothClipVelocityTracker.h +5 -5
- package/ios/SmoothClipGeometry.h +23 -34
- package/ios/SmoothClipRegistry.mm +206 -711
- package/ios/SmoothClipTurboModule.cpp +217 -715
- package/ios/SmoothClipTurboModule.h +54 -297
- package/ios/SmoothClipView.mm +599 -505
- package/ios/SmoothClipViewRegistry.h +2 -7
- package/lib/module/NativeSmoothClipModule.js.map +1 -1
- package/lib/module/SmoothClipView.ios.js +84 -17
- package/lib/module/SmoothClipView.ios.js.map +1 -1
- package/lib/module/SmoothClipView.js +1 -1
- package/lib/module/SmoothClipView.js.map +1 -1
- package/lib/module/SmoothClipViewNativeComponent.ts +20 -26
- package/lib/module/capabilities.native.js +1 -19
- package/lib/module/capabilities.native.js.map +1 -1
- package/lib/module/capabilityTypes.js +0 -9
- package/lib/module/capabilityTypes.js.map +1 -1
- package/lib/module/controllerInternals.js +28 -0
- package/lib/module/controllerInternals.js.map +1 -0
- package/lib/module/controllerLifecycle.js +9 -0
- package/lib/module/controllerLifecycle.js.map +1 -0
- package/lib/module/controllerTypes.js +4 -0
- package/lib/module/controllerTypes.js.map +1 -0
- package/lib/module/controllers.android.js +4 -0
- package/lib/module/controllers.android.js.map +1 -0
- package/lib/module/controllers.ios.js +4 -0
- package/lib/module/controllers.ios.js.map +1 -0
- package/lib/module/controllers.js +4 -0
- package/lib/module/controllers.js.map +1 -0
- package/lib/module/controllers.native.js +89 -0
- package/lib/module/controllers.native.js.map +1 -0
- package/lib/module/geometry.js +71 -51
- package/lib/module/geometry.js.map +1 -1
- package/lib/module/groupTypes.js +4 -0
- package/lib/module/{groupDriverTypes.js.map → groupTypes.js.map} +1 -1
- package/lib/module/groups.android.js +4 -0
- package/lib/module/groups.android.js.map +1 -0
- package/lib/module/groups.ios.js +4 -0
- package/lib/module/groups.ios.js.map +1 -0
- package/lib/module/groups.js +4 -0
- package/lib/module/groups.js.map +1 -0
- package/lib/module/groups.native.js +399 -0
- package/lib/module/groups.native.js.map +1 -0
- package/lib/module/ids.js +8 -0
- package/lib/module/ids.js.map +1 -0
- package/lib/module/index.js +3 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/nativeCompletion.js +37 -16
- package/lib/module/nativeCompletion.js.map +1 -1
- package/lib/module/presentationCodec.js +81 -0
- package/lib/module/presentationCodec.js.map +1 -0
- package/lib/typescript/src/NativeSmoothClipModule.d.ts +14 -23
- package/lib/typescript/src/NativeSmoothClipModule.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts +1 -1
- package/lib/typescript/src/SmoothClipView.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipView.ios.d.ts +18 -14
- package/lib/typescript/src/SmoothClipView.ios.d.ts.map +1 -1
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts +10 -5
- package/lib/typescript/src/SmoothClipViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/capabilities.native.d.ts +1 -1
- package/lib/typescript/src/capabilities.native.d.ts.map +1 -1
- package/lib/typescript/src/capabilityTypes.d.ts +0 -6
- package/lib/typescript/src/capabilityTypes.d.ts.map +1 -1
- package/lib/typescript/src/controllerInternals.d.ts +14 -0
- package/lib/typescript/src/controllerInternals.d.ts.map +1 -0
- package/lib/typescript/src/controllerLifecycle.d.ts +3 -0
- package/lib/typescript/src/controllerLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/controllerTypes.d.ts +62 -0
- package/lib/typescript/src/controllerTypes.d.ts.map +1 -0
- package/lib/typescript/src/controllers.android.d.ts +2 -0
- package/lib/typescript/src/controllers.android.d.ts.map +1 -0
- package/lib/typescript/src/controllers.d.ts +2 -0
- package/lib/typescript/src/controllers.d.ts.map +1 -0
- package/lib/typescript/src/controllers.ios.d.ts +2 -0
- package/lib/typescript/src/controllers.ios.d.ts.map +1 -0
- package/lib/typescript/src/controllers.native.d.ts +3 -0
- package/lib/typescript/src/controllers.native.d.ts.map +1 -0
- package/lib/typescript/src/geometry.d.ts +22 -12
- package/lib/typescript/src/geometry.d.ts.map +1 -1
- package/lib/typescript/src/groupTypes.d.ts +31 -0
- package/lib/typescript/src/groupTypes.d.ts.map +1 -0
- package/lib/typescript/src/groups.android.d.ts +2 -0
- package/lib/typescript/src/groups.android.d.ts.map +1 -0
- package/lib/typescript/src/groups.d.ts +2 -0
- package/lib/typescript/src/groups.d.ts.map +1 -0
- package/lib/typescript/src/groups.ios.d.ts +2 -0
- package/lib/typescript/src/groups.ios.d.ts.map +1 -0
- package/lib/typescript/src/groups.native.d.ts +3 -0
- package/lib/typescript/src/groups.native.d.ts.map +1 -0
- package/lib/typescript/src/ids.d.ts +2 -0
- package/lib/typescript/src/ids.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -6
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/nativeCompletion.d.ts +16 -6
- package/lib/typescript/src/nativeCompletion.d.ts.map +1 -1
- package/lib/typescript/src/presentationCodec.d.ts +10 -0
- package/lib/typescript/src/presentationCodec.d.ts.map +1 -0
- package/package.json +8 -3
- package/src/NativeSmoothClipModule.ts +41 -292
- package/src/SmoothClipView.ios.tsx +130 -23
- package/src/SmoothClipView.tsx +5 -1
- package/src/SmoothClipViewNativeComponent.ts +20 -26
- package/src/capabilities.native.ts +2 -52
- package/src/capabilityTypes.ts +0 -14
- package/src/controllerInternals.ts +52 -0
- package/src/controllerLifecycle.ts +6 -0
- package/src/controllerTypes.ts +87 -0
- package/src/controllers.android.ts +1 -0
- package/src/controllers.ios.ts +1 -0
- package/src/controllers.native.ts +104 -0
- package/src/controllers.ts +1 -0
- package/src/geometry.ts +118 -74
- package/src/groupTypes.ts +53 -0
- package/src/groups.android.ts +1 -0
- package/src/groups.ios.ts +1 -0
- package/src/groups.native.ts +604 -0
- package/src/groups.ts +1 -0
- package/src/ids.ts +6 -0
- package/src/index.ts +20 -28
- package/src/nativeCompletion.ts +67 -32
- package/src/presentationCodec.ts +134 -0
- package/lib/module/driverState.js +0 -82
- package/lib/module/driverState.js.map +0 -1
- package/lib/module/driverTypes.js +0 -4
- package/lib/module/driverTypes.js.map +0 -1
- package/lib/module/drivers.android.js +0 -6
- package/lib/module/drivers.android.js.map +0 -1
- package/lib/module/drivers.ios.js +0 -6
- package/lib/module/drivers.ios.js.map +0 -1
- package/lib/module/drivers.js +0 -4
- package/lib/module/drivers.js.map +0 -1
- package/lib/module/drivers.native.js +0 -756
- package/lib/module/drivers.native.js.map +0 -1
- package/lib/module/groupDriverTypes.js +0 -4
- package/lib/module/groupDrivers.android.js +0 -4
- package/lib/module/groupDrivers.android.js.map +0 -1
- package/lib/module/groupDrivers.ios.js +0 -4
- package/lib/module/groupDrivers.ios.js.map +0 -1
- package/lib/module/groupDrivers.js +0 -4
- package/lib/module/groupDrivers.js.map +0 -1
- package/lib/module/groupDrivers.native.js +0 -799
- package/lib/module/groupDrivers.native.js.map +0 -1
- package/lib/module/presentationProtocol.js +0 -15
- package/lib/module/presentationProtocol.js.map +0 -1
- package/lib/module/reactRequests.js +0 -56
- package/lib/module/reactRequests.js.map +0 -1
- package/lib/typescript/src/driverState.d.ts +0 -26
- package/lib/typescript/src/driverState.d.ts.map +0 -1
- package/lib/typescript/src/driverTypes.d.ts +0 -121
- package/lib/typescript/src/driverTypes.d.ts.map +0 -1
- package/lib/typescript/src/drivers.android.d.ts +0 -3
- package/lib/typescript/src/drivers.android.d.ts.map +0 -1
- package/lib/typescript/src/drivers.d.ts +0 -3
- package/lib/typescript/src/drivers.d.ts.map +0 -1
- package/lib/typescript/src/drivers.ios.d.ts +0 -3
- package/lib/typescript/src/drivers.ios.d.ts.map +0 -1
- package/lib/typescript/src/drivers.native.d.ts +0 -5
- package/lib/typescript/src/drivers.native.d.ts.map +0 -1
- package/lib/typescript/src/groupDriverTypes.d.ts +0 -66
- package/lib/typescript/src/groupDriverTypes.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.android.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.android.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.ios.d.ts +0 -2
- package/lib/typescript/src/groupDrivers.ios.d.ts.map +0 -1
- package/lib/typescript/src/groupDrivers.native.d.ts +0 -4
- package/lib/typescript/src/groupDrivers.native.d.ts.map +0 -1
- package/lib/typescript/src/presentationProtocol.d.ts +0 -4
- package/lib/typescript/src/presentationProtocol.d.ts.map +0 -1
- package/lib/typescript/src/reactRequests.d.ts +0 -9
- package/lib/typescript/src/reactRequests.d.ts.map +0 -1
- package/src/driverState.ts +0 -118
- package/src/driverTypes.ts +0 -171
- package/src/drivers.android.ts +0 -8
- package/src/drivers.ios.ts +0 -8
- package/src/drivers.native.ts +0 -1599
- package/src/drivers.ts +0 -6
- package/src/groupDriverTypes.ts +0 -117
- package/src/groupDrivers.android.ts +0 -1
- package/src/groupDrivers.ios.ts +0 -1
- package/src/groupDrivers.native.ts +0 -1189
- package/src/groupDrivers.ts +0 -1
- package/src/presentationProtocol.ts +0 -28
- package/src/reactRequests.ts +0 -76
package/README.md
CHANGED
|
@@ -1,449 +1,198 @@
|
|
|
1
1
|
# react-native-smooth-clip-view
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Layout-free animated rounded clipping for React Native Fabric.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
`SmoothClipView` is a fixed viewport. Its controller moves a rounded aperture
|
|
6
|
+
inside that viewport using raw host-local coordinates. Content is clipped by the
|
|
7
|
+
aperture, an optional outset shadow may extend outside the aperture, and the host
|
|
8
|
+
finally crops both. A full-screen host therefore accepts screen coordinates
|
|
9
|
+
directly, including negative and off-screen frames.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Use this library when a transition needs streamed gesture updates followed by
|
|
12
|
+
native-owned timing or spring motion without animating Yoga layout. A regular
|
|
13
|
+
Reanimated `View` with `overflow: 'hidden'` remains the simpler choice when
|
|
14
|
+
layout-thread work and interruption snapshots are not concerns.
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
## Demo
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
https://github.com/user-attachments/assets/899235b3-de69-46d7-b6db-61bc54d80df8
|
|
16
19
|
|
|
17
20
|
## Requirements
|
|
18
21
|
|
|
19
|
-
- React Native 0.86 or newer with the New Architecture
|
|
22
|
+
- React Native 0.86 or newer with the New Architecture
|
|
23
|
+
- React 19.2 or newer
|
|
20
24
|
- React Native Reanimated 4.5 or newer
|
|
21
|
-
- React Native Worklets 0.10 or newer
|
|
22
|
-
-
|
|
23
|
-
- Android API 33 or newer
|
|
24
|
-
|
|
25
|
-
The package supports iOS and Android. React Native Web and the legacy Paper
|
|
26
|
-
architecture are not supported; keep imports and rendered usage behind native
|
|
27
|
-
platform boundaries.
|
|
28
|
-
|
|
29
|
-
## Installation
|
|
30
|
-
|
|
31
|
-
Available from [npm](https://www.npmjs.com/package/react-native-smooth-clip-view):
|
|
25
|
+
- React Native Worklets 0.10.1 or newer
|
|
26
|
+
- Android API 24 or newer
|
|
32
27
|
|
|
33
28
|
```sh
|
|
34
29
|
npm install react-native-smooth-clip-view react-native-reanimated react-native-worklets
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
For iOS, install pods after adding the package:
|
|
38
|
-
|
|
39
|
-
```sh
|
|
40
30
|
cd ios && pod install
|
|
41
31
|
```
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
Expo SDK 57 configures the required Babel plugin through `babel-preset-expo`.
|
|
45
|
-
|
|
46
|
-
## Usage
|
|
33
|
+
## One clip
|
|
47
34
|
|
|
48
35
|
```tsx
|
|
49
|
-
import { useState } from 'react';
|
|
50
|
-
import { Button, StyleSheet, View } from 'react-native';
|
|
51
36
|
import {
|
|
52
|
-
|
|
37
|
+
ClipEasings,
|
|
53
38
|
SmoothClipView,
|
|
54
|
-
|
|
55
|
-
useSmoothClipDriver,
|
|
39
|
+
useSmoothClipController,
|
|
56
40
|
} from 'react-native-smooth-clip-view';
|
|
57
41
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export function ClipExample() {
|
|
67
|
-
const [expanded, setExpanded] = useState(false);
|
|
68
|
-
const driver = useSmoothClipDriver(
|
|
69
|
-
createClipPresentation(initialClip, -initialClip.x, -initialClip.y)
|
|
70
|
-
);
|
|
42
|
+
function Reveal() {
|
|
43
|
+
const clip = useSmoothClipController({
|
|
44
|
+
clip: { x: 24, y: 80, width: 96, height: 96, radius: 24 },
|
|
45
|
+
contentTranslateX: -24,
|
|
46
|
+
contentTranslateY: -80,
|
|
47
|
+
contentScale: 1,
|
|
48
|
+
});
|
|
71
49
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
50
|
+
const open = () => {
|
|
51
|
+
clip.react.animateTo(
|
|
52
|
+
{
|
|
53
|
+
clip: { x: 0, y: 0, width: 390, height: 844, radius: 0 },
|
|
54
|
+
contentTranslateX: 0,
|
|
55
|
+
contentTranslateY: 0,
|
|
56
|
+
contentScale: 1,
|
|
57
|
+
},
|
|
80
58
|
{
|
|
81
59
|
type: 'timing',
|
|
82
|
-
duration:
|
|
83
|
-
controlPoints:
|
|
60
|
+
duration: 400,
|
|
61
|
+
controlPoints: ClipEasings.easeOutCubic,
|
|
84
62
|
}
|
|
85
63
|
);
|
|
86
64
|
};
|
|
87
65
|
|
|
88
66
|
return (
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</SmoothClipView>
|
|
93
|
-
<Button title="Toggle clip" onPress={toggle} />
|
|
94
|
-
</View>
|
|
67
|
+
<SmoothClipView controller={clip} style={{ flex: 1 }}>
|
|
68
|
+
{/* screen-sized transition content */}
|
|
69
|
+
</SmoothClipView>
|
|
95
70
|
);
|
|
96
71
|
}
|
|
97
|
-
|
|
98
|
-
const styles = StyleSheet.create({
|
|
99
|
-
host: { width: 320, height: 480 },
|
|
100
|
-
content: { width: 320, height: 480, backgroundColor: '#112743' },
|
|
101
|
-
});
|
|
102
72
|
```
|
|
103
73
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
centered content scale. Interactive updates avoid Yoga and ShadowTree commits.
|
|
74
|
+
One controller may have one mounted host at a time. Sequential unmount and
|
|
75
|
+
remount is supported. In development, mounting the same controller in two hosts
|
|
76
|
+
throws an error.
|
|
108
77
|
|
|
109
|
-
|
|
110
|
-
render server:
|
|
78
|
+
## Worklet API
|
|
111
79
|
|
|
112
|
-
|
|
113
|
-
const expandFromReact = () =>
|
|
114
|
-
driver.react.animateTo(
|
|
115
|
-
createClipPresentation(
|
|
116
|
-
{ x: 0, y: 0, width: 320, height: 480, radius: 24 },
|
|
117
|
-
0,
|
|
118
|
-
0
|
|
119
|
-
),
|
|
120
|
-
{
|
|
121
|
-
type: 'timing',
|
|
122
|
-
duration: 450,
|
|
123
|
-
controlPoints: [0.42, 0, 0.58, 1],
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
```
|
|
80
|
+
Call `clip.ui` methods from a UI-runtime worklet:
|
|
127
81
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
translation as one presentation. Android runs one C++ frame-loop driven by the
|
|
131
|
-
vsync clock with registry fanout; outline calculation and invalidation still
|
|
132
|
-
run on its UI thread. Springs are physically integrated per geometry channel
|
|
133
|
-
and finish when they settle below sub-pixel thresholds on both platforms.
|
|
134
|
-
Retargeting starts from visible state.
|
|
135
|
-
|
|
136
|
-
Blocked-thread behavior: a blocked JS thread never stalls a running
|
|
137
|
-
transition on either platform — no JS runs on the frame path. A blocked
|
|
138
|
-
main thread stalls it on Android (the frame loop and the View property
|
|
139
|
-
writes are main-thread-only by platform design; the RenderThread only
|
|
140
|
-
replays what the main thread records) but not on iOS, where the render
|
|
141
|
-
server advances installed Core Animations out of process. The Android
|
|
142
|
-
behavior is not recoverable through public APIs — RenderThread-driven
|
|
143
|
-
property animation (`RenderNodeAnimator`, what ripples use) is hidden API —
|
|
144
|
-
and it is also the coherent choice for this library: parallel Reanimated
|
|
145
|
-
content runs on the main thread on both platforms, so on Android the clip
|
|
146
|
-
and its content stall and resume together, while on iOS a main-thread stall
|
|
147
|
-
lets the natively animated channels keep moving past any Reanimated-driven
|
|
148
|
-
ones.
|
|
149
|
-
The same driver can be grabbed by a gesture without a visual jump:
|
|
82
|
+
```ts
|
|
83
|
+
clip.ui.setFrame(presentation);
|
|
150
84
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const visible = driver.ui.beginInteraction();
|
|
155
|
-
dragStart.value = visible.clip.height;
|
|
156
|
-
})
|
|
157
|
-
.onUpdate((event) => {
|
|
158
|
-
const clip = geometryForDrag(dragStart.value, event.translationY);
|
|
159
|
-
// Per-frame hot path: writes straight to native without SharedValue
|
|
160
|
-
// bookkeeping. Assigning driver.presentation.value also works.
|
|
161
|
-
driver.ui.setScalars(clip.x, clip.y, clip.width, clip.height, clip.radius, 0, 0);
|
|
162
|
-
})
|
|
163
|
-
.onEnd((event) => {
|
|
164
|
-
// The release event is fresher than the last onUpdate (on Android,
|
|
165
|
-
// ACTION_UP carries a position no MOVE ever delivered). Pass the final
|
|
166
|
-
// geometry as `from` so the animation starts from exactly that value —
|
|
167
|
-
// it fuses a setScalars hot write with the handoff in one call.
|
|
168
|
-
const clip = geometryForDrag(dragStart.value, event.translationY);
|
|
169
|
-
// 'inherit' projects launch speed from the drag's last two samples. On
|
|
170
|
-
// Android that sampling is opt-in — create the driver with
|
|
171
|
-
// useSmoothClipDriver(initial, { velocityTracking: true }).
|
|
172
|
-
driver.ui.animateTo(createClipPresentation(expandedClip), {
|
|
173
|
-
type: 'spring',
|
|
174
|
-
initialVelocity: 'inherit',
|
|
175
|
-
from: createClipPresentation(clip),
|
|
176
|
-
});
|
|
177
|
-
});
|
|
85
|
+
const run = clip.ui.animateTo(target, spring, 1);
|
|
86
|
+
|
|
87
|
+
clip.ui.cancel(run); // optional; freezes at the visible frame
|
|
178
88
|
```
|
|
179
89
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
`SmoothClipViewProps` extends React Native `ViewProps` and adds:
|
|
185
|
-
|
|
186
|
-
| Prop | Type | Description |
|
|
187
|
-
| ---------- | ------------------ | --------------------------------------- |
|
|
188
|
-
| `driver` | `SmoothClipDriver` | Reusable hybrid clip driver. |
|
|
189
|
-
| `children` | `ReactNode` | Content rendered inside the fixed host. |
|
|
190
|
-
|
|
191
|
-
The host hides itself, drops out of the accessibility tree, and stops accepting
|
|
192
|
-
touches while the clip is empty. The two platforms draw the boundary where they
|
|
193
|
-
actually stop rendering, which differs below one pixel: Android emits an integer
|
|
194
|
-
`Outline`, which collapses to nothing under half a physical pixel, so an extent
|
|
195
|
-
in `(0, 0.5)` px counts as empty there; iOS masks in floats and treats only a
|
|
196
|
-
zero-or-negative extent as empty. A clip animating through that band therefore
|
|
197
|
-
turns non-interactive one frame earlier on Android — matching what each platform
|
|
198
|
-
puts on screen, which is the property worth keeping identical.
|
|
199
|
-
|
|
200
|
-
### Driver
|
|
201
|
-
|
|
202
|
-
- `useSmoothClipDriver(initialPresentation, options)` returns one hybrid driver
|
|
203
|
-
whose writable `presentation` SharedValue contains clip geometry and content
|
|
204
|
-
translation. Passing a `ClipGeometry` remains supported and initializes both
|
|
205
|
-
translations to zero.
|
|
206
|
-
- `driver.ui` is the synchronous UI-worklet interface. `beginInteraction()`
|
|
207
|
-
freezes a transition at visible presentation state; `set()`, `animateTo()`,
|
|
208
|
-
and `cancel()` change ownership atomically.
|
|
209
|
-
- `driver.ui.setScalars(x, y, width, height, radius, tx, ty)` is the per-frame
|
|
210
|
-
hot path: it writes geometry straight to native without touching
|
|
211
|
-
`driver.presentation`, skipping SharedValue bookkeeping for high-frequency
|
|
212
|
-
streams such as gestures. `driver.presentation.value` is stale after hot
|
|
213
|
-
writes by design; `beginInteraction()` remains the source of truth for
|
|
214
|
-
visible geometry, and `animateTo()` after hot writes starts from the native
|
|
215
|
-
registry's latest value rather than the stale SharedValue. To start from a
|
|
216
|
-
value fresher than the last hot write (a gesture's release sample), pass it
|
|
217
|
-
as `animation.from` — `animateTo` then performs the hot write and the
|
|
218
|
-
handoff in one call. Do not interleave `setScalars` with
|
|
219
|
-
`presentation.value` writes on the same driver.
|
|
220
|
-
- `driver.ui.setPresentationScalars(x, y, width, height, topLeft, topRight,
|
|
221
|
-
bottomRight, bottomLeft, curveCode, tx, ty, scale)` is the V2 hot path.
|
|
222
|
-
`curveCode` is `0` for circular and `1` for continuous. V2 values are
|
|
223
|
-
validated as one transaction; widened values are never silently downgraded
|
|
224
|
-
onto a V1 native binary.
|
|
225
|
-
- Spring `initialVelocity` is one normalized scalar along the current-to-target
|
|
226
|
-
trajectory, in units of the remaining distance per second (`1` covers the
|
|
227
|
-
remaining distance in one second). Every geometry channel continues with the
|
|
228
|
-
same normalized rate, so grab/release preserves the felt direction and
|
|
229
|
-
speed. `'inherit'` (the default) estimates the scalar from the last two
|
|
230
|
-
interactive samples on iOS and Android. On Android, sampling on the
|
|
231
|
-
`setScalars` hot path is **opt-in**: pass
|
|
232
|
-
`velocityTracking: true` to `useSmoothClipDriver` or `'inherit'` inherits
|
|
233
|
-
zero after a hot-write drag (a dev-mode warning flags this). **Behavior
|
|
234
|
-
change:** earlier releases always recorded on Android, so an existing
|
|
235
|
-
`setScalars`-drag → inherit-spring handoff keeps its momentum only after
|
|
236
|
-
adding the flag. The recording
|
|
237
|
-
is a clock read plus channel copies on every per-frame write, so drivers
|
|
238
|
-
that never hand off into an inherit spring skip it by default. iOS always
|
|
239
|
-
records, and Android's declarative `presentation.value` channel always
|
|
240
|
-
records too. How long the finger has been still since that last sample scales the
|
|
241
|
-
result: full credit for one frame (16.7 ms), then a linear decay to zero at
|
|
242
|
-
100 ms. A release straight out of a drag is therefore untouched, and holding
|
|
243
|
-
still before releasing bleeds the momentum off smoothly instead of keeping
|
|
244
|
-
all of it until 99 ms and none at 101 ms. Two writes landing inside the same frame
|
|
245
|
-
(< 4 ms apart, e.g. a release-sample `from` seed right after the last drag
|
|
246
|
-
write) coalesce into one sample, and an identical re-write is ignored, so a
|
|
247
|
-
fused handoff can neither zero nor inflate the inherited velocity.
|
|
248
|
-
Only interactive writes contribute samples. Internal freeze/join/resume and
|
|
249
|
-
static-finalization writes do not, so grabbing a native animation cannot
|
|
250
|
-
manufacture velocity for a later `'inherit'` spring; a subsequent drag (or
|
|
251
|
-
explicit `animation.from`) supplies the release samples instead.
|
|
252
|
-
- `driver.react` exposes `beginInteraction`, `set`, `animateTo`, and `cancel`
|
|
253
|
-
as Promises (`setScalars` is UI-worklet-only). React code never blocks
|
|
254
|
-
waiting for main/UI-thread work. An immediate animation request resolves
|
|
255
|
-
before its completion callback is delivered.
|
|
256
|
-
- `animateTo()` transfers ownership to native animation. Timing uses
|
|
257
|
-
cubic Bézier control points — `ClipEasings` exports exact-form presets
|
|
258
|
-
(`easeOutCubic` = `Easing.out(Easing.cubic)` etc.) so a parallel Reanimated
|
|
259
|
-
animation can run the identical curve without hand-deriving it; springs
|
|
260
|
-
accept mass, stiffness, damping, and an
|
|
261
|
-
explicit normalized velocity or `'inherit'` (the default). Keyframes accept
|
|
262
|
-
validated, monotonically increasing offsets from zero through one; there is
|
|
263
|
-
deliberately no keyframe easing field — playback is linear between offsets
|
|
264
|
-
and the frames encode the curve, which also expresses per-channel-nonlinear
|
|
265
|
-
paths that no single time-warp could reproduce. Every
|
|
266
|
-
kind accepts an optional `from` presentation — a fused take-ownership hot
|
|
267
|
-
write issued immediately before the handoff, so the animation starts from
|
|
268
|
-
exactly that value (pass `frames[0].presentation` for keyframes, which
|
|
269
|
-
interpolate absolutely). A non-finite `from` rejects the whole call; against
|
|
270
|
-
a held pending-animation latch, explicit `from` is the newer intent: it
|
|
271
|
-
cancels that latch once with `finished: false`, records/applies `from`, then
|
|
272
|
-
starts the replacement from that native value. Passive hook seeds and public
|
|
273
|
-
`set`/`setScalars` writes still leave a held latch intact. `from` behaves the
|
|
274
|
-
same on both platforms (it is driver-layer, not native): on iOS the seed
|
|
275
|
-
stops any running Core Animation, applies `from` to the model layer, and
|
|
276
|
-
installs timing, spring, or keyframe playback from that exact presentation in
|
|
277
|
-
the same start transaction.
|
|
278
|
-
- `cancel()` freezes visible presentation by default. Pass `'target'` as its
|
|
279
|
-
behavior to jump to the requested endpoint.
|
|
280
|
-
- `options.onAnimationComplete` fires exactly once per animation with its ID
|
|
281
|
-
and `finished` state, including cancellation, replacement, and native-side
|
|
282
|
-
rejection (`animateTo` then returns a fresh non-zero id whose single
|
|
283
|
-
`finished: false` completion follows — key completion handling by the
|
|
284
|
-
returned id, never by `0`). A valid pre-registration request carries its
|
|
285
|
-
authoritative interactive start, creates the missing driver state and
|
|
286
|
-
returns a real id. `0` — with no completion — is reserved for off-main,
|
|
287
|
-
invalid-id, or otherwise unsupported dispatch: a missing-state native request
|
|
288
|
-
with no authoritative start, any `driver.ui.animateTo` issued after the
|
|
289
|
-
driver's hook has unmounted, and an invalid-parameter request issued before
|
|
290
|
-
the driver's first seed reached native (validation rejections mint their
|
|
291
|
-
`finished: false` completion only once the native entry exists). (The
|
|
292
|
-
post-unmount case is decided on the UI runtime,
|
|
293
|
-
because a destroyed driver and a not-yet-seeded one are the same missing
|
|
294
|
-
registry entry to native — accepting it would build a latch nothing can start
|
|
295
|
-
and nothing can cancel.) A host is displayable only while it is attached,
|
|
296
|
-
foreground/window-visible, laid out, and positive-sized (the window's own
|
|
297
|
-
`hidden`/scene state is not consulted — RN's single always-visible window
|
|
298
|
-
makes it moot). Losing the last
|
|
299
|
-
displayable host mid-flight — including temporary detach, zero-size layout,
|
|
300
|
-
or app background — freezes and re-latches the exact remainder instead of
|
|
301
|
-
consuming duration offscreen. Foreground/reattach resumes the same animation
|
|
302
|
-
ID from that stored timing/keyframe phase or spring state.
|
|
303
|
-
Parallel Reanimated clocks do **not** pause with it: a `withTiming` started
|
|
304
|
-
beside `animateTo` keeps its wall-clock start, so after backgrounding it
|
|
305
|
-
completes on its first resumed frame while the native run still animates its
|
|
306
|
-
preserved remainder. Key teardown and state transitions off
|
|
307
|
-
`onAnimationComplete` (or re-synchronize on `AppState`) rather than off a
|
|
308
|
-
duration-matched Reanimated callback.
|
|
309
|
-
With multiple hosts on one driver, a host becomes an installed participant
|
|
310
|
-
only after its native animation starts. Temporary loss moves it to suspended;
|
|
311
|
-
a rejoin restores active participation. Unregistering an installed/suspended
|
|
312
|
-
host, or reaching completion while it remains suspended, makes the eventual
|
|
313
|
-
completion `finished: false`. A host that stayed deferred because it was
|
|
314
|
-
detached, unlaid-out, or zero-sized never poisons completion. `finished: true`
|
|
315
|
-
means every installed participant either ran to the end or rejoined and did so.
|
|
316
|
-
- An `animateTo` issued before any host view can produce a visible frame (for
|
|
317
|
-
example from an effect in the same commit that mounts the host, or inside a
|
|
318
|
-
modal route whose subtree attaches to its window late) is held pending and
|
|
319
|
-
starts with its full duration at the first moment a registered host can
|
|
320
|
-
produce a frame — positive layout, window attach/visibility, and foreground
|
|
321
|
-
state must all be present. This also covers an animation worklet that runs before the
|
|
322
|
-
hook's seed worklet: the animation creates the state and the later passive
|
|
323
|
-
seed cannot reset its ownership or active id. A pending animation owns the
|
|
324
|
-
driver: ordinary take-ownership writes (`set`, `setScalars`, the hook's seed)
|
|
325
|
-
are dropped while it is held. Replace it with another `animateTo`, override
|
|
326
|
-
it with an explicit `animation.from`, or cancel it via `beginInteraction()`
|
|
327
|
-
or `cancel()`. If no view ever becomes displayable, it survives until it is
|
|
328
|
-
replaced, cancelled, or the driver is destroyed — at which point its single
|
|
329
|
-
`finished: false` completion is delivered.
|
|
330
|
-
|
|
331
|
-
Do not call `driver.ui` from React code — it throws on the React runtime; use
|
|
332
|
-
`driver.react` there. During a native transition, `driver.presentation.value`
|
|
333
|
-
is the requested target, not a per-frame mirror of the native presentation
|
|
334
|
-
layer. `beginInteraction()` returns geometry normalized against the host
|
|
335
|
-
bounds, so a clip that extended beyond the host comes back clamped. Start
|
|
336
|
-
gestures with `beginInteraction()`: interactive writes issued while native
|
|
337
|
-
still owns rendering are dropped.
|
|
338
|
-
|
|
339
|
-
### Group driver
|
|
340
|
-
|
|
341
|
-
`useSmoothClipGroupDriver({ reduceMotion, onAnimationComplete })` coordinates
|
|
342
|
-
multiple drivers with one native group ID and one completion. Its worklet-safe
|
|
343
|
-
`ui` and Promise-based `react` interfaces expose:
|
|
344
|
-
|
|
345
|
-
- `beginInteraction(drivers)` to freeze every overlapping group atomically and
|
|
346
|
-
return canonical visible snapshots in input order.
|
|
347
|
-
- `snapshotCurrent(drivers)` to sample presentation and readiness without
|
|
348
|
-
changing ownership.
|
|
349
|
-
- `setBatch(entries)` to validate every entry before committing one native
|
|
350
|
-
transaction.
|
|
351
|
-
- `animateTo(entries, animation)` for shared timing, spring, or linear
|
|
352
|
-
keyframe progress. Membership is immutable for that group ID; a replacement
|
|
353
|
-
is a new atomically installed group.
|
|
354
|
-
- `cancel(groupId, 'freeze' | 'finish')` to return every participant snapshot.
|
|
355
|
-
|
|
356
|
-
Groups wait until all participants have a visible, attached, laid-out,
|
|
357
|
-
positive-size host. The default `suspensionPolicy: 'pause'` freezes and
|
|
358
|
-
re-latches the whole group if any participant loses readiness; `'finish'`
|
|
359
|
-
applies every target. Complex-path native settlement is capability-gated—use
|
|
360
|
-
`getSmoothClipCapabilities()` and keep streaming with `setBatch` when
|
|
361
|
-
`autonomousComplexPathAnimation` is false.
|
|
362
|
-
|
|
363
|
-
### `SmoothClipPresentation`
|
|
90
|
+
Native terminal events are delivered on the React JavaScript runtime through
|
|
91
|
+
one stable controller callback. Consumer callbacks are never retained in the
|
|
92
|
+
UI runtime:
|
|
364
93
|
|
|
365
94
|
```ts
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
95
|
+
const clip = useSmoothClipController(initialPresentation, {
|
|
96
|
+
onAnimationComplete(result) {
|
|
97
|
+
// result.completionTag is 1 for the tagged run above
|
|
98
|
+
// result.finished is true only when the target was reached
|
|
99
|
+
},
|
|
100
|
+
});
|
|
372
101
|
```
|
|
373
102
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
animate clip position, bounds, radius, and content translation together.
|
|
103
|
+
The optional completion tag is a nonnegative 32-bit integer. Use it to identify
|
|
104
|
+
UI-runtime runs without retaining a consumer worklet for the animation lifetime.
|
|
377
105
|
|
|
378
|
-
|
|
106
|
+
`setFrame` canonicalizes the object in the worklet and makes one scalar JSI call.
|
|
107
|
+
It does not update React state, animated props, Yoga, the ShadowTree, or a public
|
|
108
|
+
SharedValue.
|
|
379
109
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
y: number;
|
|
384
|
-
width: number;
|
|
385
|
-
height: number;
|
|
386
|
-
radius: number;
|
|
387
|
-
topLeftRadius?: number;
|
|
388
|
-
topRightRadius?: number;
|
|
389
|
-
bottomRightRadius?: number;
|
|
390
|
-
bottomLeftRadius?: number;
|
|
391
|
-
curve?: 'circular' | 'continuous';
|
|
392
|
-
}>;
|
|
393
|
-
```
|
|
110
|
+
`beginInteraction()` atomically interrupts native motion and returns its visible
|
|
111
|
+
raw presentation. Apply the final gesture sample with `setFrame()` before calling
|
|
112
|
+
it when the release must start from that exact frame.
|
|
394
113
|
|
|
395
|
-
|
|
396
|
-
intersects the requested rectangle with the actual host bounds, prevents
|
|
397
|
-
negative sizes, and proportionally scales overlapping corners with the CSS
|
|
398
|
-
corner-normalization rule. Content scale is centered on the native content
|
|
399
|
-
container; translation is independent and is not multiplied by scale.
|
|
114
|
+
## React API
|
|
400
115
|
|
|
401
|
-
|
|
116
|
+
React-thread animation starts synchronously return a run object:
|
|
402
117
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
118
|
+
```ts
|
|
119
|
+
const run = clip.react.animateTo(target, {
|
|
120
|
+
type: 'spring',
|
|
121
|
+
stiffness: 180,
|
|
122
|
+
damping: 22,
|
|
123
|
+
velocity: 1.4,
|
|
124
|
+
});
|
|
406
125
|
|
|
407
|
-
|
|
126
|
+
// run.cancel(); // optional; freezes the visible frame and resolves false
|
|
127
|
+
const finished = await run.finished;
|
|
128
|
+
```
|
|
408
129
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
`
|
|
414
|
-
|
|
130
|
+
Timing accepts `duration` and cubic-Bezier `controlPoints`. Spring accepts
|
|
131
|
+
optional `mass`, `stiffness`, `damping`, normalized `velocity`, and
|
|
132
|
+
`energyThreshold`. Both specifications accept `reduceMotion`. The physical
|
|
133
|
+
defaults match Reanimated 4.5: mass `4`, stiffness `900`, damping `120`, and
|
|
134
|
+
relative energy threshold `6e-9`. A spring is rejected when its resolved native
|
|
135
|
+
trajectory could make `contentScale` nonpositive.
|
|
415
136
|
|
|
416
|
-
|
|
137
|
+
An animation requested before the host has a positive layout waits, then starts
|
|
138
|
+
with its full duration. Host loss after motion starts freezes every member and
|
|
139
|
+
resolves the transaction `false`. Replacement, cancellation, destruction, and
|
|
140
|
+
rejection also settle once with `false`.
|
|
417
141
|
|
|
418
|
-
|
|
419
|
-
Yoga layout.
|
|
420
|
-
- Put visual backgrounds inside `SmoothClipView`.
|
|
421
|
-
- Keep borders, shadows, rotation, and anisotropic transforms on an outer
|
|
422
|
-
visual carrier rather than the clip host.
|
|
423
|
-
- Uniform circular corners use platform fast paths. Unequal or continuous
|
|
424
|
-
corners use a fixed-topology portable path; Android intentionally does not
|
|
425
|
-
claim pixel identity with Apple's proprietary continuous curve.
|
|
142
|
+
## Atomic groups
|
|
426
143
|
|
|
427
|
-
|
|
144
|
+
Store `controller.ref` when multiple clips must update or settle atomically:
|
|
428
145
|
|
|
429
|
-
|
|
430
|
-
|
|
146
|
+
```ts
|
|
147
|
+
const group = useSmoothClipGroup();
|
|
431
148
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
```
|
|
149
|
+
group.ui.setFrames([
|
|
150
|
+
{ clip: first.ref, frame: firstFrame },
|
|
151
|
+
{ clip: second.ref, frame: secondFrame },
|
|
152
|
+
]);
|
|
437
153
|
|
|
438
|
-
|
|
154
|
+
const snapshots = group.ui.beginInteraction([first.ref, second.ref]);
|
|
439
155
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
156
|
+
const run = group.react.animateTo(
|
|
157
|
+
[
|
|
158
|
+
{ clip: first.ref, target: firstTarget },
|
|
159
|
+
{ clip: second.ref, target: secondTarget },
|
|
160
|
+
],
|
|
161
|
+
{ type: 'timing', duration: 320, controlPoints: ClipEasings.easeOutCubic }
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const result = await run.finished;
|
|
445
165
|
```
|
|
446
166
|
|
|
167
|
+
`group.ui.setFrames` uses one native batch call. `beginInteraction` snapshots and
|
|
168
|
+
cancel snapshots preserve input order and report whether each clip currently has
|
|
169
|
+
a displayable host. Controllers are implemented as one-member groups, so both
|
|
170
|
+
APIs share validation, run ownership, and completion behavior.
|
|
171
|
+
|
|
172
|
+
## Geometry and rendering
|
|
173
|
+
|
|
174
|
+
- `x` and `y` are never intersected with the host.
|
|
175
|
+
- Negative width and height canonicalize to zero.
|
|
176
|
+
- Corner-overlap scaling follows CSS rules against the requested rectangle.
|
|
177
|
+
- `contentTranslateX`, `contentTranslateY`, and positive `contentScale` animate
|
|
178
|
+
with the aperture.
|
|
179
|
+
- Circular and continuous curves and independent corner radii are supported.
|
|
180
|
+
- One outset `boxShadow` is supported. It escapes the aperture but not the host.
|
|
181
|
+
- A fully off-host aperture is not touchable, even when only its shadow overlaps.
|
|
182
|
+
- Descendant accessibility is hidden during autonomous native motion and restored
|
|
183
|
+
from aperture/host intersection at the endpoint.
|
|
184
|
+
|
|
185
|
+
Use `getSmoothClipCapabilities()` when a consumer needs to decide whether a
|
|
186
|
+
complex native path can be promoted on the current platform.
|
|
187
|
+
|
|
188
|
+
## Performance contract
|
|
189
|
+
|
|
190
|
+
- One worklet-to-native call per `ui.setFrame`, or per group batch.
|
|
191
|
+
- Native timing and springs run without JS work between start and completion.
|
|
192
|
+
- The shadow-disabled rendering path keeps no shadow drawing resources.
|
|
193
|
+
- The fixed host is the maximum rendering viewport; consumers should size it to
|
|
194
|
+
the region in which content and shadow may appear.
|
|
195
|
+
|
|
447
196
|
## License
|
|
448
197
|
|
|
449
198
|
MIT
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
buildscript {
|
|
2
2
|
ext.SmoothClipView = [
|
|
3
3
|
kotlinVersion: "2.0.21",
|
|
4
|
-
minSdkVersion:
|
|
4
|
+
minSdkVersion: 24,
|
|
5
5
|
compileSdkVersion: 36,
|
|
6
6
|
targetSdkVersion: 36
|
|
7
7
|
]
|
|
@@ -76,14 +76,11 @@ android {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
defaultConfig {
|
|
79
|
-
//
|
|
80
|
-
// Keep this as a hard library floor: allowing a consuming root project to
|
|
81
|
-
// override it lower would make the V2 per-corner/continuous path silently
|
|
82
|
-
// unsupported at runtime.
|
|
79
|
+
// API 24-32 clip child drawing directly; API 33+ use path outlines.
|
|
83
80
|
minSdkVersion SmoothClipView.minSdkVersion
|
|
84
81
|
targetSdkVersion getExtOrDefault("targetSdkVersion")
|
|
85
|
-
// C++ resolves SmoothClipView by name and calls
|
|
86
|
-
//
|
|
82
|
+
// C++ resolves SmoothClipView by name and calls its presentation setters via
|
|
83
|
+
// cached jmethodIDs; consumer minification must not rename or strip them.
|
|
87
84
|
consumerProguardFiles "proguard-rules.pro"
|
|
88
85
|
|
|
89
86
|
externalNativeBuild {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SmoothClipView is resolved from C++ by name (fbjni findClassStatic) and its
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
# presentation setters are invoked through cached jmethodIDs;
|
|
3
|
+
# SmoothClipBindings registers native methods by name. None
|
|
4
4
|
# of them may be renamed or stripped in consumer release builds.
|
|
5
5
|
-keep class com.smoothclipview.SmoothClipView { *; }
|
|
6
6
|
-keep class com.smoothclipview.SmoothClipBindings { *; }
|