@healthcloudai/hc-cameravitals-connector 0.2.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 +163 -0
- package/android/build.gradle +37 -0
- package/android/src/main/java/expo/modules/circadify/CircadifyModule.kt +202 -0
- package/android/src/main/java/expo/modules/circadify/CircadifyPreviewView.kt +60 -0
- package/dist/index.cjs +2073 -0
- package/dist/index.d.cts +294 -0
- package/dist/index.d.ts +294 -0
- package/dist/index.js +2047 -0
- package/expo-module.config.json +11 -0
- package/ios/CircadifyModule.podspec +30 -0
- package/ios/CircadifyModule.swift +154 -0
- package/ios/CircadifyPreview.swift +77 -0
- package/package.json +78 -0
- package/plugin.cjs +443 -0
- package/src/CameraVitalsNativeModule.ts +4 -0
- package/src/CameraVitalsPreview.tsx +97 -0
- package/src/CameraVitalsPreviewView.ts +14 -0
- package/src/CameraVitalsScan.tsx +634 -0
- package/src/index.ts +43 -0
- package/src/internal/CameraVitalsAdapter.ts +25 -0
- package/src/internal/circadify/circadifyAdapter.android.ts +47 -0
- package/src/internal/circadify/circadifyAdapter.ios.ts +43 -0
- package/src/internal/circadify/circadifyAdapter.web.ts +333 -0
- package/src/internal/circadify/vendor/circadify-web-sdk.mjs +3 -0
- package/src/internal/circadify/webPreview.ts +46 -0
- package/src/internal/demographics.ts +20 -0
- package/src/theme/CameraVitalsThemeProvider.tsx +76 -0
- package/src/theme/tokens.ts +96 -0
- package/src/types.ts +114 -0
- package/src/useCameraVitals.ts +131 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# @healthcloudai/hc-cameravitals-connector
|
|
2
|
+
|
|
3
|
+
Camera-based vital signs measurement for React Native (Expo). Measure heart rate, HRV, respiratory rate, and SpO2 using the device camera — with a single `<CameraVitalsScan>` component that works on iOS, Android, and Web.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import {
|
|
7
|
+
CameraVitalsScan,
|
|
8
|
+
CameraVitalsThemeProvider,
|
|
9
|
+
} from "@healthcloudai/hc-cameravitals-connector";
|
|
10
|
+
|
|
11
|
+
export default function App() {
|
|
12
|
+
return (
|
|
13
|
+
<CameraVitalsThemeProvider>
|
|
14
|
+
<CameraVitalsScan
|
|
15
|
+
apiKey="your-api-key"
|
|
16
|
+
onResult={(result) => console.log("Vitals:", result)}
|
|
17
|
+
/>
|
|
18
|
+
</CameraVitalsThemeProvider>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Zero config** — Expo config plugin handles ALL native setup
|
|
26
|
+
- **Single component** — `<CameraVitalsScan>` works on iOS, Android, and Web
|
|
27
|
+
- **Vendor-transparent** — Public API uses generic names (`CameraVitalsConfig`, `CameraVitalsResult`); the underlying SDK is an implementation detail
|
|
28
|
+
- **Headless hook** — `useCameraVitals()` gives you full control over measurement lifecycle
|
|
29
|
+
- **Themable** — `CameraVitalsThemeProvider` + design tokens for complete visual customization
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @healthcloudai/hc-cameravitals-connector
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Expo Config Plugin
|
|
38
|
+
|
|
39
|
+
Add the plugin to your `app.json`:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"expo": {
|
|
44
|
+
"plugins": [
|
|
45
|
+
"@healthcloudai/hc-cameravitals-connector"
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This automatically:
|
|
52
|
+
- Adds camera permissions (iOS + Android)
|
|
53
|
+
- Configures the Circadify SDK dependency (Android Maven repo + Gradle dep)
|
|
54
|
+
- Adds linker flags and SPM package references (iOS)
|
|
55
|
+
- Injects RocketSim support for iOS Simulator (debug builds)
|
|
56
|
+
|
|
57
|
+
After adding the plugin, rebuild your native apps:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx expo prebuild --clean
|
|
61
|
+
npx expo run:ios # or run:android
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Web
|
|
65
|
+
|
|
66
|
+
Web measurement requires `@circadify/web-sdk` (optional peer dependency):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install @circadify/web-sdk
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
> **Note**: `@circadify/web-sdk` is hosted on GitHub Packages. Configure a `.npmrc` with your GitHub token:
|
|
73
|
+
> ```
|
|
74
|
+
> @circadify:registry=https://npm.pkg.github.com
|
|
75
|
+
> //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
|
|
76
|
+
> ```
|
|
77
|
+
|
|
78
|
+
## API
|
|
79
|
+
|
|
80
|
+
### Components
|
|
81
|
+
|
|
82
|
+
#### `<CameraVitalsScan>`
|
|
83
|
+
|
|
84
|
+
All-in-one scan component with four screens: idle → scanning → results → error.
|
|
85
|
+
|
|
86
|
+
| Prop | Type | Required | Description |
|
|
87
|
+
|------|------|----------|-------------|
|
|
88
|
+
| `apiKey` | `string` | Yes | API key for the vital signs SDK |
|
|
89
|
+
| `onResult` | `(result: CameraVitalsResult) => void` | No | Called on successful measurement |
|
|
90
|
+
| `onError` | `(error: Error) => void` | No | Called on fatal error |
|
|
91
|
+
|
|
92
|
+
#### `<CameraVitalsPreview>`
|
|
93
|
+
|
|
94
|
+
Low-level camera preview. Renders the native viewfinder on iOS/Android.
|
|
95
|
+
|
|
96
|
+
#### `<CameraVitalsThemeProvider>`
|
|
97
|
+
|
|
98
|
+
Theme context provider. Wrap your app to customize colors, spacing, radii, fonts, and dimensions.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import {
|
|
102
|
+
CameraVitalsThemeProvider,
|
|
103
|
+
DEFAULT_THEME,
|
|
104
|
+
mergeCameraVitalsTheme,
|
|
105
|
+
} from "@healthcloudai/hc-cameravitals-connector";
|
|
106
|
+
|
|
107
|
+
const myTheme = mergeCameraVitalsTheme(DEFAULT_THEME, {
|
|
108
|
+
colors: { accent: "#FF6B35" },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function App() {
|
|
112
|
+
return (
|
|
113
|
+
<CameraVitalsThemeProvider value={myTheme}>
|
|
114
|
+
<CameraVitalsScan apiKey="..." />
|
|
115
|
+
</CameraVitalsThemeProvider>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Hooks
|
|
121
|
+
|
|
122
|
+
#### `useCameraVitals()`
|
|
123
|
+
|
|
124
|
+
Headless hook. Returns a `CameraVitalsAPI` object:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const {
|
|
128
|
+
start, // (config, options?) => Promise<CameraVitalsResult>
|
|
129
|
+
stop, // () => Promise<void>
|
|
130
|
+
progress, // ProgressUpdate | null
|
|
131
|
+
quality, // QualityWarning[]
|
|
132
|
+
isScanning, // boolean
|
|
133
|
+
result, // CameraVitalsResult | null
|
|
134
|
+
error, // CameraVitalsError | null
|
|
135
|
+
} = useCameraVitals();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Types
|
|
139
|
+
|
|
140
|
+
| Type | Description |
|
|
141
|
+
|------|-------------|
|
|
142
|
+
| `CameraVitalsConfig` | `{ apiKey, demographics? }` |
|
|
143
|
+
| `CameraVitalsResult` | `{ heartRate, hrv?, respiratoryRate?, spo2?, systolicBp?, diastolicBp?, confidence? }` |
|
|
144
|
+
| `CameraVitalsError` | `{ code, message }` |
|
|
145
|
+
| `Demographics` | `{ age?, sex?: "male" \| "female", fitzpatrick?: 1-6 }` |
|
|
146
|
+
| `MeasurementOptions` | `{ demographics?, onProgress?, onQualityWarning? }` |
|
|
147
|
+
| `ProgressUpdate` | `{ percent, phase, elapsedSeconds }` |
|
|
148
|
+
| `QualityWarning` | `{ type, message, severity }` |
|
|
149
|
+
| `CameraVitalsAPI` | Return type of `useCameraVitals()` |
|
|
150
|
+
|
|
151
|
+
## Requirements
|
|
152
|
+
|
|
153
|
+
| Platform | Minimum Version |
|
|
154
|
+
|----------|----------------|
|
|
155
|
+
| iOS | 17.0+ |
|
|
156
|
+
| Android | API 24+ (Android 7.0) |
|
|
157
|
+
| Expo SDK | 52+ |
|
|
158
|
+
| React Native | 0.72+ |
|
|
159
|
+
| React | 18–19 |
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
apply plugin: 'kotlin-android'
|
|
3
|
+
|
|
4
|
+
group = "expo.modules.circadify"
|
|
5
|
+
version = "0.1.0"
|
|
6
|
+
|
|
7
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
8
|
+
apply from: expoModulesCorePlugin
|
|
9
|
+
applyKotlinExpoModulesCorePlugin()
|
|
10
|
+
useCoreDependencies()
|
|
11
|
+
useDefaultAndroidSdkVersions()
|
|
12
|
+
useExpoPublishing()
|
|
13
|
+
|
|
14
|
+
android {
|
|
15
|
+
namespace "expo.modules.circadify"
|
|
16
|
+
compileSdk rootProject.ext.compileSdkVersion
|
|
17
|
+
defaultConfig {
|
|
18
|
+
minSdk rootProject.ext.minSdkVersion
|
|
19
|
+
versionCode 1
|
|
20
|
+
versionName "0.1.0"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
compileOptions {
|
|
24
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
25
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
kotlinOptions {
|
|
29
|
+
jvmTarget = "17"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
dependencies {
|
|
34
|
+
implementation("com.circadify:circadify-android-sdk:0.1.1")
|
|
35
|
+
implementation("androidx.camera:camera-view:1.3.4")
|
|
36
|
+
implementation("androidx.camera:camera-lifecycle:1.3.4")
|
|
37
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
package expo.modules.circadify
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import androidx.camera.view.PreviewView
|
|
7
|
+
import androidx.core.content.ContextCompat
|
|
8
|
+
import com.circadify.sdk.CircadifyCallbacks
|
|
9
|
+
import com.circadify.sdk.CircadifyConfig
|
|
10
|
+
import com.circadify.sdk.CircadifyError
|
|
11
|
+
import com.circadify.sdk.CircadifySDK
|
|
12
|
+
import com.circadify.sdk.Demographics
|
|
13
|
+
import com.circadify.sdk.MeasurementOptions
|
|
14
|
+
import com.circadify.sdk.ProgressEvent
|
|
15
|
+
import com.circadify.sdk.QualityState
|
|
16
|
+
import com.circadify.sdk.QualityWarning
|
|
17
|
+
import com.circadify.sdk.Sex
|
|
18
|
+
import expo.modules.kotlin.Promise
|
|
19
|
+
import expo.modules.kotlin.modules.Module
|
|
20
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
21
|
+
import kotlinx.coroutines.CoroutineScope
|
|
22
|
+
import kotlinx.coroutines.Dispatchers
|
|
23
|
+
import kotlinx.coroutines.launch
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Expo native module wrapping the Circadify Android SDK.
|
|
27
|
+
*
|
|
28
|
+
* Usage from JS:
|
|
29
|
+
* const result = await CircadifyModule.measureVitals("ck_live_...", {});
|
|
30
|
+
*/
|
|
31
|
+
class CircadifyModule : Module() {
|
|
32
|
+
companion object {
|
|
33
|
+
private const val TAG = "CircadifyModule"
|
|
34
|
+
|
|
35
|
+
/** The currently mounted [PreviewView], set by [CircadifyPreviewView]. */
|
|
36
|
+
var currentPreview: PreviewView? = null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private var sdk: CircadifySDK? = null
|
|
40
|
+
private val scope = CoroutineScope(Dispatchers.Main)
|
|
41
|
+
|
|
42
|
+
override fun definition() = ModuleDefinition {
|
|
43
|
+
Name("CircadifyModule")
|
|
44
|
+
|
|
45
|
+
View(CircadifyPreviewView::class) { }
|
|
46
|
+
|
|
47
|
+
Events("progress", "qualityWarning", "qualityState")
|
|
48
|
+
|
|
49
|
+
AsyncFunction("measureVitals") { apiKey: String, demographicsMap: Map<String, Any>?, promise: Promise ->
|
|
50
|
+
scope.launch {
|
|
51
|
+
try {
|
|
52
|
+
val activity = appContext.currentActivity
|
|
53
|
+
if (activity == null) {
|
|
54
|
+
promise.reject("NO_ACTIVITY", "No current activity available", null)
|
|
55
|
+
return@launch
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!hasCameraPermission()) {
|
|
59
|
+
promise.reject(
|
|
60
|
+
"CAMERA_PERMISSION_DENIED",
|
|
61
|
+
"Camera permission is required for vital sign measurement",
|
|
62
|
+
null
|
|
63
|
+
)
|
|
64
|
+
return@launch
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
val previewForSdk = currentPreview
|
|
68
|
+
Log.d(TAG, "measureVitals: currentPreview=${previewForSdk}, " +
|
|
69
|
+
"surfaceProvider=${previewForSdk?.surfaceProvider}, " +
|
|
70
|
+
"implementationMode=${previewForSdk?.implementationMode}")
|
|
71
|
+
|
|
72
|
+
val callbacks = CircadifyCallbacks(
|
|
73
|
+
onProgress = { event: ProgressEvent ->
|
|
74
|
+
Log.d(TAG, "Progress: ${event.percent}% phase=${event.phase}")
|
|
75
|
+
sendEvent("progress", mapOf(
|
|
76
|
+
"percent" to event.percent,
|
|
77
|
+
"phase" to event.phase.name.lowercase(),
|
|
78
|
+
"elapsedSeconds" to event.elapsedSeconds
|
|
79
|
+
))
|
|
80
|
+
},
|
|
81
|
+
onQualityWarning = { warning: QualityWarning ->
|
|
82
|
+
Log.d(TAG, "Quality warning: ${warning.message}")
|
|
83
|
+
sendEvent("qualityWarning", mapOf(
|
|
84
|
+
"type" to warning.type.name.lowercase(),
|
|
85
|
+
"message" to warning.message,
|
|
86
|
+
"severity" to warning.severity.name.lowercase()
|
|
87
|
+
))
|
|
88
|
+
},
|
|
89
|
+
onQualityState = { state: QualityState ->
|
|
90
|
+
sendEvent("qualityState", mapOf(
|
|
91
|
+
"lighting" to mapOf(
|
|
92
|
+
"brightness" to state.lighting.brightness,
|
|
93
|
+
"isOk" to state.lighting.isOk,
|
|
94
|
+
"isTooDark" to state.lighting.isTooDark
|
|
95
|
+
),
|
|
96
|
+
"motion" to mapOf(
|
|
97
|
+
"magnitude" to state.motion.motionMagnitude,
|
|
98
|
+
"isStill" to state.motion.isStill
|
|
99
|
+
),
|
|
100
|
+
"pose" to mapOf(
|
|
101
|
+
"yaw" to state.pose.yaw,
|
|
102
|
+
"pitch" to state.pose.pitch,
|
|
103
|
+
"isFacingForward" to state.pose.isFacingForward
|
|
104
|
+
),
|
|
105
|
+
"isReady" to state.isReady,
|
|
106
|
+
"messages" to state.messages
|
|
107
|
+
))
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
val config = CircadifyConfig(
|
|
112
|
+
apiKey = apiKey,
|
|
113
|
+
debug = true,
|
|
114
|
+
callbacks = callbacks
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
sdk = CircadifySDK(
|
|
118
|
+
context = appContext.reactContext ?: activity,
|
|
119
|
+
config = config
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
val demographics = buildDemographics(demographicsMap)
|
|
123
|
+
|
|
124
|
+
val result = sdk!!.measureVitals(
|
|
125
|
+
MeasurementOptions(
|
|
126
|
+
lifecycleOwner = activity as androidx.lifecycle.LifecycleOwner,
|
|
127
|
+
previewView = previewForSdk,
|
|
128
|
+
demographics = demographics
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Build result map — only include keys for values that exist
|
|
133
|
+
val resultMap = mutableMapOf<String, Any?>(
|
|
134
|
+
"heartRate" to result.heartRate
|
|
135
|
+
)
|
|
136
|
+
result.confidence?.let { resultMap["confidence"] = it }
|
|
137
|
+
result.hrv?.let { resultMap["hrv"] = it }
|
|
138
|
+
result.respiratoryRate?.let { resultMap["respiratoryRate"] = it }
|
|
139
|
+
result.spo2?.let { resultMap["spo2"] = it }
|
|
140
|
+
result.systolicBp?.let { resultMap["systolicBp"] = it }
|
|
141
|
+
result.diastolicBp?.let { resultMap["diastolicBp"] = it }
|
|
142
|
+
promise.resolve(resultMap)
|
|
143
|
+
} catch (e: CircadifyError) {
|
|
144
|
+
promise.reject(
|
|
145
|
+
"CIRCADIFY_ERROR",
|
|
146
|
+
"${e.code}: ${e.message}",
|
|
147
|
+
e
|
|
148
|
+
)
|
|
149
|
+
} catch (e: Exception) {
|
|
150
|
+
promise.reject(
|
|
151
|
+
"MEASUREMENT_FAILED",
|
|
152
|
+
e.message ?: "Unknown measurement error",
|
|
153
|
+
e
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
AsyncFunction("hasCameraPermission") { promise: Promise ->
|
|
160
|
+
promise.resolve(hasCameraPermission())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
AsyncFunction("requestCameraPermission") { promise: Promise ->
|
|
164
|
+
// Android camera permission uses ActivityResultLauncher which
|
|
165
|
+
// requires a registry tied to the host Activity/Fragment.
|
|
166
|
+
// Expo Modules don't expose launcher registration APIs yet.
|
|
167
|
+
// As a graceful fallback we check current status — consumers
|
|
168
|
+
// should ensure camera permission is pre-granted on Android
|
|
169
|
+
// (the Expo config plugin adds CAMERA permission by default).
|
|
170
|
+
Log.w(TAG, "requestCameraPermission: ActivityResultLauncher not available in Expo Module; returning current status")
|
|
171
|
+
promise.resolve(hasCameraPermission())
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
AsyncFunction("cancel") { promise: Promise ->
|
|
175
|
+
scope.launch {
|
|
176
|
+
try {
|
|
177
|
+
sdk?.cancel()
|
|
178
|
+
promise.resolve(null)
|
|
179
|
+
} catch (e: Exception) {
|
|
180
|
+
promise.reject("CANCEL_FAILED", e.message, e)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private fun hasCameraPermission(): Boolean {
|
|
187
|
+
val context = appContext.reactContext ?: return false
|
|
188
|
+
return ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
|
189
|
+
PackageManager.PERMISSION_GRANTED
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun buildDemographics(map: Map<String, Any>?): Demographics? {
|
|
193
|
+
if (map.isNullOrEmpty()) return null
|
|
194
|
+
return Demographics(
|
|
195
|
+
age = (map["age"] as? Number)?.toInt(),
|
|
196
|
+
sex = (map["sex"] as? String)?.let {
|
|
197
|
+
try { Sex.valueOf(it) } catch (_: Exception) { null }
|
|
198
|
+
},
|
|
199
|
+
fitzpatrick = (map["fitzpatrick"] as? Number)?.toInt()
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package expo.modules.circadify
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Outline
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.view.ViewGroup
|
|
8
|
+
import android.view.ViewOutlineProvider
|
|
9
|
+
import androidx.camera.view.PreviewView
|
|
10
|
+
import expo.modules.kotlin.AppContext
|
|
11
|
+
import expo.modules.kotlin.views.ExpoView
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Expo native view wrapping a CameraX [PreviewView].
|
|
15
|
+
*
|
|
16
|
+
* When mounted in the React Native view tree the [previewView] is registered
|
|
17
|
+
* as [CircadifyModule.currentPreview] so the measurement flow can bind
|
|
18
|
+
* CameraX to it. On unmount the reference is cleared.
|
|
19
|
+
*
|
|
20
|
+
* Usage from JS:
|
|
21
|
+
* <CircadifyPreviewView style={{ flex: 1 }} />
|
|
22
|
+
*/
|
|
23
|
+
class CircadifyPreviewView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
24
|
+
|
|
25
|
+
override val shouldUseAndroidLayout: Boolean = true
|
|
26
|
+
|
|
27
|
+
val previewView: PreviewView = PreviewView(context).apply {
|
|
28
|
+
layoutParams = ViewGroup.LayoutParams(
|
|
29
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
30
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
31
|
+
)
|
|
32
|
+
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
|
|
33
|
+
scaleType = PreviewView.ScaleType.FILL_CENTER
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
init {
|
|
37
|
+
clipToPadding = true
|
|
38
|
+
addView(previewView)
|
|
39
|
+
CircadifyModule.currentPreview = previewView
|
|
40
|
+
Log.d("CircadifyPreviewView", "PreviewView registered: " +
|
|
41
|
+
"implMode=${previewView.implementationMode}, " +
|
|
42
|
+
"size=${previewView.width}x${previewView.height}, " +
|
|
43
|
+
"surfaceProvider=${previewView.surfaceProvider}")
|
|
44
|
+
|
|
45
|
+
clipToOutline = true
|
|
46
|
+
outlineProvider = object : ViewOutlineProvider() {
|
|
47
|
+
override fun getOutline(view: View, outline: Outline) {
|
|
48
|
+
val radius = 14f * resources.displayMetrics.density
|
|
49
|
+
outline.setRoundRect(0, 0, view.width, view.height, radius)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun onDetachedFromWindow() {
|
|
55
|
+
super.onDetachedFromWindow()
|
|
56
|
+
if (CircadifyModule.currentPreview == previewView) {
|
|
57
|
+
CircadifyModule.currentPreview = null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|