@mobileai/react-native 0.9.27 → 0.9.28
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 +24 -11
- 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/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 +405 -168
- 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 +103 -1
- package/lib/module/core/FiberTreeWalker.js +98 -0
- 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 +45 -35
- 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 +9 -0
- 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 +35 -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/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 +5 -2
- package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
- package/ios/MobileAIFloatingOverlayComponentView.mm +0 -73
- package/ios/MobileAIPilotIntents.swift +0 -51
package/README.md
CHANGED
|
@@ -391,6 +391,8 @@ npx @mobileai/react-native generate-map
|
|
|
391
391
|
|
|
392
392
|
### 2. Wrap Your App
|
|
393
393
|
|
|
394
|
+
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.
|
|
395
|
+
|
|
394
396
|
#### React Navigation
|
|
395
397
|
|
|
396
398
|
```tsx
|
|
@@ -403,13 +405,10 @@ export default function App() {
|
|
|
403
405
|
|
|
404
406
|
return (
|
|
405
407
|
<AIAgent
|
|
406
|
-
// Your MobileAI Dashboard ID
|
|
408
|
+
// Your MobileAI Dashboard ID
|
|
409
|
+
// This now auto-configures the hosted MobileAI text + voice proxies too.
|
|
407
410
|
analyticsKey="mobileai_pub_xxxxxxxx"
|
|
408
411
|
|
|
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
412
|
navRef={navRef}
|
|
414
413
|
screenMap={screenMap} // optional but recommended
|
|
415
414
|
>
|
|
@@ -435,9 +434,8 @@ export default function RootLayout() {
|
|
|
435
434
|
|
|
436
435
|
return (
|
|
437
436
|
<AIAgent
|
|
437
|
+
// Hosted MobileAI proxies are inferred automatically from analyticsKey
|
|
438
438
|
analyticsKey="mobileai_pub_xxxxxxxx"
|
|
439
|
-
proxyUrl="https://mobileai.cloud/api/v1/hosted-proxy/text"
|
|
440
|
-
voiceProxyUrl="wss://mobileai.cloud/ws/hosted-proxy/voice"
|
|
441
439
|
navRef={navRef}
|
|
442
440
|
screenMap={screenMap}
|
|
443
441
|
>
|
|
@@ -464,6 +462,19 @@ The examples above use **Gemini** (default). To use **OpenAI** for text mode, ad
|
|
|
464
462
|
|
|
465
463
|
A floating chat bar appears automatically. Ask the AI to navigate, tap buttons, fill forms, answer questions.
|
|
466
464
|
|
|
465
|
+
### Hosted MobileAI Defaults
|
|
466
|
+
|
|
467
|
+
For the standard MobileAI Cloud setup, this is enough:
|
|
468
|
+
|
|
469
|
+
```tsx
|
|
470
|
+
<AIAgent analyticsKey="mobileai_pub_xxxxxxxx" navRef={navRef} />
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Only pass explicit proxy props when:
|
|
474
|
+
- you want to use your own backend proxy
|
|
475
|
+
- you want a dedicated voice proxy
|
|
476
|
+
- you are self-hosting the MobileAI backend
|
|
477
|
+
|
|
467
478
|
### Knowledge-Only Mode — AI Assistant Without UI Automation
|
|
468
479
|
|
|
469
480
|
Set `enableUIControl={false}` for a lightweight FAQ / support assistant. Single LLM call, ~70% fewer tokens:
|
|
@@ -871,9 +882,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
871
882
|
|------|------|---------|-------------|
|
|
872
883
|
| `apiKey` | `string` | — | API key for your provider (prototyping only — use `proxyUrl` in production). |
|
|
873
884
|
| `provider` | `'gemini' \| 'openai'` | `'gemini'` | LLM provider for text mode. |
|
|
874
|
-
| `proxyUrl` | `string` |
|
|
885
|
+
| `proxyUrl` | `string` | Hosted MobileAI text proxy when `analyticsKey` is set | Backend proxy URL (production). Routes all LLM traffic through your server. |
|
|
875
886
|
| `proxyHeaders` | `Record<string, string>` | — | Auth headers for proxy (e.g., `Authorization: Bearer ${token}`). |
|
|
876
|
-
| `voiceProxyUrl` | `string` |
|
|
887
|
+
| `voiceProxyUrl` | `string` | Hosted MobileAI voice proxy when `analyticsKey` is set; otherwise falls back to `proxyUrl` | Dedicated proxy for Voice Mode WebSockets. |
|
|
877
888
|
| `voiceProxyHeaders` | `Record<string, string>` | — | Auth headers for voice proxy. |
|
|
878
889
|
| `model` | `string` | Provider default | Model name (e.g. `gemini-2.5-flash`, `gpt-4.1-mini`). |
|
|
879
890
|
| `navRef` | `NavigationContainerRef` | — | Navigation ref for auto-navigation. |
|
|
@@ -953,7 +964,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
953
964
|
|
|
954
965
|
| Prop | Type | Default | Description |
|
|
955
966
|
|------|------|---------|-------------|
|
|
956
|
-
| `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics. |
|
|
967
|
+
| `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics and, by default, the hosted MobileAI text/voice proxies. |
|
|
957
968
|
| `analyticsProxyUrl` | `string` | — | Enterprise: route events through your backend. |
|
|
958
969
|
| `analyticsProxyHeaders` | `Record<string, string>` | — | Auth headers for analytics proxy. |
|
|
959
970
|
|
|
@@ -1065,7 +1076,7 @@ const { send } = useAI({
|
|
|
1065
1076
|
|
|
1066
1077
|
## 📊 Zero-Config Analytics — Auto-Capture Every Tap
|
|
1067
1078
|
|
|
1068
|
-
Just add `analyticsKey` — every button tap, screen navigation, and session is tracked automatically. **Zero code changes** to your app components.
|
|
1079
|
+
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
1080
|
|
|
1070
1081
|
```tsx
|
|
1071
1082
|
<AIAgent
|
|
@@ -1116,6 +1127,8 @@ MobileAI.identify('user_123', { plan: 'pro' });
|
|
|
1116
1127
|
|
|
1117
1128
|
### Backend Proxy — Keep API Keys Secure
|
|
1118
1129
|
|
|
1130
|
+
Use this only when you want to override the default hosted MobileAI proxy behavior or route traffic through your own backend.
|
|
1131
|
+
|
|
1119
1132
|
```tsx
|
|
1120
1133
|
<AIAgent
|
|
1121
1134
|
proxyUrl="https://myapp.vercel.app/api/gemini"
|
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
|
+
}
|