react-native-s-pen 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 +172 -0
- package/android/build.gradle +33 -0
- package/android/consumer-rules.pro +2 -0
- package/android/gradle.properties +3 -0
- package/android/libs/README.md +10 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/samsungspen/NativeSpenModule.java +910 -0
- package/android/src/main/java/com/samsungspen/SpenCanvasManager.java +67 -0
- package/android/src/main/java/com/samsungspen/SpenCanvasView.java +186 -0
- package/android/src/main/java/com/samsungspen/SpenPackage.java +57 -0
- package/package.json +46 -0
- package/react-native.config.js +9 -0
- package/src/SPen.ts +179 -0
- package/src/SpenCanvas.tsx +47 -0
- package/src/index.ts +15 -0
- package/src/native.ts +29 -0
- package/src/specs/NativeSpenModule.ts +50 -0
- package/src/specs/SpenCanvasNativeComponent.ts +23 -0
- package/src/types.ts +79 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
package com.samsungspen;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
import androidx.annotation.Nullable;
|
|
5
|
+
|
|
6
|
+
import com.facebook.react.common.MapBuilder;
|
|
7
|
+
import com.facebook.react.uimanager.SimpleViewManager;
|
|
8
|
+
import com.facebook.react.uimanager.ThemedReactContext;
|
|
9
|
+
import com.facebook.react.uimanager.ViewManagerDelegate;
|
|
10
|
+
import com.facebook.react.uimanager.annotations.ReactProp;
|
|
11
|
+
import com.facebook.react.viewmanagers.SpenCanvasManagerDelegate;
|
|
12
|
+
import com.facebook.react.viewmanagers.SpenCanvasManagerInterface;
|
|
13
|
+
|
|
14
|
+
import java.util.Map;
|
|
15
|
+
|
|
16
|
+
public final class SpenCanvasManager extends SimpleViewManager<SpenCanvasView>
|
|
17
|
+
implements SpenCanvasManagerInterface<SpenCanvasView> {
|
|
18
|
+
static final String NAME = "SpenCanvas";
|
|
19
|
+
private final ViewManagerDelegate<SpenCanvasView> delegate =
|
|
20
|
+
new SpenCanvasManagerDelegate<>(this);
|
|
21
|
+
|
|
22
|
+
@Override
|
|
23
|
+
protected ViewManagerDelegate<SpenCanvasView> getDelegate() {
|
|
24
|
+
return delegate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@NonNull
|
|
28
|
+
@Override
|
|
29
|
+
public String getName() {
|
|
30
|
+
return NAME;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@NonNull
|
|
34
|
+
@Override
|
|
35
|
+
protected SpenCanvasView createViewInstance(@NonNull ThemedReactContext context) {
|
|
36
|
+
return new SpenCanvasView(context);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@ReactProp(name = "inkColor")
|
|
40
|
+
public void setInkColor(SpenCanvasView view, @Nullable String color) {
|
|
41
|
+
if (color != null) {
|
|
42
|
+
view.setInkColor(color);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@ReactProp(name = "minStrokeWidth", defaultFloat = 2f)
|
|
47
|
+
public void setMinStrokeWidth(SpenCanvasView view, float width) {
|
|
48
|
+
view.setMinStrokeWidth(width);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@ReactProp(name = "maxStrokeWidth", defaultFloat = 16f)
|
|
52
|
+
public void setMaxStrokeWidth(SpenCanvasView view, float width) {
|
|
53
|
+
view.setMaxStrokeWidth(width);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@ReactProp(name = "clearToken", defaultInt = 0)
|
|
57
|
+
public void setClearToken(SpenCanvasView view, int token) {
|
|
58
|
+
view.setClearToken(token);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Override
|
|
62
|
+
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
|
|
63
|
+
return MapBuilder.<String, Object>builder()
|
|
64
|
+
.put("topSpenDraw", MapBuilder.of("registrationName", "onSpenDraw"))
|
|
65
|
+
.build();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package com.samsungspen;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Canvas;
|
|
5
|
+
import android.graphics.Color;
|
|
6
|
+
import android.graphics.Paint;
|
|
7
|
+
import android.graphics.Path;
|
|
8
|
+
import android.view.MotionEvent;
|
|
9
|
+
import android.view.View;
|
|
10
|
+
|
|
11
|
+
import androidx.annotation.NonNull;
|
|
12
|
+
|
|
13
|
+
import com.facebook.react.bridge.Arguments;
|
|
14
|
+
import com.facebook.react.bridge.ReactContext;
|
|
15
|
+
import com.facebook.react.bridge.WritableMap;
|
|
16
|
+
import com.facebook.react.uimanager.UIManagerHelper;
|
|
17
|
+
import com.facebook.react.uimanager.events.Event;
|
|
18
|
+
import com.facebook.react.uimanager.events.EventDispatcher;
|
|
19
|
+
|
|
20
|
+
import java.util.ArrayList;
|
|
21
|
+
import java.util.List;
|
|
22
|
+
|
|
23
|
+
final class SpenCanvasView extends View {
|
|
24
|
+
private static final class StrokePoint {
|
|
25
|
+
final float x;
|
|
26
|
+
final float y;
|
|
27
|
+
final float pressure;
|
|
28
|
+
final boolean startsStroke;
|
|
29
|
+
|
|
30
|
+
StrokePoint(float x, float y, float pressure, boolean startsStroke) {
|
|
31
|
+
this.x = x;
|
|
32
|
+
this.y = y;
|
|
33
|
+
this.pressure = pressure;
|
|
34
|
+
this.startsStroke = startsStroke;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
|
39
|
+
private final List<StrokePoint> points = new ArrayList<>();
|
|
40
|
+
private float minStrokeWidth = 2f;
|
|
41
|
+
private float maxStrokeWidth = 16f;
|
|
42
|
+
private int clearToken = 0;
|
|
43
|
+
|
|
44
|
+
SpenCanvasView(Context context) {
|
|
45
|
+
super(context);
|
|
46
|
+
paint.setColor(Color.rgb(17, 20, 15));
|
|
47
|
+
paint.setStyle(Paint.Style.STROKE);
|
|
48
|
+
paint.setStrokeCap(Paint.Cap.ROUND);
|
|
49
|
+
paint.setStrokeJoin(Paint.Join.ROUND);
|
|
50
|
+
setBackgroundColor(Color.rgb(233, 237, 223));
|
|
51
|
+
setFocusable(true);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void setInkColor(String color) {
|
|
55
|
+
try {
|
|
56
|
+
paint.setColor(Color.parseColor(color));
|
|
57
|
+
invalidate();
|
|
58
|
+
} catch (IllegalArgumentException ignored) {
|
|
59
|
+
// Keep the previous valid color.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
void setMinStrokeWidth(float width) {
|
|
64
|
+
minStrokeWidth = Math.max(0.5f, width);
|
|
65
|
+
invalidate();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void setMaxStrokeWidth(float width) {
|
|
69
|
+
maxStrokeWidth = Math.max(minStrokeWidth, width);
|
|
70
|
+
invalidate();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void setClearToken(int token) {
|
|
74
|
+
if (clearToken != token) {
|
|
75
|
+
clearToken = token;
|
|
76
|
+
points.clear();
|
|
77
|
+
invalidate();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Override
|
|
82
|
+
protected void onDraw(@NonNull Canvas canvas) {
|
|
83
|
+
super.onDraw(canvas);
|
|
84
|
+
if (points.isEmpty()) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
StrokePoint previous = null;
|
|
89
|
+
for (StrokePoint point : points) {
|
|
90
|
+
float width = pressureToWidth(point.pressure);
|
|
91
|
+
paint.setStrokeWidth(width);
|
|
92
|
+
if (previous == null || point.startsStroke) {
|
|
93
|
+
canvas.drawPoint(point.x, point.y, paint);
|
|
94
|
+
} else {
|
|
95
|
+
Path segment = new Path();
|
|
96
|
+
segment.moveTo(previous.x, previous.y);
|
|
97
|
+
segment.lineTo(point.x, point.y);
|
|
98
|
+
canvas.drawPath(segment, paint);
|
|
99
|
+
}
|
|
100
|
+
previous = point;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Override
|
|
105
|
+
public boolean onTouchEvent(MotionEvent event) {
|
|
106
|
+
int pointerIndex = Math.max(0, event.getActionIndex());
|
|
107
|
+
int toolType = event.getToolType(pointerIndex);
|
|
108
|
+
if (toolType != MotionEvent.TOOL_TYPE_STYLUS && toolType != MotionEvent.TOOL_TYPE_ERASER) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
int action = event.getActionMasked();
|
|
113
|
+
if (action == MotionEvent.ACTION_DOWN) {
|
|
114
|
+
getParent().requestDisallowInterceptTouchEvent(true);
|
|
115
|
+
addPoint(event, pointerIndex, true);
|
|
116
|
+
emitPoint(event, pointerIndex, "down", toolType);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (action == MotionEvent.ACTION_MOVE) {
|
|
120
|
+
addPoint(event, pointerIndex, false);
|
|
121
|
+
emitPoint(event, pointerIndex, "move", toolType);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
|
125
|
+
addPoint(event, pointerIndex, false);
|
|
126
|
+
emitPoint(event, pointerIndex, action == MotionEvent.ACTION_CANCEL ? "cancel" : "up", toolType);
|
|
127
|
+
getParent().requestDisallowInterceptTouchEvent(false);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private void addPoint(MotionEvent event, int pointerIndex, boolean startsStroke) {
|
|
134
|
+
points.add(new StrokePoint(event.getX(pointerIndex), event.getY(pointerIndex), event.getPressure(pointerIndex), startsStroke));
|
|
135
|
+
invalidate();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private float pressureToWidth(float pressure) {
|
|
139
|
+
float normalized = Math.max(0f, Math.min(1f, pressure));
|
|
140
|
+
return minStrokeWidth + ((maxStrokeWidth - minStrokeWidth) * normalized);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private void emitPoint(MotionEvent event, int pointerIndex, String action, int toolType) {
|
|
144
|
+
WritableMap payload = Arguments.createMap();
|
|
145
|
+
payload.putDouble("x", event.getX(pointerIndex));
|
|
146
|
+
payload.putDouble("y", event.getY(pointerIndex));
|
|
147
|
+
payload.putDouble("pressure", event.getPressure(pointerIndex));
|
|
148
|
+
payload.putDouble("tilt", event.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex));
|
|
149
|
+
payload.putDouble("hoverDistance", event.getAxisValue(MotionEvent.AXIS_DISTANCE, pointerIndex));
|
|
150
|
+
payload.putDouble("timestamp", event.getEventTime());
|
|
151
|
+
payload.putString("action", action);
|
|
152
|
+
payload.putString("toolType", toolType == MotionEvent.TOOL_TYPE_ERASER ? "eraser" : "stylus");
|
|
153
|
+
|
|
154
|
+
EventDispatcher dispatcher =
|
|
155
|
+
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) getContext(), getId());
|
|
156
|
+
if (dispatcher != null) {
|
|
157
|
+
int surfaceId = UIManagerHelper.getSurfaceId(this);
|
|
158
|
+
dispatcher.dispatchEvent(new SpenCanvasEvent(surfaceId, getId(), payload));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static final class SpenCanvasEvent extends Event<SpenCanvasEvent> {
|
|
163
|
+
private final WritableMap payload;
|
|
164
|
+
|
|
165
|
+
SpenCanvasEvent(int surfaceId, int viewTag, WritableMap payload) {
|
|
166
|
+
super(surfaceId, viewTag);
|
|
167
|
+
this.payload = payload;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@NonNull
|
|
171
|
+
@Override
|
|
172
|
+
public String getEventName() {
|
|
173
|
+
return "topSpenDraw";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Override
|
|
177
|
+
public boolean canCoalesce() {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@Override
|
|
182
|
+
protected WritableMap getEventData() {
|
|
183
|
+
return payload;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package com.samsungspen;
|
|
2
|
+
|
|
3
|
+
import androidx.annotation.NonNull;
|
|
4
|
+
import androidx.annotation.Nullable;
|
|
5
|
+
|
|
6
|
+
import com.facebook.react.TurboReactPackage;
|
|
7
|
+
import com.facebook.react.bridge.NativeModule;
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
9
|
+
import com.facebook.react.module.model.ReactModuleInfo;
|
|
10
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
11
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
12
|
+
|
|
13
|
+
import java.util.Collections;
|
|
14
|
+
import java.util.HashMap;
|
|
15
|
+
import java.util.List;
|
|
16
|
+
import java.util.Map;
|
|
17
|
+
|
|
18
|
+
public class SpenPackage extends TurboReactPackage {
|
|
19
|
+
private static final String MODULE_NAME = "SpenModule";
|
|
20
|
+
|
|
21
|
+
@Nullable
|
|
22
|
+
@Override
|
|
23
|
+
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
|
|
24
|
+
if (MODULE_NAME.equals(name)) {
|
|
25
|
+
return new NativeSpenModule(reactContext);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Override
|
|
31
|
+
public ReactModuleInfoProvider getReactModuleInfoProvider() {
|
|
32
|
+
return new ReactModuleInfoProvider() {
|
|
33
|
+
@NonNull
|
|
34
|
+
@Override
|
|
35
|
+
public Map<String, ReactModuleInfo> getReactModuleInfos() {
|
|
36
|
+
Map<String, ReactModuleInfo> infos = new HashMap<>();
|
|
37
|
+
infos.put(
|
|
38
|
+
MODULE_NAME,
|
|
39
|
+
new ReactModuleInfo(
|
|
40
|
+
MODULE_NAME,
|
|
41
|
+
NativeSpenModule.class.getName(),
|
|
42
|
+
false,
|
|
43
|
+
false,
|
|
44
|
+
false,
|
|
45
|
+
false,
|
|
46
|
+
true));
|
|
47
|
+
return infos;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@NonNull
|
|
53
|
+
@Override
|
|
54
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
55
|
+
return Collections.<ViewManager>singletonList(new SpenCanvasManager());
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-s-pen",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native library for Samsung S Pen support on Android",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/alihamzaazhar/react-native-s-pen.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/alihamzaazhar/react-native-s-pen#readme",
|
|
10
|
+
"bugs": "https://github.com/alihamzaazhar/react-native-s-pen/issues",
|
|
11
|
+
"main": "src/index.ts",
|
|
12
|
+
"types": "src/index.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"src",
|
|
15
|
+
"android/build.gradle",
|
|
16
|
+
"android/consumer-rules.pro",
|
|
17
|
+
"android/gradle.properties",
|
|
18
|
+
"android/libs/README.md",
|
|
19
|
+
"android/src",
|
|
20
|
+
"react-native.config.js",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"codegenConfig": {
|
|
24
|
+
"name": "SpenModule",
|
|
25
|
+
"type": "all",
|
|
26
|
+
"jsSrcsDir": "src",
|
|
27
|
+
"android": {
|
|
28
|
+
"javaPackageName": "com.samsungspen"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"clean": "rm -rf node_modules android/build ios/build",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"react-native": "src/index.ts",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": ">=19.2.3",
|
|
38
|
+
"react-native": ">=0.86.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/react": "^19.2.0",
|
|
42
|
+
"react": "19.2.3",
|
|
43
|
+
"react-native": "0.86.0",
|
|
44
|
+
"typescript": "^5.5.4"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/SPen.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import { createEventEmitter, requireNativeModule } from "./native";
|
|
3
|
+
import type {
|
|
4
|
+
SpenConnectionState,
|
|
5
|
+
SpenDeviceInfo,
|
|
6
|
+
SpenEventPayload,
|
|
7
|
+
SpenInsertionState,
|
|
8
|
+
SpenListener,
|
|
9
|
+
SpenRemoteFeature,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
class SPenManager {
|
|
13
|
+
private eventSubscriptions = new Set<{ remove: () => void }>();
|
|
14
|
+
private emitter: ReturnType<typeof createEventEmitter> | null = null;
|
|
15
|
+
private listenerCount = 0;
|
|
16
|
+
|
|
17
|
+
private getEmitter() {
|
|
18
|
+
if (!this.emitter) {
|
|
19
|
+
this.emitter = createEventEmitter();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return this.emitter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async isSupported(): Promise<boolean> {
|
|
26
|
+
if (Platform.OS !== "android") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return requireNativeModule().isSupported();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getDeviceInfo(): Promise<SpenDeviceInfo> {
|
|
34
|
+
if (Platform.OS !== "android") {
|
|
35
|
+
throw new Error("react-native-s-pen only supports Android.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return requireNativeModule().getDeviceInfo();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async getVersionCode(): Promise<number> {
|
|
42
|
+
if (Platform.OS !== "android") {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return requireNativeModule().getVersionCode();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getVersionName(): Promise<string> {
|
|
50
|
+
if (Platform.OS !== "android") {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return requireNativeModule().getVersionName();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getPenInsertionState(): Promise<SpenInsertionState> {
|
|
58
|
+
if (Platform.OS !== "android") {
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return requireNativeModule().getPenInsertionState();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async isConnected(): Promise<boolean> {
|
|
66
|
+
if (Platform.OS !== "android") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return requireNativeModule().isConnected();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async isFeatureEnabled(feature: SpenRemoteFeature): Promise<boolean> {
|
|
74
|
+
if (Platform.OS !== "android") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return requireNativeModule().isFeatureEnabled(feature);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async startListening(): Promise<void> {
|
|
82
|
+
if (Platform.OS !== "android") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await requireNativeModule().startListening();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async stopListening(): Promise<void> {
|
|
90
|
+
if (Platform.OS !== "android") {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await requireNativeModule().stopListening();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
addListener(listener: SpenListener) {
|
|
98
|
+
const subscription = this.getEmitter().addListener("SpenEvent", (event: SpenEventPayload) => {
|
|
99
|
+
listener(event);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.eventSubscriptions.add(subscription);
|
|
103
|
+
this.listenerCount += 1;
|
|
104
|
+
void this.startListening();
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
subscription.remove();
|
|
108
|
+
this.eventSubscriptions.delete(subscription);
|
|
109
|
+
this.listenerCount = Math.max(0, this.listenerCount - 1);
|
|
110
|
+
|
|
111
|
+
if (this.listenerCount === 0) {
|
|
112
|
+
void this.stopListening();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
addConnectionStateListener(listener: (state: SpenConnectionState) => void) {
|
|
118
|
+
let receivedLiveState = false;
|
|
119
|
+
const subscription = this.getEmitter().addListener("SpenEvent", (event: SpenEventPayload) => {
|
|
120
|
+
if (event.name === "spen-connection-state" && event.connectionState) {
|
|
121
|
+
receivedLiveState = true;
|
|
122
|
+
listener(event.connectionState);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.eventSubscriptions.add(subscription);
|
|
127
|
+
this.listenerCount += 1;
|
|
128
|
+
void this.startListening()
|
|
129
|
+
.then(() => this.isConnected())
|
|
130
|
+
.then((connected) => {
|
|
131
|
+
if (connected && !receivedLiveState && this.eventSubscriptions.has(subscription)) {
|
|
132
|
+
listener("connected");
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
subscription.remove();
|
|
138
|
+
this.eventSubscriptions.delete(subscription);
|
|
139
|
+
this.listenerCount = Math.max(0, this.listenerCount - 1);
|
|
140
|
+
|
|
141
|
+
if (this.listenerCount === 0) {
|
|
142
|
+
void this.stopListening();
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
addPenInsertionStateListener(listener: (state: SpenInsertionState) => void) {
|
|
148
|
+
const subscription = this.getEmitter().addListener("SpenEvent", (event: SpenEventPayload) => {
|
|
149
|
+
if (event.name === "spen-insertion-state" && event.insertionState) {
|
|
150
|
+
listener(event.insertionState);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.eventSubscriptions.add(subscription);
|
|
155
|
+
this.listenerCount += 1;
|
|
156
|
+
void this.startListening();
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
subscription.remove();
|
|
160
|
+
this.eventSubscriptions.delete(subscription);
|
|
161
|
+
this.listenerCount = Math.max(0, this.listenerCount - 1);
|
|
162
|
+
|
|
163
|
+
if (this.listenerCount === 0) {
|
|
164
|
+
void this.stopListening();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
removeAllListeners() {
|
|
170
|
+
for (const subscription of this.eventSubscriptions) {
|
|
171
|
+
subscription.remove();
|
|
172
|
+
}
|
|
173
|
+
this.eventSubscriptions.clear();
|
|
174
|
+
this.listenerCount = 0;
|
|
175
|
+
void this.stopListening();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const SPen = new SPenManager();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
View,
|
|
5
|
+
type NativeSyntheticEvent,
|
|
6
|
+
type ViewProps,
|
|
7
|
+
} from "react-native";
|
|
8
|
+
import * as RequireNativeComponentModule from "react-native/Libraries/ReactNative/requireNativeComponent";
|
|
9
|
+
|
|
10
|
+
export type SpenCanvasPoint = {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
pressure: number;
|
|
14
|
+
tilt: number;
|
|
15
|
+
hoverDistance: number;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
action: "down" | "move" | "up" | "cancel";
|
|
18
|
+
toolType: "stylus" | "eraser";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SpenCanvasProps = ViewProps & {
|
|
22
|
+
inkColor?: string;
|
|
23
|
+
minStrokeWidth?: number;
|
|
24
|
+
maxStrokeWidth?: number;
|
|
25
|
+
clearToken?: number;
|
|
26
|
+
onDraw?: (point: SpenCanvasPoint) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type NativeCanvasProps = Omit<SpenCanvasProps, "onDraw"> & {
|
|
30
|
+
onSpenDraw?: (event: NativeSyntheticEvent<SpenCanvasPoint>) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// The separate NativeComponent spec generates Fabric metadata and the Android manager delegate.
|
|
34
|
+
const requireNativeComponent = (
|
|
35
|
+
RequireNativeComponentModule as unknown as {
|
|
36
|
+
default: <T>(name: string) => React.ComponentType<T>;
|
|
37
|
+
}
|
|
38
|
+
).default;
|
|
39
|
+
const NativeSpenCanvas = requireNativeComponent<NativeCanvasProps>("SpenCanvas");
|
|
40
|
+
|
|
41
|
+
export function SpenCanvas({ onDraw, ...props }: SpenCanvasProps) {
|
|
42
|
+
if (Platform.OS !== "android") {
|
|
43
|
+
return <View {...props} />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return <NativeSpenCanvas {...props} onSpenDraw={(event) => onDraw?.(event.nativeEvent)} />;
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { SPen } from "./SPen";
|
|
2
|
+
export { SpenCanvas } from "./SpenCanvas";
|
|
3
|
+
export type { SpenCanvasPoint, SpenCanvasProps } from "./SpenCanvas";
|
|
4
|
+
export type {
|
|
5
|
+
SpenConnectionState,
|
|
6
|
+
SpenInsertionState,
|
|
7
|
+
SpenCapability,
|
|
8
|
+
SpenErrorCode,
|
|
9
|
+
SpenDeviceInfo,
|
|
10
|
+
SpenEventName,
|
|
11
|
+
SpenEventPayload,
|
|
12
|
+
SpenPoint,
|
|
13
|
+
SpenRemoteFeature,
|
|
14
|
+
SpenListener,
|
|
15
|
+
} from "./types";
|
package/src/native.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NativeEventEmitter, NativeModules, Platform } from "react-native";
|
|
2
|
+
import NativeSpenModule from "./specs/NativeSpenModule";
|
|
3
|
+
import type { Spec as NativeSpenModuleSpec } from "./specs/NativeSpenModule";
|
|
4
|
+
import type { SpenEventPayload } from "./types";
|
|
5
|
+
|
|
6
|
+
const LINKING_ERROR =
|
|
7
|
+
`The package 'react-native-s-pen' doesn't seem to be linked. ` +
|
|
8
|
+
Platform.select({ ios: "This library only supports Android.", default: "Run the native build step again." });
|
|
9
|
+
|
|
10
|
+
export function requireNativeModule(): NativeSpenModuleSpec {
|
|
11
|
+
if (Platform.OS !== "android") {
|
|
12
|
+
throw new Error(LINKING_ERROR);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const legacyModule = NativeModules.SpenModule as NativeSpenModuleSpec | undefined;
|
|
16
|
+
const nativeModule = NativeSpenModule ?? legacyModule;
|
|
17
|
+
|
|
18
|
+
if (!nativeModule) {
|
|
19
|
+
throw new Error(LINKING_ERROR);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return nativeModule;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createEventEmitter() {
|
|
26
|
+
return new NativeEventEmitter(requireNativeModule());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type { SpenEventPayload };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type {TurboModule} from "react-native";
|
|
2
|
+
import {TurboModuleRegistry} from "react-native";
|
|
3
|
+
|
|
4
|
+
type TurboSpenCapability =
|
|
5
|
+
| "stylusInput"
|
|
6
|
+
| "hover"
|
|
7
|
+
| "button"
|
|
8
|
+
| "airActions"
|
|
9
|
+
| "airCommand"
|
|
10
|
+
| "pressure";
|
|
11
|
+
|
|
12
|
+
type TurboSpenDeviceInfo = {
|
|
13
|
+
platform: "android";
|
|
14
|
+
manufacturer: string;
|
|
15
|
+
brand: string;
|
|
16
|
+
model: string;
|
|
17
|
+
sdkInt: number;
|
|
18
|
+
isSamsungDevice: boolean;
|
|
19
|
+
capabilities: Array<TurboSpenCapability>;
|
|
20
|
+
samsungSdk?: {
|
|
21
|
+
available: boolean;
|
|
22
|
+
versionCode?: number;
|
|
23
|
+
versionName?: string;
|
|
24
|
+
connected?: boolean;
|
|
25
|
+
featureFlags?: {
|
|
26
|
+
button?: boolean;
|
|
27
|
+
airMotion?: boolean;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export interface Spec extends TurboModule {
|
|
33
|
+
isSupported(): Promise<boolean>;
|
|
34
|
+
getDeviceInfo(): Promise<TurboSpenDeviceInfo>;
|
|
35
|
+
getVersionCode(): Promise<number>;
|
|
36
|
+
getVersionName(): Promise<string>;
|
|
37
|
+
getPenInsertionState(): Promise<"inserted" | "detached" | "unknown">;
|
|
38
|
+
isConnected(): Promise<boolean>;
|
|
39
|
+
isFeatureEnabled(feature: "button" | "airMotion"): Promise<boolean>;
|
|
40
|
+
startListening(): Promise<void>;
|
|
41
|
+
stopListening(): Promise<void>;
|
|
42
|
+
addListener(eventName: string): void;
|
|
43
|
+
removeListeners(count: number): void;
|
|
44
|
+
addConnectionStateListener(eventName: string): void;
|
|
45
|
+
removeConnectionStateListeners(count: number): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const NativeSpenModule = TurboModuleRegistry.get<Spec>("SpenModule");
|
|
49
|
+
|
|
50
|
+
export default NativeSpenModule;
|