react-native-swipeable-actions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +250 -0
- package/android/build.gradle +99 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeConstants.kt +29 -0
- package/android/src/main/java/expo/modules/swipeable/SwipePhysics.kt +66 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeableModule.kt +69 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeableView.kt +951 -0
- package/build/Swipeable.d.ts +10 -0
- package/build/Swipeable.d.ts.map +1 -0
- package/build/Swipeable.js +114 -0
- package/build/Swipeable.js.map +1 -0
- package/build/Swipeable.types.d.ts +64 -0
- package/build/Swipeable.types.d.ts.map +1 -0
- package/build/Swipeable.types.js +3 -0
- package/build/Swipeable.types.js.map +1 -0
- package/build/SwipeableActions.d.ts +17 -0
- package/build/SwipeableActions.d.ts.map +1 -0
- package/build/SwipeableActions.js +31 -0
- package/build/SwipeableActions.js.map +1 -0
- package/build/SwipeableView.d.ts +10 -0
- package/build/SwipeableView.d.ts.map +1 -0
- package/build/SwipeableView.js +11 -0
- package/build/SwipeableView.js.map +1 -0
- package/build/constants.d.ts +11 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +14 -0
- package/build/constants.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +21 -0
- package/build/index.js.map +1 -0
- package/build/nativeModuleUtils.d.ts +11 -0
- package/build/nativeModuleUtils.d.ts.map +1 -0
- package/build/nativeModuleUtils.js +49 -0
- package/build/nativeModuleUtils.js.map +1 -0
- package/build/swipeStateUtils.d.ts +16 -0
- package/build/swipeStateUtils.d.ts.map +1 -0
- package/build/swipeStateUtils.js +37 -0
- package/build/swipeStateUtils.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Package.swift +28 -0
- package/ios/SwipeAnimationConfig.swift +27 -0
- package/ios/SwipePhysics.swift +66 -0
- package/ios/SwipeableModule.swift +84 -0
- package/ios/SwipeableView.swift +845 -0
- package/ios/react-native-swipeable-actions.podspec +22 -0
- package/package.json +85 -0
- package/src/Swipeable.tsx +195 -0
- package/src/Swipeable.types.ts +70 -0
- package/src/SwipeableActions.tsx +41 -0
- package/src/SwipeableView.tsx +20 -0
- package/src/constants.ts +11 -0
- package/src/index.ts +22 -0
- package/src/nativeModuleUtils.ts +57 -0
- package/src/swipeStateUtils.ts +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# react-native-swipeable-actions
|
|
2
|
+
|
|
3
|
+
A high-performance native swipeable row component for React Native. Native alternative to `react-native-gesture-handler/Swipeable` - no `react-native-reanimated` required.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Pure native gestures - no JS thread blocking
|
|
8
|
+
- 60fps spring animations on both iOS and Android
|
|
9
|
+
- No `react-native-reanimated` dependency
|
|
10
|
+
- Built-in FlashList/virtualized list support via `recyclingKey`
|
|
11
|
+
- AutoClose pattern for chat-style swipe-to-reply
|
|
12
|
+
- Lazy action rendering for optimal performance
|
|
13
|
+
|
|
14
|
+
## Performance
|
|
15
|
+
|
|
16
|
+
Benchmark comparison on Android 16, Pixel 8 Pro, GrapheneOS — List Demo with 100 items:
|
|
17
|
+
|
|
18
|
+
<table>
|
|
19
|
+
<tr>
|
|
20
|
+
<th>react-native-gesture-handler/Swipeable</th>
|
|
21
|
+
<th>react-native-swipeable-actions</th>
|
|
22
|
+
</tr>
|
|
23
|
+
<tr>
|
|
24
|
+
<td><video src="example/assets/rngh.mp4" controls width="300"></video></td>
|
|
25
|
+
<td><video src="example/assets/swipeable.mp4" controls width="300"></video></td>
|
|
26
|
+
</tr>
|
|
27
|
+
<tr>
|
|
28
|
+
<td align="center">Reanimated</td>
|
|
29
|
+
<td align="center">Pure native</td>
|
|
30
|
+
</tr>
|
|
31
|
+
<tr>
|
|
32
|
+
<td align="center"><b>Avg 60.9</b> FPS (Min: 56.7 / Max: 65.7)</td>
|
|
33
|
+
<td align="center"><b>Avg 116.8</b> FPS (Min: 109.9 / Max: 120.3)</td>
|
|
34
|
+
</tr>
|
|
35
|
+
</table>
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install react-native-swipeable-actions
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Peer Dependencies
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"expo": ">=50.0.0",
|
|
48
|
+
"expo-modules-core": ">=1.0.0",
|
|
49
|
+
"react": ">=18.0.0",
|
|
50
|
+
"react-native": ">=0.73.0"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Basic Usage
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { Swipeable, SwipeableMethods } from 'react-native-swipeable-actions'
|
|
58
|
+
import { useRef } from 'react'
|
|
59
|
+
import { View, Text, TouchableOpacity } from 'react-native'
|
|
60
|
+
|
|
61
|
+
function DeleteAction({ onPress }: { onPress: () => void }) {
|
|
62
|
+
return (
|
|
63
|
+
<TouchableOpacity onPress={onPress} style={styles.deleteAction}>
|
|
64
|
+
<Text style={styles.actionText}>Delete</Text>
|
|
65
|
+
</TouchableOpacity>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function MyRow() {
|
|
70
|
+
const swipeableRef = useRef<SwipeableMethods>(null)
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Swipeable
|
|
74
|
+
ref={swipeableRef}
|
|
75
|
+
actions={<DeleteAction onPress={() => console.log('Delete!')} />}
|
|
76
|
+
actionsWidth={80}
|
|
77
|
+
onSwipeEnd={(state) => console.log('Swipe ended:', state)}
|
|
78
|
+
>
|
|
79
|
+
<View style={styles.row}>
|
|
80
|
+
<Text>Swipe me left</Text>
|
|
81
|
+
</View>
|
|
82
|
+
</Swipeable>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Props
|
|
88
|
+
|
|
89
|
+
| Prop | Type | Default | Description |
|
|
90
|
+
|------|------|---------|-------------|
|
|
91
|
+
| `children` | `ReactNode` | required | Row content |
|
|
92
|
+
| `actions` | `ReactNode` | required | Action buttons revealed on swipe |
|
|
93
|
+
| `actionsWidth` | `number` | required | Width of actions container in pixels |
|
|
94
|
+
| `actionsPosition` | `'left' \| 'right'` | `'right'` | Position of actions (`'left'` = swipe right to reveal) |
|
|
95
|
+
| `friction` | `number` | `1` | Drag damping factor (0-1). Lower = more resistance |
|
|
96
|
+
| `threshold` | `number` | `0.4` | Snap-to threshold as percentage of actionsWidth (0-1) |
|
|
97
|
+
| `dragOffsetFromEdge` | `number` | `0` | Minimum drag distance before gesture starts |
|
|
98
|
+
| `autoClose` | `boolean` | `false` | Auto-close after swipe release (for swipe-to-reply) |
|
|
99
|
+
| `autoCloseTimeout` | `number` | `0` | Delay in ms before auto-closing |
|
|
100
|
+
| `recyclingKey` | `string \| number` | - | Unique key for FlashList/recycling support |
|
|
101
|
+
| `style` | `StyleProp<ViewStyle>` | - | Container style |
|
|
102
|
+
| `testID` | `string` | - | Test ID for e2e testing |
|
|
103
|
+
|
|
104
|
+
## Callbacks
|
|
105
|
+
|
|
106
|
+
| Callback | Type | Description |
|
|
107
|
+
|----------|------|-------------|
|
|
108
|
+
| `onSwipeStart` | `() => void` | Called when swipe gesture begins |
|
|
109
|
+
| `onSwipeStateChange` | `(state: 'open' \| 'closed') => void` | Called when gesture ends (before animation) |
|
|
110
|
+
| `onSwipeEnd` | `(state: 'open' \| 'closed') => void` | Called when animation completes |
|
|
111
|
+
| `onProgress` | `(progress: number) => void` | Called on each frame (0 = closed, 1 = fully open) |
|
|
112
|
+
|
|
113
|
+
## Ref Methods
|
|
114
|
+
|
|
115
|
+
Access via ref:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const swipeableRef = useRef<SwipeableMethods>(null)
|
|
119
|
+
|
|
120
|
+
// Close the row (with optional animation)
|
|
121
|
+
swipeableRef.current?.close() // animated
|
|
122
|
+
swipeableRef.current?.close(false) // instant
|
|
123
|
+
|
|
124
|
+
// Open the row
|
|
125
|
+
swipeableRef.current?.open()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Static Methods
|
|
129
|
+
|
|
130
|
+
Control swipeables globally by their `recyclingKey`:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { Swipeable } from 'react-native-swipeable-actions'
|
|
134
|
+
|
|
135
|
+
// Open a specific row
|
|
136
|
+
Swipeable.open('row-1')
|
|
137
|
+
|
|
138
|
+
// Close a specific row
|
|
139
|
+
Swipeable.close('row-1') // animated
|
|
140
|
+
Swipeable.close('row-1', false) // instant
|
|
141
|
+
|
|
142
|
+
// Close all open rows
|
|
143
|
+
Swipeable.closeAll() // animated
|
|
144
|
+
Swipeable.closeAll(false) // instant
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## AutoClose Pattern (Swipe-to-Reply)
|
|
148
|
+
|
|
149
|
+
For chat-style swipe-to-reply where the row should automatically close:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<Swipeable
|
|
153
|
+
actions={<ReplyIndicator />}
|
|
154
|
+
actionsWidth={60}
|
|
155
|
+
actionsPosition="right"
|
|
156
|
+
autoClose={true}
|
|
157
|
+
threshold={0.6}
|
|
158
|
+
onSwipeEnd={(state) => {
|
|
159
|
+
if (state === 'open') {
|
|
160
|
+
// Trigger reply action
|
|
161
|
+
onReply(message)
|
|
162
|
+
}
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<ChatMessage message={message} />
|
|
166
|
+
</Swipeable>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## FlashList Integration
|
|
170
|
+
|
|
171
|
+
For virtualized lists, use `recyclingKey` to persist swipe state across recycling:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
import { FlashList } from '@shopify/flash-list'
|
|
175
|
+
|
|
176
|
+
function MessageList({ messages }) {
|
|
177
|
+
return (
|
|
178
|
+
<FlashList
|
|
179
|
+
data={messages}
|
|
180
|
+
renderItem={({ item }) => (
|
|
181
|
+
<Swipeable
|
|
182
|
+
recyclingKey={item.id}
|
|
183
|
+
actions={<DeleteAction />}
|
|
184
|
+
actionsWidth={80}
|
|
185
|
+
>
|
|
186
|
+
<MessageRow message={item} />
|
|
187
|
+
</Swipeable>
|
|
188
|
+
)}
|
|
189
|
+
estimatedItemSize={60}
|
|
190
|
+
/>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Architecture
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
199
|
+
│ JS Layer (React) │
|
|
200
|
+
│ ┌─────────────────┐ ┌──────────────────────────────────┐ │
|
|
201
|
+
│ │ <Swipeable> │ │ Actions (ReactNode) │ │
|
|
202
|
+
│ │ - renderActions│──│ - Rendered lazily on first swipe│ │
|
|
203
|
+
│ │ - ref methods │ │ - Any React components │ │
|
|
204
|
+
│ └────────┬────────┘ └──────────────────────────────────┘ │
|
|
205
|
+
└───────────│─────────────────────────────────────────────────┘
|
|
206
|
+
│ Expo Modules Bridge
|
|
207
|
+
┌───────────▼─────────────────────────────────────────────────┐
|
|
208
|
+
│ Native Layer (Kotlin/Swift) │
|
|
209
|
+
│ ┌─────────────────────────────────────────────────────────┐│
|
|
210
|
+
│ │ SwipeableView ││
|
|
211
|
+
│ │ - UIPanGestureRecognizer (iOS) / OnTouchListener (And) ││
|
|
212
|
+
│ │ - Spring animations (UIView.animate / DynamicAnimation)││
|
|
213
|
+
│ │ - Progress events @ 60fps (CADisplayLink / Choreograph)││
|
|
214
|
+
│ │ - LRU cache for recycling state persistence ││
|
|
215
|
+
│ └─────────────────────────────────────────────────────────┘│
|
|
216
|
+
└─────────────────────────────────────────────────────────────┘
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Key design decisions:**
|
|
220
|
+
|
|
221
|
+
- Gestures handled entirely in native code for 60fps performance
|
|
222
|
+
- Actions rendered lazily on first swipe gesture (performance optimization)
|
|
223
|
+
- Recycling key enables state persistence in virtualized lists
|
|
224
|
+
- No SharedValue dependency - progress synced via native events
|
|
225
|
+
|
|
226
|
+
## Platform Support
|
|
227
|
+
|
|
228
|
+
| Platform | Version |
|
|
229
|
+
|----------|---------|
|
|
230
|
+
| iOS | 12.0+ |
|
|
231
|
+
| Android | API 21+ |
|
|
232
|
+
|
|
233
|
+
## TypeScript
|
|
234
|
+
|
|
235
|
+
Full TypeScript support with exported types:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import {
|
|
239
|
+
Swipeable,
|
|
240
|
+
SwipeableProps,
|
|
241
|
+
SwipeableMethods,
|
|
242
|
+
SwipeableStatic,
|
|
243
|
+
SwipeProgressEvent,
|
|
244
|
+
SwipeStateEvent,
|
|
245
|
+
} from 'react-native-swipeable-actions'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
apply plugin: 'maven-publish'
|
|
4
|
+
|
|
5
|
+
group = 'expo.modules.swipeable'
|
|
6
|
+
version = '0.1.0'
|
|
7
|
+
|
|
8
|
+
buildscript {
|
|
9
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
10
|
+
if (expoModulesCorePlugin.exists()) {
|
|
11
|
+
apply from: expoModulesCorePlugin
|
|
12
|
+
applyKotlinExpoModulesCorePlugin()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
repositories {
|
|
16
|
+
mavenCentral()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
dependencies {
|
|
20
|
+
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEvaluate {
|
|
25
|
+
publishing {
|
|
26
|
+
publications {
|
|
27
|
+
release(MavenPublication) {
|
|
28
|
+
from components.release
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
repositories {
|
|
32
|
+
maven {
|
|
33
|
+
url = mavenLocal().url
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
android {
|
|
40
|
+
namespace "expo.modules.swipeable"
|
|
41
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
42
|
+
|
|
43
|
+
defaultConfig {
|
|
44
|
+
minSdkVersion safeExtGet("minSdkVersion", 23)
|
|
45
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
46
|
+
versionCode 1
|
|
47
|
+
versionName "0.1.0"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
publishing {
|
|
51
|
+
singleVariant("release") {
|
|
52
|
+
withSourcesJar()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
lintOptions {
|
|
57
|
+
abortOnError false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
compileOptions {
|
|
61
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
62
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
kotlinOptions {
|
|
66
|
+
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
testOptions {
|
|
70
|
+
unitTests {
|
|
71
|
+
includeAndroidResources = true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
repositories {
|
|
77
|
+
mavenCentral()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
dependencies {
|
|
81
|
+
implementation project(':expo-modules-core')
|
|
82
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
83
|
+
implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
|
|
84
|
+
|
|
85
|
+
testImplementation "junit:junit:4.13.2"
|
|
86
|
+
testImplementation "io.mockk:mockk:1.13.8"
|
|
87
|
+
testImplementation "org.robolectric:robolectric:4.11.1"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def getKotlinVersion() {
|
|
91
|
+
def kotlin_version = rootProject.ext.has("kotlinVersion")
|
|
92
|
+
? rootProject.ext.get("kotlinVersion")
|
|
93
|
+
: "1.9.23"
|
|
94
|
+
return kotlin_version
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def safeExtGet(String prop, Integer defaultValue) {
|
|
98
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : defaultValue
|
|
99
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package expo.modules.swipeable
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Physics and animation constants for swipeable behavior.
|
|
5
|
+
*
|
|
6
|
+
* Cross-platform constants (must match iOS SwipeAnimationConfig.swift):
|
|
7
|
+
* - RUBBER_BAND_DIVISOR, VELOCITY_NORMALIZER, MAX_NORMALIZED_VELOCITY
|
|
8
|
+
* - VELOCITY_PROJECTION_FACTOR, PROGRESS_CHANGE_THRESHOLD, *_DAMPING_RATIO
|
|
9
|
+
*
|
|
10
|
+
* Platform-specific: Android uses spring physics (stiffness), iOS uses duration-based animation.
|
|
11
|
+
* Both produce similar visual results with critical damping (no oscillation).
|
|
12
|
+
*/
|
|
13
|
+
object SwipeConstants {
|
|
14
|
+
// Cross-platform constants - must match iOS
|
|
15
|
+
const val RUBBER_BAND_DIVISOR = 3f
|
|
16
|
+
const val VELOCITY_NORMALIZER = 500f
|
|
17
|
+
const val MAX_NORMALIZED_VELOCITY = 2.0f
|
|
18
|
+
const val VELOCITY_PROJECTION_FACTOR = 0.15f
|
|
19
|
+
const val PROGRESS_CHANGE_THRESHOLD = 0.001f
|
|
20
|
+
|
|
21
|
+
// Android-specific: velocity conversion
|
|
22
|
+
const val VELOCITY_SCALE = 1000f
|
|
23
|
+
|
|
24
|
+
// Android-specific: spring physics (iOS uses duration-based animation)
|
|
25
|
+
const val OPEN_STIFFNESS = 300f
|
|
26
|
+
const val OPEN_DAMPING_RATIO = 1.0f // Critical damping - no oscillation (matches iOS)
|
|
27
|
+
const val CLOSE_STIFFNESS = 400f
|
|
28
|
+
const val CLOSE_DAMPING_RATIO = 1.0f // Critical damping - no oscillation (matches iOS)
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package expo.modules.swipeable
|
|
2
|
+
|
|
3
|
+
import kotlin.math.abs
|
|
4
|
+
import kotlin.math.min
|
|
5
|
+
import kotlin.math.sign
|
|
6
|
+
|
|
7
|
+
object SwipePhysics {
|
|
8
|
+
|
|
9
|
+
fun applyRubberBand(
|
|
10
|
+
translation: Float,
|
|
11
|
+
actionsWidth: Float,
|
|
12
|
+
isLeading: Boolean
|
|
13
|
+
): Float {
|
|
14
|
+
var result = translation
|
|
15
|
+
if (isLeading) {
|
|
16
|
+
when {
|
|
17
|
+
result > actionsWidth -> {
|
|
18
|
+
result = actionsWidth + ((result - actionsWidth) / SwipeConstants.RUBBER_BAND_DIVISOR)
|
|
19
|
+
}
|
|
20
|
+
result < 0f -> {
|
|
21
|
+
result /= SwipeConstants.RUBBER_BAND_DIVISOR
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
when {
|
|
26
|
+
result < -actionsWidth -> {
|
|
27
|
+
result = -actionsWidth - ((abs(result) - actionsWidth) / SwipeConstants.RUBBER_BAND_DIVISOR)
|
|
28
|
+
}
|
|
29
|
+
result > 0f -> {
|
|
30
|
+
result /= SwipeConstants.RUBBER_BAND_DIVISOR
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun shouldSnapToOpen(
|
|
38
|
+
projectedPosition: Float,
|
|
39
|
+
thresholdDistance: Float,
|
|
40
|
+
isOpen: Boolean,
|
|
41
|
+
isLeading: Boolean
|
|
42
|
+
): Boolean {
|
|
43
|
+
return if (isLeading) {
|
|
44
|
+
if (isOpen) projectedPosition > -thresholdDistance else projectedPosition > thresholdDistance
|
|
45
|
+
} else {
|
|
46
|
+
if (isOpen) projectedPosition < thresholdDistance else projectedPosition < -thresholdDistance
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun projectFinalPosition(
|
|
51
|
+
translation: Float,
|
|
52
|
+
velocity: Float,
|
|
53
|
+
friction: Float
|
|
54
|
+
): Float {
|
|
55
|
+
return (translation * friction) + (velocity * friction * SwipeConstants.VELOCITY_PROJECTION_FACTOR)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun calculateSpringVelocity(gestureVelocity: Float): Float {
|
|
59
|
+
// Preserve the sign of the velocity - critical for correct spring animation direction
|
|
60
|
+
val magnitude = min(
|
|
61
|
+
abs(gestureVelocity) / SwipeConstants.VELOCITY_NORMALIZER,
|
|
62
|
+
SwipeConstants.MAX_NORMALIZED_VELOCITY
|
|
63
|
+
) * SwipeConstants.VELOCITY_SCALE
|
|
64
|
+
return sign(gestureVelocity) * magnitude
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package expo.modules.swipeable
|
|
2
|
+
|
|
3
|
+
import expo.modules.kotlin.modules.Module
|
|
4
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
|
+
|
|
6
|
+
class SwipeableModule : Module() {
|
|
7
|
+
override fun definition() = ModuleDefinition {
|
|
8
|
+
Name("Swipeable")
|
|
9
|
+
|
|
10
|
+
// Module-level functions (by recyclingKey)
|
|
11
|
+
Function("openByKey") { key: String ->
|
|
12
|
+
SwipeableView.openByKey(key)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Function("closeByKey") { key: String, animated: Boolean? ->
|
|
16
|
+
SwipeableView.closeByKey(key, animated ?: true)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Function("closeAll") { animated: Boolean? ->
|
|
20
|
+
SwipeableView.closeAll(animated ?: true)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
View(SwipeableView::class) {
|
|
24
|
+
Prop("actionsWidth") { view: SwipeableView, width: Float ->
|
|
25
|
+
view.actionsWidth = width
|
|
26
|
+
}
|
|
27
|
+
Prop("actionsPosition") { view: SwipeableView, position: String ->
|
|
28
|
+
view.actionsPosition = position
|
|
29
|
+
}
|
|
30
|
+
Prop("friction") { view: SwipeableView, friction: Float ->
|
|
31
|
+
view.friction = friction.coerceIn(0f, 1f)
|
|
32
|
+
}
|
|
33
|
+
Prop("threshold") { view: SwipeableView, threshold: Float ->
|
|
34
|
+
view.threshold = threshold.coerceIn(0f, 1f)
|
|
35
|
+
}
|
|
36
|
+
Prop("dragOffsetFromEdge") { view: SwipeableView, offset: Float ->
|
|
37
|
+
view.dragOffsetFromEdge = offset
|
|
38
|
+
}
|
|
39
|
+
Prop("recyclingKey") { view: SwipeableView, key: String? ->
|
|
40
|
+
view.recyclingKey = key
|
|
41
|
+
}
|
|
42
|
+
Prop("autoClose") { view: SwipeableView, autoClose: Boolean ->
|
|
43
|
+
view.autoClose = autoClose
|
|
44
|
+
}
|
|
45
|
+
Prop("autoCloseTimeout") { view: SwipeableView, timeout: Float ->
|
|
46
|
+
view.autoCloseTimeout = timeout
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Events("onSwipeProgress", "onSwipeStart", "onSwipeStateChange", "onSwipeEnd")
|
|
50
|
+
|
|
51
|
+
AsyncFunction("close") { view: SwipeableView, animated: Boolean? ->
|
|
52
|
+
view.post { view.close(animated ?: true) }
|
|
53
|
+
}
|
|
54
|
+
AsyncFunction("open") { view: SwipeableView ->
|
|
55
|
+
view.post { view.open() }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
AsyncFunction("handleGestureStart") { view: SwipeableView ->
|
|
59
|
+
view.handleGestureStart()
|
|
60
|
+
}
|
|
61
|
+
AsyncFunction("handleGestureUpdate") { view: SwipeableView, translationX: Float, velocityX: Float ->
|
|
62
|
+
view.handleGestureUpdate(translationX, velocityX)
|
|
63
|
+
}
|
|
64
|
+
AsyncFunction("handleGestureEnd") { view: SwipeableView, translationX: Float, velocityX: Float ->
|
|
65
|
+
view.handleGestureEnd(translationX, velocityX)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|