@mobileai/react-native 0.9.27 → 0.9.29
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 +28 -16
- package/android/build.gradle +17 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
- package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
- package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
- package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
- package/bin/generate-map.cjs +45 -6
- package/ios/MobileAIFloatingOverlayComponentView.h +8 -0
- package/ios/MobileAIFloatingOverlayComponentView.mm +12 -41
- package/ios/Podfile +63 -0
- package/ios/Podfile.lock +2290 -0
- package/ios/Podfile.properties.json +4 -0
- package/ios/mobileaireactnative/AppDelegate.swift +69 -0
- package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
- package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
- package/ios/mobileaireactnative/Info.plist +55 -0
- package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
- package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
- package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
- package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
- package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
- package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
- package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
- package/lib/module/components/AIAgent.js +501 -191
- package/lib/module/components/AgentChatBar.js +250 -59
- package/lib/module/components/FloatingOverlayWrapper.js +68 -32
- package/lib/module/config/endpoints.js +22 -1
- package/lib/module/core/AgentRuntime.js +110 -8
- package/lib/module/core/FiberTreeWalker.js +211 -10
- package/lib/module/core/OutcomeVerifier.js +149 -0
- package/lib/module/core/systemPrompt.js +96 -25
- package/lib/module/providers/GeminiProvider.js +9 -3
- package/lib/module/services/telemetry/TelemetryService.js +21 -2
- package/lib/module/services/telemetry/TouchAutoCapture.js +235 -38
- package/lib/module/services/telemetry/analyticsLabeling.js +187 -0
- package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/lib/module/support/supportPrompt.js +22 -7
- package/lib/module/support/supportStyle.js +55 -0
- package/lib/module/support/types.js +2 -0
- package/lib/module/tools/typeTool.js +20 -0
- package/lib/module/utils/humanizeScreenName.js +49 -0
- package/lib/typescript/src/components/AIAgent.d.ts +6 -2
- package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
- package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
- package/lib/typescript/src/config/endpoints.d.ts +4 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +12 -3
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
- package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
- package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
- package/lib/typescript/src/core/types.d.ts +63 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts +6 -1
- package/lib/typescript/src/services/telemetry/analyticsLabeling.d.ts +20 -0
- package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
- package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
- package/lib/typescript/src/support/index.d.ts +1 -0
- package/lib/typescript/src/support/supportStyle.d.ts +9 -0
- package/lib/typescript/src/support/types.d.ts +3 -0
- package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
- package/package.json +10 -10
- package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/ios/MobileAIPilotIntents.swift +0 -51
package/README.md
CHANGED
|
@@ -307,14 +307,13 @@ npm install react-native-agentic-ai
|
|
|
307
307
|
|
|
308
308
|
No native modules required by default. Works with **Expo managed workflow** out of the box — no eject needed.
|
|
309
309
|
|
|
310
|
-
###
|
|
310
|
+
### Screenshot Capture
|
|
311
311
|
|
|
312
312
|
<details>
|
|
313
313
|
<summary><b>📸 Screenshots</b> — for image/video content understanding</summary>
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
```
|
|
315
|
+
`react-native-view-shot` is a required dependency for screenshot capture and is included with
|
|
316
|
+
`@mobileai/react-native`, so you do **not** need to add it separately.
|
|
318
317
|
|
|
319
318
|
</details>
|
|
320
319
|
|
|
@@ -391,6 +390,8 @@ npx @mobileai/react-native generate-map
|
|
|
391
390
|
|
|
392
391
|
### 2. Wrap Your App
|
|
393
392
|
|
|
393
|
+
If you use a MobileAI publishable key, the SDK now defaults to the hosted MobileAI text and voice proxies automatically. You only need to pass `proxyUrl` and `voiceProxyUrl` when you want to override them with your own backend.
|
|
394
|
+
|
|
394
395
|
#### React Navigation
|
|
395
396
|
|
|
396
397
|
```tsx
|
|
@@ -403,13 +404,10 @@ export default function App() {
|
|
|
403
404
|
|
|
404
405
|
return (
|
|
405
406
|
<AIAgent
|
|
406
|
-
// Your MobileAI Dashboard ID
|
|
407
|
+
// Your MobileAI Dashboard ID
|
|
408
|
+
// This now auto-configures the hosted MobileAI text + voice proxies too.
|
|
407
409
|
analyticsKey="mobileai_pub_xxxxxxxx"
|
|
408
410
|
|
|
409
|
-
// Route all traffic through the secure MobileAI Cloud proxies
|
|
410
|
-
proxyUrl="https://mobileai.cloud/api/v1/hosted-proxy/text"
|
|
411
|
-
voiceProxyUrl="wss://mobileai.cloud/ws/hosted-proxy/voice"
|
|
412
|
-
|
|
413
411
|
navRef={navRef}
|
|
414
412
|
screenMap={screenMap} // optional but recommended
|
|
415
413
|
>
|
|
@@ -435,9 +433,8 @@ export default function RootLayout() {
|
|
|
435
433
|
|
|
436
434
|
return (
|
|
437
435
|
<AIAgent
|
|
436
|
+
// Hosted MobileAI proxies are inferred automatically from analyticsKey
|
|
438
437
|
analyticsKey="mobileai_pub_xxxxxxxx"
|
|
439
|
-
proxyUrl="https://mobileai.cloud/api/v1/hosted-proxy/text"
|
|
440
|
-
voiceProxyUrl="wss://mobileai.cloud/ws/hosted-proxy/voice"
|
|
441
438
|
navRef={navRef}
|
|
442
439
|
screenMap={screenMap}
|
|
443
440
|
>
|
|
@@ -464,6 +461,19 @@ The examples above use **Gemini** (default). To use **OpenAI** for text mode, ad
|
|
|
464
461
|
|
|
465
462
|
A floating chat bar appears automatically. Ask the AI to navigate, tap buttons, fill forms, answer questions.
|
|
466
463
|
|
|
464
|
+
### Hosted MobileAI Defaults
|
|
465
|
+
|
|
466
|
+
For the standard MobileAI Cloud setup, this is enough:
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
<AIAgent analyticsKey="mobileai_pub_xxxxxxxx" navRef={navRef} />
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Only pass explicit proxy props when:
|
|
473
|
+
- you want to use your own backend proxy
|
|
474
|
+
- you want a dedicated voice proxy
|
|
475
|
+
- you are self-hosting the MobileAI backend
|
|
476
|
+
|
|
467
477
|
### Knowledge-Only Mode — AI Assistant Without UI Automation
|
|
468
478
|
|
|
469
479
|
Set `enableUIControl={false}` for a lightweight FAQ / support assistant. Single LLM call, ~70% fewer tokens:
|
|
@@ -871,9 +881,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
871
881
|
|------|------|---------|-------------|
|
|
872
882
|
| `apiKey` | `string` | — | API key for your provider (prototyping only — use `proxyUrl` in production). |
|
|
873
883
|
| `provider` | `'gemini' \| 'openai'` | `'gemini'` | LLM provider for text mode. |
|
|
874
|
-
| `proxyUrl` | `string` |
|
|
884
|
+
| `proxyUrl` | `string` | Hosted MobileAI text proxy when `analyticsKey` is set | Backend proxy URL (production). Routes all LLM traffic through your server. |
|
|
875
885
|
| `proxyHeaders` | `Record<string, string>` | — | Auth headers for proxy (e.g., `Authorization: Bearer ${token}`). |
|
|
876
|
-
| `voiceProxyUrl` | `string` |
|
|
886
|
+
| `voiceProxyUrl` | `string` | Hosted MobileAI voice proxy when `analyticsKey` is set; otherwise falls back to `proxyUrl` | Dedicated proxy for Voice Mode WebSockets. |
|
|
877
887
|
| `voiceProxyHeaders` | `Record<string, string>` | — | Auth headers for voice proxy. |
|
|
878
888
|
| `model` | `string` | Provider default | Model name (e.g. `gemini-2.5-flash`, `gpt-4.1-mini`). |
|
|
879
889
|
| `navRef` | `NavigationContainerRef` | — | Navigation ref for auto-navigation. |
|
|
@@ -953,7 +963,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
953
963
|
|
|
954
964
|
| Prop | Type | Default | Description |
|
|
955
965
|
|------|------|---------|-------------|
|
|
956
|
-
| `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics. |
|
|
966
|
+
| `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics and, by default, the hosted MobileAI text/voice proxies. |
|
|
957
967
|
| `analyticsProxyUrl` | `string` | — | Enterprise: route events through your backend. |
|
|
958
968
|
| `analyticsProxyHeaders` | `Record<string, string>` | — | Auth headers for analytics proxy. |
|
|
959
969
|
|
|
@@ -1065,7 +1075,7 @@ const { send } = useAI({
|
|
|
1065
1075
|
|
|
1066
1076
|
## 📊 Zero-Config Analytics — Auto-Capture Every Tap
|
|
1067
1077
|
|
|
1068
|
-
Just add `analyticsKey` — every button tap, screen navigation, and session is tracked automatically. **Zero code changes** to your app components.
|
|
1078
|
+
Just add `analyticsKey` — every button tap, screen navigation, and session is tracked automatically. It also enables the default hosted MobileAI AI proxies unless you override them. **Zero code changes** to your app components.
|
|
1069
1079
|
|
|
1070
1080
|
```tsx
|
|
1071
1081
|
<AIAgent
|
|
@@ -1116,6 +1126,8 @@ MobileAI.identify('user_123', { plan: 'pro' });
|
|
|
1116
1126
|
|
|
1117
1127
|
### Backend Proxy — Keep API Keys Secure
|
|
1118
1128
|
|
|
1129
|
+
Use this only when you want to override the default hosted MobileAI proxy behavior or route traffic through your own backend.
|
|
1130
|
+
|
|
1119
1131
|
```tsx
|
|
1120
1132
|
<AIAgent
|
|
1121
1133
|
proxyUrl="https://myapp.vercel.app/api/gemini"
|
|
@@ -1262,7 +1274,7 @@ Tag any element with `aiPriority` to control AI visibility:
|
|
|
1262
1274
|
| `date_picker(index, date)` | Set a date on a date picker |
|
|
1263
1275
|
| `navigate(screen)` | Navigate to any screen |
|
|
1264
1276
|
| `wait(seconds)` | Wait for loading states before acting |
|
|
1265
|
-
| `capture_screenshot(reason)` | Capture the
|
|
1277
|
+
| `capture_screenshot(reason)` | Capture the SDK root component as an image (requires `react-native-view-shot`) |
|
|
1266
1278
|
| `done(text)` | Finish the task with a response |
|
|
1267
1279
|
| `ask_user(question)` | Ask the user for clarification |
|
|
1268
1280
|
| `query_knowledge(question)` | Search the knowledge base |
|
package/android/build.gradle
CHANGED
|
@@ -10,6 +10,15 @@ buildscript {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
def isNewArchitectureEnabled() {
|
|
14
|
+
// Libraries opt into Fabric/TurboModule codegen only when the consuming app enables it.
|
|
15
|
+
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (isNewArchitectureEnabled()) {
|
|
19
|
+
apply plugin: "com.facebook.react"
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
apply plugin: "com.android.library"
|
|
14
23
|
apply plugin: "kotlin-android"
|
|
15
24
|
|
|
@@ -59,3 +68,11 @@ repositories {
|
|
|
59
68
|
dependencies {
|
|
60
69
|
implementation "com.facebook.react:react-android"
|
|
61
70
|
}
|
|
71
|
+
|
|
72
|
+
if (isNewArchitectureEnabled()) {
|
|
73
|
+
react {
|
|
74
|
+
jsRootDir = file("../")
|
|
75
|
+
libraryName = "RNMobileAIOverlaySpec"
|
|
76
|
+
codegenJavaPackageName = "com.mobileai.overlay"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
package com.mobileai.overlay
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.view.MotionEvent
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewConfiguration
|
|
8
|
+
import com.facebook.react.bridge.GuardedRunnable
|
|
9
|
+
import com.facebook.react.bridge.WritableMap
|
|
10
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
11
|
+
import com.facebook.react.common.annotations.UnstableReactNativeAPI
|
|
12
|
+
import com.facebook.react.common.build.ReactBuildConfig
|
|
13
|
+
import com.facebook.react.config.ReactFeatureFlags
|
|
14
|
+
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
15
|
+
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
16
|
+
import com.facebook.react.uimanager.PixelUtil.pxToDp
|
|
17
|
+
import com.facebook.react.uimanager.RootView
|
|
18
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
19
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
20
|
+
import com.facebook.react.uimanager.UIManagerModule
|
|
21
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
22
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
23
|
+
import kotlin.math.abs
|
|
24
|
+
import kotlin.math.roundToInt
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Root view rendered inside the Android floating panel dialog.
|
|
28
|
+
*
|
|
29
|
+
* This mirrors the touch and layout behavior of React Native's modal dialog
|
|
30
|
+
* host so JS children remain fully interactive even though they render inside
|
|
31
|
+
* a separate native window.
|
|
32
|
+
*/
|
|
33
|
+
internal class FloatingOverlayDialogRootViewGroup(context: Context) :
|
|
34
|
+
ReactViewGroup(context),
|
|
35
|
+
RootView {
|
|
36
|
+
|
|
37
|
+
var stateWrapper: StateWrapper? = null
|
|
38
|
+
var eventDispatcher: EventDispatcher? = null
|
|
39
|
+
var overlayHost: FloatingOverlayView? = null
|
|
40
|
+
|
|
41
|
+
private var viewWidth: Int = 0
|
|
42
|
+
private var viewHeight: Int = 0
|
|
43
|
+
private val jsTouchDispatcher = JSTouchDispatcher(this)
|
|
44
|
+
private var jsPointerDispatcher: JSPointerDispatcher? = null
|
|
45
|
+
private val touchSlop: Float = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
|
|
46
|
+
private var dragCandidate: Boolean = false
|
|
47
|
+
private var isDraggingWindow: Boolean = false
|
|
48
|
+
private var dragStartRawX: Float = 0f
|
|
49
|
+
private var dragStartRawY: Float = 0f
|
|
50
|
+
private var dragStartWindowX: Int = 0
|
|
51
|
+
private var dragStartWindowY: Int = 0
|
|
52
|
+
|
|
53
|
+
private val reactContext: ThemedReactContext
|
|
54
|
+
get() = context as ThemedReactContext
|
|
55
|
+
|
|
56
|
+
init {
|
|
57
|
+
if (ReactFeatureFlags.dispatchPointerEvents) {
|
|
58
|
+
jsPointerDispatcher = JSPointerDispatcher(this)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
63
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
64
|
+
viewWidth = w
|
|
65
|
+
viewHeight = h
|
|
66
|
+
updateState(viewWidth, viewHeight)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fun updateState(width: Int, height: Int) {
|
|
70
|
+
val realWidth = width.toFloat().pxToDp()
|
|
71
|
+
val realHeight = height.toFloat().pxToDp()
|
|
72
|
+
|
|
73
|
+
val wrapper = stateWrapper
|
|
74
|
+
if (wrapper != null) {
|
|
75
|
+
val newStateData: WritableMap = WritableNativeMap()
|
|
76
|
+
newStateData.putDouble("screenWidth", realWidth.toDouble())
|
|
77
|
+
newStateData.putDouble("screenHeight", realHeight.toDouble())
|
|
78
|
+
wrapper.updateState(newStateData)
|
|
79
|
+
} else if (
|
|
80
|
+
!ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE &&
|
|
81
|
+
!reactContext.isBridgeless
|
|
82
|
+
) {
|
|
83
|
+
@Suppress("DEPRECATION")
|
|
84
|
+
reactContext.runOnNativeModulesQueueThread(
|
|
85
|
+
object : GuardedRunnable(reactContext) {
|
|
86
|
+
override fun runGuarded() {
|
|
87
|
+
reactContext.reactApplicationContext
|
|
88
|
+
.getNativeModule(UIManagerModule::class.java)
|
|
89
|
+
?.updateNodeSize(id, viewWidth, viewHeight)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override fun handleException(t: Throwable) {
|
|
97
|
+
reactContext.reactApplicationContext.handleException(RuntimeException(t))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
|
101
|
+
when (event.actionMasked) {
|
|
102
|
+
MotionEvent.ACTION_DOWN -> {
|
|
103
|
+
dragCandidate = isInNativeDragRegion(event.x, event.y)
|
|
104
|
+
isDraggingWindow = false
|
|
105
|
+
if (dragCandidate) {
|
|
106
|
+
dragStartRawX = event.rawX
|
|
107
|
+
dragStartRawY = event.rawY
|
|
108
|
+
dragStartWindowX = overlayHost?.getWindowXPx() ?: 0
|
|
109
|
+
dragStartWindowY = overlayHost?.getWindowYPx() ?: 0
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
MotionEvent.ACTION_MOVE -> {
|
|
114
|
+
if (dragCandidate && !isDraggingWindow && hasExceededTouchSlop(event)) {
|
|
115
|
+
isDraggingWindow = true
|
|
116
|
+
parent?.requestDisallowInterceptTouchEvent(true)
|
|
117
|
+
updateNativeWindowDrag(event)
|
|
118
|
+
return true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
MotionEvent.ACTION_UP,
|
|
123
|
+
MotionEvent.ACTION_CANCEL -> resetNativeDrag()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (isDraggingWindow) return true
|
|
127
|
+
|
|
128
|
+
eventDispatcher?.let { dispatcher ->
|
|
129
|
+
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
|
|
130
|
+
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
|
|
131
|
+
}
|
|
132
|
+
return super.onInterceptTouchEvent(event)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
136
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
137
|
+
if (isDraggingWindow || dragCandidate) {
|
|
138
|
+
when (event.actionMasked) {
|
|
139
|
+
MotionEvent.ACTION_MOVE -> {
|
|
140
|
+
if (!isDraggingWindow && hasExceededTouchSlop(event)) {
|
|
141
|
+
isDraggingWindow = true
|
|
142
|
+
parent?.requestDisallowInterceptTouchEvent(true)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isDraggingWindow) {
|
|
146
|
+
updateNativeWindowDrag(event)
|
|
147
|
+
return true
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
MotionEvent.ACTION_UP -> {
|
|
152
|
+
if (isDraggingWindow) {
|
|
153
|
+
updateNativeWindowDrag(event)
|
|
154
|
+
overlayHost?.emitWindowDragEnd()
|
|
155
|
+
resetNativeDrag()
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
resetNativeDrag()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
162
|
+
if (isDraggingWindow) {
|
|
163
|
+
overlayHost?.emitWindowDragEnd()
|
|
164
|
+
resetNativeDrag()
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
resetNativeDrag()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
eventDispatcher?.let { dispatcher ->
|
|
173
|
+
jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
|
|
174
|
+
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
|
|
175
|
+
}
|
|
176
|
+
super.onTouchEvent(event)
|
|
177
|
+
return true
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
|
|
181
|
+
eventDispatcher?.let { dispatcher ->
|
|
182
|
+
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
|
|
183
|
+
}
|
|
184
|
+
return super.onInterceptHoverEvent(event)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
override fun onHoverEvent(event: MotionEvent): Boolean {
|
|
188
|
+
eventDispatcher?.let { dispatcher ->
|
|
189
|
+
jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
|
|
190
|
+
}
|
|
191
|
+
return super.onHoverEvent(event)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@OptIn(UnstableReactNativeAPI::class)
|
|
195
|
+
override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
|
|
196
|
+
eventDispatcher?.let { dispatcher ->
|
|
197
|
+
jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher, reactContext)
|
|
198
|
+
jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
|
|
203
|
+
eventDispatcher?.let { dispatcher ->
|
|
204
|
+
jsTouchDispatcher.onChildEndedNativeGesture(ev, dispatcher)
|
|
205
|
+
}
|
|
206
|
+
jsPointerDispatcher?.onChildEndedNativeGesture()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
|
|
210
|
+
// No-op so the root can continue receiving events for JS dispatch.
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fun hasExceededTouchSlop(event: MotionEvent): Boolean {
|
|
214
|
+
return abs(event.rawX - dragStartRawX) > touchSlop || abs(event.rawY - dragStartRawY) > touchSlop
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private fun updateNativeWindowDrag(event: MotionEvent) {
|
|
218
|
+
val targetX = dragStartWindowX + (event.rawX - dragStartRawX).roundToInt()
|
|
219
|
+
val targetY = dragStartWindowY + (event.rawY - dragStartRawY).roundToInt()
|
|
220
|
+
overlayHost?.updateWindowPositionFromNative(targetX, targetY)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private fun resetNativeDrag() {
|
|
224
|
+
dragCandidate = false
|
|
225
|
+
isDraggingWindow = false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private fun isInNativeDragRegion(x: Float, y: Float): Boolean {
|
|
229
|
+
if (viewWidth <= dpToPx(80) && viewHeight <= dpToPx(80)) {
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
val handleTop = dpToPx(44)
|
|
234
|
+
val handleHalfWidth = dpToPx(72)
|
|
235
|
+
val centerX = viewWidth / 2f
|
|
236
|
+
|
|
237
|
+
return y <= handleTop && x in (centerX - handleHalfWidth)..(centerX + handleHalfWidth)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private fun dpToPx(value: Int): Float {
|
|
241
|
+
return value * resources.displayMetrics.density
|
|
242
|
+
}
|
|
243
|
+
}
|