@expo/ui 57.0.10 → 57.0.12
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/CHANGELOG.md +28 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/ui/ExpoUIModule.kt +21 -1
- package/android/src/main/java/expo/modules/ui/graphics/DrawablePainter.kt +43 -0
- package/android/src/main/java/expo/modules/ui/{icon/VectorIconLoader.kt → graphics/ImageLoader.kt} +122 -87
- package/android/src/main/java/expo/modules/ui/graphics/ImageSource.kt +28 -0
- package/android/src/main/java/expo/modules/ui/{icon → graphics}/ResourceIdHelper.kt +1 -1
- package/android/src/main/java/expo/modules/ui/icon/IconView.kt +7 -78
- package/android/src/main/java/expo/modules/ui/image/ImageView.kt +136 -0
- package/build/State/useWorkletProp.d.ts.map +1 -1
- package/build/jetpack-compose/Image/index.d.ts +107 -0
- package/build/jetpack-compose/Image/index.d.ts.map +1 -0
- package/build/jetpack-compose/index.d.ts +1 -0
- package/build/jetpack-compose/index.d.ts.map +1 -1
- package/build/swift-ui/Alert/index.d.ts.map +1 -1
- package/build/swift-ui/ConfirmationDialog/index.d.ts.map +1 -1
- package/build/swift-ui/Overlay/index.d.ts.map +1 -1
- package/build/swift-ui/Popover/index.d.ts.map +1 -1
- package/build/swift-ui/Slider/index.d.ts.map +1 -1
- package/build/swift-ui/modifiers/index.d.ts +61 -9
- package/build/swift-ui/modifiers/index.d.ts.map +1 -1
- package/build/universal/BottomSheet/index.android.d.ts +1 -1
- package/build/universal/BottomSheet/index.android.d.ts.map +1 -1
- package/build/universal/BottomSheet/index.d.ts +1 -1
- package/build/universal/BottomSheet/index.d.ts.map +1 -1
- package/build/universal/BottomSheet/index.ios.d.ts +1 -1
- package/build/universal/BottomSheet/index.ios.d.ts.map +1 -1
- package/build/universal/BottomSheet/types.d.ts +28 -0
- package/build/universal/BottomSheet/types.d.ts.map +1 -1
- package/build/universal/BottomSheet/utils.d.ts +13 -0
- package/build/universal/BottomSheet/utils.d.ts.map +1 -0
- package/expo-module.config.json +1 -1
- package/ios/Convertibles/PaddingValue.swift +57 -0
- package/ios/Modifiers/MenuIndicatorModifier.swift +18 -0
- package/ios/Modifiers/MenuStyleModifier.swift +28 -0
- package/ios/Modifiers/ViewModifierRegistry.swift +105 -18
- package/ios/SliderView.swift +11 -5
- package/ios/State/WorkletCallbackClassDef.swift +7 -0
- package/ios/Toggle/ToggleView.swift +16 -18
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.10/expo.modules.ui-57.0.10-sources.jar → 57.0.12/expo.modules.ui-57.0.12-sources.jar} +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12-sources.jar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.aar.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.10/expo.modules.ui-57.0.10.module → 57.0.12/expo.modules.ui-57.0.12.module} +22 -22
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.module.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/{57.0.10/expo.modules.ui-57.0.10.pom → 57.0.12/expo.modules.ui-57.0.12.pom} +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.md5 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.sha1 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.sha256 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.12/expo.modules.ui-57.0.12.pom.sha512 +1 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml +4 -4
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.md5 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha1 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha256 +1 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/maven-metadata.xml.sha512 +1 -1
- package/package.json +4 -4
- package/src/State/useWorkletProp.ts +31 -14
- package/src/jetpack-compose/Image/index.tsx +149 -0
- package/src/jetpack-compose/index.ts +1 -0
- package/src/swift-ui/Alert/index.tsx +1 -0
- package/src/swift-ui/ConfirmationDialog/index.tsx +1 -0
- package/src/swift-ui/Overlay/index.tsx +1 -0
- package/src/swift-ui/Popover/index.tsx +1 -0
- package/src/swift-ui/Slider/index.tsx +14 -8
- package/src/swift-ui/modifiers/index.ts +70 -8
- package/src/universal/BottomSheet/index.android.tsx +13 -2
- package/src/universal/BottomSheet/index.ios.tsx +9 -1
- package/src/universal/BottomSheet/index.tsx +17 -2
- package/src/universal/BottomSheet/types.ts +33 -0
- package/src/universal/BottomSheet/utils.ts +35 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10-sources.jar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10-sources.jar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10-sources.jar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10-sources.jar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.aar +0 -0
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.aar.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.aar.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.aar.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.aar.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.module.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.module.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.module.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.module.sha512 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.pom.md5 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.pom.sha1 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.pom.sha256 +0 -1
- package/local-maven-repo/expo/modules/ui/expo.modules.ui/57.0.10/expo.modules.ui-57.0.10.pom.sha512 +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,34 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 57.0.12 — 2026-08-20
|
|
14
|
+
|
|
15
|
+
### 🎉 New features
|
|
16
|
+
|
|
17
|
+
- [iOS] Added `'default'` as a value for every edge of the `padding` modifier, so a single edge can use the system default padding while the other edges use explicit lengths. ([#48903](https://github.com/expo/expo/pull/48903) by [@Den1Marshall](https://github.com/Den1Marshall))
|
|
18
|
+
- [android] Add `shouldDismissOnBackPress` prop to the universal `BottomSheet` component, forwarding it to `ModalBottomSheet` via `properties.shouldDismissOnBackPress`. When false, the back press does not dismiss the sheet (and the press still does not reach BackHandler). ([#48987](https://github.com/expo/expo/pull/48987) by [@webdevsamran](https://github.com/webdevsamran))
|
|
19
|
+
- [universal] Added a `contentPadding` prop to `BottomSheet`, so sheet content can reach the sheet's edges instead of always sitting inside a hardcoded 16-unit inset. ([#48902](https://github.com/expo/expo/issues/48902) by [@kilarsky](https://github.com/kilarsky))
|
|
20
|
+
|
|
21
|
+
### 🐛 Bug fixes
|
|
22
|
+
|
|
23
|
+
- [Android] Preserve vector drawable `fillType` values when loading images so even-odd paths render correctly. ([#49049](https://github.com/expo/expo/pull/49049) by [@aonan-brex](https://github.com/aonan-brex))
|
|
24
|
+
- [iOS] Fixed `Overlay`, `Alert`, `ConfirmationDialog` and `Popover` silently ignoring the `modifiers` prop, which never reached the native view. ([#48949](https://github.com/expo/expo/pull/48949) by [@Den1Marshall](https://github.com/Den1Marshall))
|
|
25
|
+
- [iOS] Fix `Slider` ignoring `value` prop updates after the first drag. ([#49075](https://github.com/expo/expo/issues/49075) by [@matinzd](https://github.com/matinzd)) ([#49139](https://github.com/expo/expo/pull/49139) by [@nishan](https://github.com/intergalacticspacehighway))
|
|
26
|
+
- [iOS] Fix `Toggle` rendering local state instead of `isOn`, which left `community/menu` checkmarks out of sync and dropped every second `onPressAction`. `Toggle` is now fully controlled: when `isOn` is set, it only moves once JS updates the prop. ([#48982](https://github.com/expo/expo/issues/48982) by [@JustJoostNL](https://github.com/JustJoostNL)) ([#49021](https://github.com/expo/expo/pull/49021) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
27
|
+
|
|
28
|
+
## 57.0.11 — 2026-08-14
|
|
29
|
+
|
|
30
|
+
### 🎉 New features
|
|
31
|
+
|
|
32
|
+
- [iOS] Added `menuStyle` and `menuIndicator` SwiftUI modifiers, which together with `buttonStyle('plain')` keep a `Menu`'s custom label as its entire trigger on Mac Catalyst. ([#48447](https://github.com/expo/expo/issues/48447) by [@cvburgess](https://github.com/cvburgess)) ([#48448](https://github.com/expo/expo/pull/48448) by [@cvburgess](https://github.com/cvburgess))
|
|
33
|
+
- [iOS] Added the `listRowSeparatorTint` modifier, so a list row separator can be recolored and not only shown or hidden. ([#48804](https://github.com/expo/expo/issues/48804) by [@marco242424](https://github.com/marco242424)) ([#48810](https://github.com/expo/expo/pull/48810) by [@expo-bot](https://github.com/expo-bot))
|
|
34
|
+
- [iOS] Added the `geometryGroup` modifier, so a view's subviews can resolve their geometry against it instead of an animating ancestor. Without it, a `Button` inside a container with an `animation` modifier can draw its label away from its own background while the animation runs. ([#48838](https://github.com/expo/expo/pull/48838) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
35
|
+
- [Android] Add a Jetpack Compose `Image` component. ([#48521](https://github.com/expo/expo/pull/48521) by [@jakex7](https://github.com/jakex7))
|
|
36
|
+
|
|
37
|
+
### 🐛 Bug fixes
|
|
38
|
+
|
|
39
|
+
- [universal] Fix `Cannot use shared object that was already released` when a worklet callback prop closes over an unstable value. ([#48819](https://github.com/expo/expo/pull/48819) by [@intergalacticspacehighway](https://github.com/intergalacticspacehighway))
|
|
40
|
+
|
|
13
41
|
## 57.0.10 — 2026-08-10
|
|
14
42
|
|
|
15
43
|
### 🎉 New features
|
package/android/build.gradle
CHANGED
|
@@ -12,13 +12,13 @@ apply plugin: 'expo-module-gradle-plugin'
|
|
|
12
12
|
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
|
|
13
13
|
|
|
14
14
|
group = 'expo.modules.ui'
|
|
15
|
-
version = '57.0.
|
|
15
|
+
version = '57.0.12'
|
|
16
16
|
|
|
17
17
|
android {
|
|
18
18
|
namespace "expo.modules.ui"
|
|
19
19
|
defaultConfig {
|
|
20
20
|
versionCode 1
|
|
21
|
-
versionName "57.0.
|
|
21
|
+
versionName "57.0.12"
|
|
22
22
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
23
23
|
}
|
|
24
24
|
buildFeatures {
|
|
@@ -28,7 +28,9 @@ import expo.modules.ui.button.IconButtonContent
|
|
|
28
28
|
import expo.modules.ui.button.FilledIconButtonContent
|
|
29
29
|
import expo.modules.ui.button.FilledTonalIconButtonContent
|
|
30
30
|
import expo.modules.ui.button.OutlinedIconButtonContent
|
|
31
|
+
import expo.modules.ui.graphics.ImageLoader
|
|
31
32
|
import expo.modules.ui.icon.IconView
|
|
33
|
+
import expo.modules.ui.image.ImageView
|
|
32
34
|
import expo.modules.ui.menu.DropdownMenuContent
|
|
33
35
|
import expo.modules.ui.menu.DropdownMenuProps
|
|
34
36
|
import expo.modules.ui.menu.DropdownMenuItemContent
|
|
@@ -55,15 +57,23 @@ import okhttp3.OkHttpClient
|
|
|
55
57
|
class ExpoUIModule : Module() {
|
|
56
58
|
var okHttpClient: OkHttpClient? = null
|
|
57
59
|
private set
|
|
60
|
+
var imageLoader: ImageLoader? = null
|
|
61
|
+
private set
|
|
58
62
|
|
|
59
63
|
override fun definition() = ModuleDefinition {
|
|
60
64
|
Name("ExpoUI")
|
|
61
65
|
|
|
62
66
|
OnCreate {
|
|
63
|
-
|
|
67
|
+
val context = requireNotNull(appContext.reactContext) {
|
|
68
|
+
"ExpoUIModule requires an active React context"
|
|
69
|
+
}
|
|
70
|
+
okHttpClient = appContext.createOkHttpClientBuilder().build()
|
|
71
|
+
imageLoader = ImageLoader(context, requireNotNull(okHttpClient))
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
OnDestroy {
|
|
75
|
+
imageLoader?.close()
|
|
76
|
+
imageLoader = null
|
|
67
77
|
okHttpClient?.dispatcher?.executorService?.shutdown()
|
|
68
78
|
okHttpClient?.connectionPool?.evictAll()
|
|
69
79
|
okHttpClient?.cache?.close()
|
|
@@ -79,6 +89,13 @@ class ExpoUIModule : Module() {
|
|
|
79
89
|
callback
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
Function("setWorklet") { callback: WorkletCallback, worklet: Worklet ->
|
|
93
|
+
// Dispatch on main thread as callback is read on the main thread
|
|
94
|
+
appContext.mainQueue.launch {
|
|
95
|
+
callback.worklet = worklet
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
Property("__expo_ui_shared_object__") { _: WorkletCallback ->
|
|
83
100
|
true
|
|
84
101
|
}
|
|
@@ -167,6 +184,9 @@ class ExpoUIModule : Module() {
|
|
|
167
184
|
View(InnerTextFieldView::class)
|
|
168
185
|
View(PlaceholderView::class)
|
|
169
186
|
View(IconView::class)
|
|
187
|
+
View(ImageView::class) {
|
|
188
|
+
Events("onLoad", "onError")
|
|
189
|
+
}
|
|
170
190
|
View(LazyColumnView::class)
|
|
171
191
|
View(LazyRowView::class)
|
|
172
192
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package expo.modules.ui.graphics
|
|
2
|
+
|
|
3
|
+
import android.graphics.drawable.BitmapDrawable
|
|
4
|
+
import android.graphics.drawable.Drawable
|
|
5
|
+
import androidx.compose.runtime.Composable
|
|
6
|
+
import androidx.compose.runtime.remember
|
|
7
|
+
import androidx.compose.ui.geometry.Size
|
|
8
|
+
import androidx.compose.ui.graphics.asImageBitmap
|
|
9
|
+
import androidx.compose.ui.graphics.drawscope.DrawScope
|
|
10
|
+
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
|
11
|
+
import androidx.compose.ui.graphics.nativeCanvas
|
|
12
|
+
import androidx.compose.ui.graphics.painter.BitmapPainter
|
|
13
|
+
import androidx.compose.ui.graphics.painter.Painter
|
|
14
|
+
|
|
15
|
+
@Composable
|
|
16
|
+
internal fun rememberDrawablePainter(drawable: Drawable?): Painter? {
|
|
17
|
+
return remember(drawable) {
|
|
18
|
+
when (drawable) {
|
|
19
|
+
null -> null
|
|
20
|
+
is BitmapDrawable -> BitmapPainter(drawable.bitmap.asImageBitmap())
|
|
21
|
+
else -> DrawablePainter(drawable.mutate())
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private class DrawablePainter(
|
|
27
|
+
private val drawable: Drawable
|
|
28
|
+
) : Painter() {
|
|
29
|
+
override val intrinsicSize: Size
|
|
30
|
+
get() = Size(
|
|
31
|
+
drawable.intrinsicWidth.toFloat().takeIf { it > 0 } ?: Size.Unspecified.width,
|
|
32
|
+
drawable.intrinsicHeight.toFloat().takeIf { it > 0 } ?: Size.Unspecified.height
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
override fun DrawScope.onDraw() {
|
|
36
|
+
drawIntoCanvas { canvas ->
|
|
37
|
+
with(drawable) {
|
|
38
|
+
setBounds(0, 0, size.width.toInt(), size.height.toInt())
|
|
39
|
+
draw(canvas.nativeCanvas)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/android/src/main/java/expo/modules/ui/{icon/VectorIconLoader.kt → graphics/ImageLoader.kt}
RENAMED
|
@@ -1,135 +1,155 @@
|
|
|
1
|
-
package expo.modules.ui.
|
|
1
|
+
package expo.modules.ui.graphics
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.content.res.XmlResourceParser
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.graphics.drawable.BitmapDrawable
|
|
5
7
|
import android.graphics.drawable.Drawable
|
|
6
8
|
import android.net.Uri
|
|
7
9
|
import android.util.Log
|
|
10
|
+
import android.util.LruCache
|
|
8
11
|
import android.util.Xml
|
|
12
|
+
import androidx.compose.ui.graphics.PathFillType
|
|
9
13
|
import androidx.compose.ui.graphics.SolidColor
|
|
10
14
|
import androidx.compose.ui.graphics.vector.ImageVector
|
|
11
15
|
import androidx.compose.ui.graphics.vector.PathParser
|
|
12
16
|
import androidx.compose.ui.unit.Dp
|
|
13
17
|
import androidx.compose.ui.unit.dp
|
|
18
|
+
import androidx.core.graphics.drawable.toDrawable
|
|
14
19
|
import androidx.core.graphics.toColorInt
|
|
20
|
+
import androidx.core.net.toUri
|
|
21
|
+
import expo.modules.kotlin.okhttp.await
|
|
22
|
+
import kotlinx.coroutines.CancellationException
|
|
15
23
|
import kotlinx.coroutines.Dispatchers
|
|
16
24
|
import kotlinx.coroutines.withContext
|
|
17
25
|
import okhttp3.OkHttpClient
|
|
18
26
|
import okhttp3.Request
|
|
19
27
|
import org.xmlpull.v1.XmlPullParser
|
|
20
28
|
import java.io.File
|
|
21
|
-
import java.io.
|
|
29
|
+
import java.io.IOException
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
|
-
* Loads and parses
|
|
32
|
+
* Loads and parses images from various sources (HTTP, file, content provider).
|
|
25
33
|
* Supports XML vector drawables (Android format) and bitmap images.
|
|
26
34
|
*
|
|
27
35
|
* All loading operations are suspend functions and run on appropriate dispatchers.
|
|
28
36
|
*/
|
|
29
|
-
class
|
|
37
|
+
class ImageLoader(
|
|
30
38
|
private val context: Context,
|
|
31
39
|
private val okHttpClient: OkHttpClient
|
|
32
40
|
) {
|
|
41
|
+
private val imageCache = object : LruCache<String, ImageResult>(memoryCacheSizeKilobytes()) {
|
|
42
|
+
override fun sizeOf(key: String, value: ImageResult): Int {
|
|
43
|
+
val bitmap = (value.drawable as? BitmapDrawable)?.bitmap ?: return 1
|
|
44
|
+
|
|
45
|
+
return (bitmap.allocationByteCount / 1024)
|
|
46
|
+
.coerceAtLeast(1)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
33
49
|
|
|
34
50
|
/**
|
|
35
|
-
* Result of
|
|
51
|
+
* Result of an image loading operation.
|
|
36
52
|
*/
|
|
37
|
-
data class
|
|
53
|
+
data class ImageResult(
|
|
38
54
|
val imageVector: ImageVector? = null,
|
|
39
|
-
val drawable: Drawable? = null
|
|
40
|
-
|
|
55
|
+
val drawable: Drawable? = null,
|
|
56
|
+
val error: String? = null
|
|
57
|
+
) {
|
|
58
|
+
val isSuccess: Boolean
|
|
59
|
+
get() = imageVector != null || drawable != null
|
|
60
|
+
}
|
|
41
61
|
|
|
42
62
|
/**
|
|
43
|
-
* Load an
|
|
44
|
-
* Returns an
|
|
63
|
+
* Load an image from a URI string.
|
|
64
|
+
* Returns an ImageResult containing either an ImageVector (for XML) or Drawable (for bitmaps).
|
|
45
65
|
*
|
|
46
66
|
* This is a suspend function that performs I/O operations on the IO dispatcher.
|
|
47
67
|
*/
|
|
48
|
-
suspend fun loadFromUri(uriString: String?):
|
|
49
|
-
if (uriString.isNullOrEmpty())
|
|
68
|
+
suspend fun loadFromUri(uriString: String?): ImageResult = withContext(Dispatchers.IO) {
|
|
69
|
+
if (uriString.isNullOrEmpty()) {
|
|
70
|
+
return@withContext ImageResult(error = "Image URI is empty")
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
val uri = try {
|
|
52
|
-
|
|
74
|
+
uriString.toUri()
|
|
53
75
|
} catch (e: Exception) {
|
|
54
76
|
Log.e(TAG, "Failed to parse URI: $uriString", e)
|
|
55
|
-
return@withContext
|
|
77
|
+
return@withContext ImageResult(error = "Failed to parse image URI: $uriString")
|
|
56
78
|
}
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
80
|
+
imageCache.get(uriString)?.let { return@withContext it }
|
|
81
|
+
loadUncached(uri).also { result ->
|
|
82
|
+
if (result.isSuccess) {
|
|
83
|
+
imageCache.put(uriString, result)
|
|
62
84
|
}
|
|
63
|
-
|
|
64
|
-
val inputStream = getInputStreamForUri(uri) ?: return@withContext IconResult()
|
|
65
|
-
inputStream.use { stream ->
|
|
66
|
-
parseIconFromStream(stream)
|
|
67
|
-
}
|
|
68
|
-
} catch (e: Exception) {
|
|
69
|
-
Log.e(TAG, "Failed to load icon from URI: $uri", e)
|
|
70
|
-
IconResult()
|
|
71
85
|
}
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
/**
|
|
75
|
-
*
|
|
89
|
+
* Clears pending work and releases decoded images held by this loader.
|
|
76
90
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
fun close() {
|
|
92
|
+
imageCache.evictAll()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private suspend fun loadUncached(uri: Uri): ImageResult {
|
|
96
|
+
return try {
|
|
97
|
+
// For resource URIs, use Android's XmlResourceParser which handles compiled binary XML.
|
|
98
|
+
if (uri.scheme == "res") {
|
|
99
|
+
loadFromResourceId(uri)
|
|
100
|
+
} else {
|
|
101
|
+
val bytes = when (uri.scheme) {
|
|
102
|
+
"file" -> {
|
|
103
|
+
val path = uri.path ?: throw IOException("Image file URI has no path: $uri")
|
|
104
|
+
File(path).readBytes()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
"content", "android.resource" -> {
|
|
108
|
+
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
|
|
109
|
+
?: throw IOException("Unable to open image URI: $uri")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
"http", "https" -> downloadFromHttp(uri.toString())
|
|
113
|
+
else -> throw IOException("Unsupported image URI scheme: ${uri.scheme ?: "none"}")
|
|
114
|
+
}
|
|
115
|
+
parseImageFromBytes(bytes)
|
|
94
116
|
}
|
|
117
|
+
} catch (e: CancellationException) {
|
|
118
|
+
throw e
|
|
119
|
+
} catch (e: Exception) {
|
|
120
|
+
val message = e.message ?: "Failed to load image from URI: $uri"
|
|
121
|
+
Log.e(TAG, message, e)
|
|
122
|
+
ImageResult(error = message)
|
|
95
123
|
}
|
|
96
124
|
}
|
|
97
125
|
|
|
98
126
|
/**
|
|
99
|
-
* Download
|
|
100
|
-
*
|
|
127
|
+
* Download an image from HTTP/HTTPS URL using OkHttp.
|
|
128
|
+
* The OkHttp call is cancelled when the enclosing coroutine is cancelled.
|
|
101
129
|
*/
|
|
102
|
-
private fun downloadFromHttp(url: String):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// OkHttp's execute() is blocking, which is fine on IO dispatcher
|
|
109
|
-
val response = okHttpClient.newCall(request).execute()
|
|
130
|
+
private suspend fun downloadFromHttp(url: String): ByteArray {
|
|
131
|
+
val request = Request.Builder()
|
|
132
|
+
.url(url)
|
|
133
|
+
.build()
|
|
134
|
+
return okHttpClient.newCall(request).await().use { response ->
|
|
110
135
|
if (!response.isSuccessful) {
|
|
111
|
-
|
|
112
|
-
response.close()
|
|
113
|
-
return null
|
|
136
|
+
throw IOException("Failed to download image from $url: HTTP ${response.code}")
|
|
114
137
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} catch (e: Exception) {
|
|
118
|
-
Log.e(TAG, "HTTP download failed: $url", e)
|
|
119
|
-
null
|
|
138
|
+
response.body?.bytes()
|
|
139
|
+
?: throw IOException("Image response from $url has no body")
|
|
120
140
|
}
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
/**
|
|
124
|
-
* Load
|
|
144
|
+
* Load an image from an Android resource ID extracted from a `res:/` URI.
|
|
125
145
|
* Uses [XmlResourceParser] which handles compiled (binary) XML in release builds.
|
|
126
146
|
*/
|
|
127
|
-
private fun loadFromResourceId(uri: Uri):
|
|
147
|
+
private fun loadFromResourceId(uri: Uri): ImageResult {
|
|
128
148
|
val resourceId = uri.lastPathSegment?.toIntOrNull()
|
|
129
149
|
?: uri.path?.trimStart('/')?.toIntOrNull()
|
|
130
150
|
if (resourceId == null || resourceId <= 0) {
|
|
131
151
|
Log.w(TAG, "Invalid resource ID in URI: $uri")
|
|
132
|
-
return
|
|
152
|
+
return ImageResult(error = "Invalid image resource ID: $uri")
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
// Try parsing as XML (handles both text and compiled binary XML)
|
|
@@ -138,38 +158,40 @@ class VectorIconLoader(
|
|
|
138
158
|
xmlParser.use { parser ->
|
|
139
159
|
val imageVector = parseXmlToImageVector(parser)
|
|
140
160
|
if (imageVector != null) {
|
|
141
|
-
return
|
|
161
|
+
return ImageResult(imageVector = imageVector)
|
|
142
162
|
}
|
|
143
163
|
}
|
|
144
164
|
} catch (e: Exception) {
|
|
145
165
|
Log.d(TAG, "Resource $resourceId is not XML, trying as raw resource", e)
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
// Fall back to
|
|
168
|
+
// Fall back to bitmap resource decoding.
|
|
149
169
|
try {
|
|
150
|
-
context.resources
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
170
|
+
val bitmap = BitmapFactory.decodeResource(context.resources, resourceId)
|
|
171
|
+
?: return ImageResult(error = "Failed to decode image resource $resourceId")
|
|
172
|
+
return ImageResult(drawable = bitmap.toDrawable(context.resources))
|
|
154
173
|
} catch (e: Exception) {
|
|
155
174
|
Log.e(TAG, "Failed to load resource $resourceId", e)
|
|
156
|
-
return
|
|
175
|
+
return ImageResult(error = e.message ?: "Failed to load image resource $resourceId")
|
|
157
176
|
}
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
/**
|
|
161
|
-
* Parse
|
|
180
|
+
* Parse an image from encoded bytes.
|
|
162
181
|
* Detects format (XML or bitmap) and returns appropriate result.
|
|
163
182
|
*/
|
|
164
|
-
private fun
|
|
165
|
-
val bytes = inputStream.readBytes()
|
|
166
|
-
|
|
183
|
+
private fun parseImageFromBytes(bytes: ByteArray): ImageResult {
|
|
167
184
|
return if (isXmlContent(bytes)) {
|
|
168
185
|
val imageVector = parseXmlToImageVector(bytes)
|
|
169
|
-
|
|
186
|
+
if (imageVector != null) {
|
|
187
|
+
ImageResult(imageVector = imageVector)
|
|
188
|
+
} else {
|
|
189
|
+
ImageResult(error = "Failed to decode XML image")
|
|
190
|
+
}
|
|
170
191
|
} else {
|
|
171
|
-
val
|
|
172
|
-
|
|
192
|
+
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
193
|
+
?: return ImageResult(error = "Failed to decode bitmap image")
|
|
194
|
+
ImageResult(drawable = bitmap.toDrawable(context.resources))
|
|
173
195
|
}
|
|
174
196
|
}
|
|
175
197
|
|
|
@@ -181,10 +203,7 @@ class VectorIconLoader(
|
|
|
181
203
|
|
|
182
204
|
// Check for UTF-8 BOM
|
|
183
205
|
var offset = 0
|
|
184
|
-
if (bytes.
|
|
185
|
-
bytes[0] == 0xEF.toByte() &&
|
|
186
|
-
bytes[1] == 0xBB.toByte() &&
|
|
187
|
-
bytes[2] == 0xBF.toByte()
|
|
206
|
+
if (bytes[0] == 0xEF.toByte() && bytes[1] == 0xBB.toByte() && bytes[2] == 0xBF.toByte()
|
|
188
207
|
) {
|
|
189
208
|
offset = 3
|
|
190
209
|
}
|
|
@@ -275,6 +294,7 @@ class VectorIconLoader(
|
|
|
275
294
|
// Note: groups, clips, gradients not yet supported
|
|
276
295
|
}
|
|
277
296
|
}
|
|
297
|
+
|
|
278
298
|
XmlPullParser.END_TAG -> {
|
|
279
299
|
if (parser.name == "vector") {
|
|
280
300
|
return builder.build()
|
|
@@ -298,6 +318,7 @@ class VectorIconLoader(
|
|
|
298
318
|
try {
|
|
299
319
|
var pathData = ""
|
|
300
320
|
var fillColor: androidx.compose.ui.graphics.Color? = null
|
|
321
|
+
var pathFillType = PathFillType.NonZero
|
|
301
322
|
|
|
302
323
|
for (i in 0 until parser.attributeCount) {
|
|
303
324
|
when (parser.getAttributeName(i)) {
|
|
@@ -305,7 +326,13 @@ class VectorIconLoader(
|
|
|
305
326
|
"fillColor" -> {
|
|
306
327
|
fillColor = parseColor(parser.getAttributeValue(i))
|
|
307
328
|
}
|
|
308
|
-
|
|
329
|
+
"fillType" -> {
|
|
330
|
+
pathFillType = when (parser.getAttributeValue(i)) {
|
|
331
|
+
"evenOdd", "1" -> PathFillType.EvenOdd
|
|
332
|
+
else -> PathFillType.NonZero
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// Note: stroke properties and opacity are not yet supported
|
|
309
336
|
}
|
|
310
337
|
}
|
|
311
338
|
|
|
@@ -313,7 +340,8 @@ class VectorIconLoader(
|
|
|
313
340
|
val nodes = PathParser().parsePathString(pathData).toNodes()
|
|
314
341
|
builder.addPath(
|
|
315
342
|
pathData = nodes,
|
|
316
|
-
fill = fillColor?.let { SolidColor(it) }
|
|
343
|
+
fill = fillColor?.let { SolidColor(it) },
|
|
344
|
+
pathFillType = pathFillType
|
|
317
345
|
)
|
|
318
346
|
}
|
|
319
347
|
} catch (e: Exception) {
|
|
@@ -332,7 +360,7 @@ class VectorIconLoader(
|
|
|
332
360
|
.replace("px", "")
|
|
333
361
|
.toFloat()
|
|
334
362
|
numericValue.dp
|
|
335
|
-
} catch (
|
|
363
|
+
} catch (_: Exception) {
|
|
336
364
|
Log.w(TAG, "Failed to parse dimension: $value, using default 24dp")
|
|
337
365
|
24.dp
|
|
338
366
|
}
|
|
@@ -347,11 +375,13 @@ class VectorIconLoader(
|
|
|
347
375
|
colorValue.startsWith("#") -> {
|
|
348
376
|
androidx.compose.ui.graphics.Color(colorValue.toColorInt())
|
|
349
377
|
}
|
|
378
|
+
|
|
350
379
|
colorValue.startsWith("@android:color/") || colorValue.startsWith("?attr/") -> {
|
|
351
380
|
// Theme attributes default to black (could be enhanced to resolve theme colors)
|
|
352
381
|
Log.d(TAG, "Theme color attribute not resolved: $colorValue, using black")
|
|
353
382
|
androidx.compose.ui.graphics.Color.Black
|
|
354
383
|
}
|
|
384
|
+
|
|
355
385
|
else -> {
|
|
356
386
|
Log.w(TAG, "Unknown color format: $colorValue, using black")
|
|
357
387
|
androidx.compose.ui.graphics.Color.Black
|
|
@@ -364,6 +394,11 @@ class VectorIconLoader(
|
|
|
364
394
|
}
|
|
365
395
|
|
|
366
396
|
companion object {
|
|
367
|
-
private const val TAG = "
|
|
397
|
+
private const val TAG = "ImageLoader"
|
|
398
|
+
|
|
399
|
+
private fun memoryCacheSizeKilobytes(): Int {
|
|
400
|
+
val maxMemoryKilobytes = Runtime.getRuntime().maxMemory() / 1024
|
|
401
|
+
return (maxMemoryKilobytes / 8).coerceIn(1, Int.MAX_VALUE.toLong()).toInt()
|
|
402
|
+
}
|
|
368
403
|
}
|
|
369
404
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package expo.modules.ui.graphics
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import expo.modules.kotlin.records.Field
|
|
6
|
+
import expo.modules.kotlin.records.Record
|
|
7
|
+
import expo.modules.kotlin.types.OptimizedRecord
|
|
8
|
+
|
|
9
|
+
@OptimizedRecord
|
|
10
|
+
data class ImageSource(
|
|
11
|
+
@Field val uri: String,
|
|
12
|
+
@Field val width: Int = 0,
|
|
13
|
+
@Field val height: Int = 0,
|
|
14
|
+
@Field val scale: Double = 1.0
|
|
15
|
+
) : Record
|
|
16
|
+
|
|
17
|
+
internal fun ImageSource.resolveUri(context: Context): String? {
|
|
18
|
+
return try {
|
|
19
|
+
val parsedUri = Uri.parse(uri)
|
|
20
|
+
if (parsedUri.scheme == null) {
|
|
21
|
+
ResourceIdHelper.getResourceUri(context, uri)?.toString()
|
|
22
|
+
} else {
|
|
23
|
+
uri
|
|
24
|
+
}
|
|
25
|
+
} catch (_: Exception) {
|
|
26
|
+
ResourceIdHelper.getResourceUri(context, uri)?.toString()
|
|
27
|
+
}
|
|
28
|
+
}
|