@onekeyfe/react-native-image 6.7.0-alpha.409 → 6.7.0-alpha.412
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 +12 -0
- package/android/build.gradle +3 -1
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyIcon.kt +93 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImageGlideRegistry.kt +1 -1
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyImagePackage.kt +2 -1
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeyNamedSVG.kt +217 -0
- package/android/src/main/java/com/margelo/nitro/onekeyimage/OneKeySVG.kt +31 -0
- package/ios/OneKeyIcon.swift +90 -0
- package/ios/OneKeyImageCoderBridge.h +4 -0
- package/ios/OneKeyImageCoderBridge.m +9 -0
- package/ios/OneKeyNamedSVG.swift +282 -0
- package/ios/tests/OneKeyIconTests.swift +484 -0
- package/lib/module/OneKeyIcon.nitro.js +4 -0
- package/lib/module/icon/assetUrls.js +7 -0
- package/lib/module/icon/dom.js +21 -0
- package/lib/module/icon/index.js +29 -0
- package/lib/module/icon/index.web.js +32 -0
- package/lib/module/icon/props.js +22 -0
- package/lib/module/package.json +6 -1
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.cpp +67 -0
- package/lib/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.hpp +66 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.cpp +79 -0
- package/lib/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.hpp +54 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyIconSpec.kt +65 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconManager.kt +96 -0
- package/lib/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconStateUpdater.kt +23 -0
- package/lib/nitrogen/generated/android/onekeyimage+autolinking.cmake +4 -0
- package/lib/nitrogen/generated/android/onekeyimageOnLoad.cpp +18 -0
- package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +17 -0
- package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +32 -15
- package/lib/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +5 -0
- package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.mm +8 -0
- package/lib/nitrogen/generated/ios/OneKeyImageAutolinking.swift +12 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.cpp +11 -0
- package/lib/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.hpp +89 -0
- package/lib/nitrogen/generated/ios/c++/views/HybridOneKeyIconComponent.mm +150 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyIconSpec.swift +56 -0
- package/lib/nitrogen/generated/ios/swift/HybridOneKeyIconSpec_cxx.swift +193 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.cpp +24 -0
- package/lib/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.hpp +66 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.cpp +36 -0
- package/lib/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.hpp +88 -0
- package/lib/nitrogen/generated/shared/json/OneKeyIconConfig.json +11 -0
- package/lib/typescript/src/OneKeyIcon.nitro.d.ts +7 -0
- package/lib/typescript/src/icon/assetUrls.d.ts +2 -0
- package/lib/typescript/src/icon/dom.d.ts +3 -0
- package/lib/typescript/src/icon/index.d.ts +140 -0
- package/lib/typescript/src/icon/index.web.d.ts +134 -0
- package/lib/typescript/src/icon/props.d.ts +8 -0
- package/nitro.json +10 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.cpp +67 -0
- package/nitrogen/generated/android/c++/JHybridOneKeyIconSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.cpp +79 -0
- package/nitrogen/generated/android/c++/views/JHybridOneKeyIconStateUpdater.hpp +54 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/HybridOneKeyIconSpec.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconManager.kt +96 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/onekeyimage/views/HybridOneKeyIconStateUpdater.kt +23 -0
- package/nitrogen/generated/android/onekeyimage+autolinking.cmake +4 -0
- package/nitrogen/generated/android/onekeyimageOnLoad.cpp +18 -0
- package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.cpp +17 -0
- package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Bridge.hpp +32 -15
- package/nitrogen/generated/ios/OneKeyImage-Swift-Cxx-Umbrella.hpp +5 -0
- package/nitrogen/generated/ios/OneKeyImageAutolinking.mm +8 -0
- package/nitrogen/generated/ios/OneKeyImageAutolinking.swift +12 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridOneKeyIconSpecSwift.hpp +89 -0
- package/nitrogen/generated/ios/c++/views/HybridOneKeyIconComponent.mm +150 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyIconSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridOneKeyIconSpec_cxx.swift +193 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.cpp +24 -0
- package/nitrogen/generated/shared/c++/HybridOneKeyIconSpec.hpp +66 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.cpp +36 -0
- package/nitrogen/generated/shared/c++/views/HybridOneKeyIconComponent.hpp +88 -0
- package/nitrogen/generated/shared/json/OneKeyIconConfig.json +11 -0
- package/package.json +40 -7
- package/scripts/configure-icon-imports.mjs +7 -0
- package/scripts/icon-fixtures.test.mjs +398 -0
- package/src/OneKeyIcon.nitro.ts +8 -0
- package/src/icon/assetUrls.ts +4 -0
- package/src/icon/dom.ts +21 -0
- package/src/icon/index.tsx +38 -0
- package/src/icon/index.web.tsx +36 -0
- package/src/icon/props.ts +38 -0
package/README.md
CHANGED
|
@@ -95,3 +95,15 @@ pod 'SDWebImage', '5.21.7', :modular_headers => true
|
|
|
95
95
|
pod 'SDWebImageWebPCoder', '0.14.6', :modular_headers => true
|
|
96
96
|
pod 'SDWebImageSVGCoder', '1.7.0', :modular_headers => true
|
|
97
97
|
```
|
|
98
|
+
|
|
99
|
+
## Named icon resource ownership
|
|
100
|
+
|
|
101
|
+
The host owns canonical SVG artwork and build output. Native builds supply a
|
|
102
|
+
`OneKeyIcons.bundle` on iOS and `assets/icons` on Android, including the manifest.
|
|
103
|
+
Web builds alias `#onekey-icon-assets` to a generated module exporting
|
|
104
|
+
`iconResourceHref(name: string): string | undefined`; the default SDK provider
|
|
105
|
+
is empty. `OneKeyIcon` props are unchanged. See [the resource contract](docs/SPEC.md#16-host-owned-icon-resources--implemented-sdk-fixture-runtime-verified).
|
|
106
|
+
|
|
107
|
+
The SDK example carries 63 independent test samples. `icons:check` and
|
|
108
|
+
`icons:test` are repository maintenance commands that require the example
|
|
109
|
+
fixtures; npm consumers do not receive those samples or the complete catalog.
|
package/android/build.gradle
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
package com.margelo.nitro.onekeyimage
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.Configuration
|
|
5
|
+
import android.graphics.PorterDuff
|
|
6
|
+
import android.graphics.PorterDuffColorFilter
|
|
7
|
+
import android.widget.ImageView
|
|
8
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
|
+
import com.margelo.nitro.views.RecyclableView
|
|
10
|
+
import java.lang.ref.WeakReference
|
|
11
|
+
|
|
12
|
+
private class OneKeyIconHost(context: Context) : ImageView(context) {
|
|
13
|
+
var changed: (() -> Unit)? = null
|
|
14
|
+
var attached = false
|
|
15
|
+
private set
|
|
16
|
+
init { scaleType = ScaleType.FIT_CENTER }
|
|
17
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
18
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
19
|
+
changed?.invoke()
|
|
20
|
+
}
|
|
21
|
+
override fun onAttachedToWindow() { super.onAttachedToWindow(); attached = true; changed?.invoke() }
|
|
22
|
+
override fun onDetachedFromWindow() { attached = false; super.onDetachedFromWindow(); changed?.invoke() }
|
|
23
|
+
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig); changed?.invoke() }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class HybridOneKeyIcon(context: ThemedReactContext) : HybridOneKeyIconSpec(), RecyclableView {
|
|
27
|
+
private val host = OneKeyIconHost(context)
|
|
28
|
+
private val owner = Any()
|
|
29
|
+
private var generation = 0L
|
|
30
|
+
private var identity: String? = null
|
|
31
|
+
private var displayedGeometry: String? = null
|
|
32
|
+
private var displayed: OneKeyNamedSVG.RenderResult? = null
|
|
33
|
+
override val view: ImageView get() = host
|
|
34
|
+
override var iconName: String? = null
|
|
35
|
+
override var resolvedColor: String? = null
|
|
36
|
+
init { host.changed = { refresh() } }
|
|
37
|
+
|
|
38
|
+
override fun afterUpdate() { super.afterUpdate(); refresh() }
|
|
39
|
+
|
|
40
|
+
private fun clearImage() {
|
|
41
|
+
displayed = null; displayedGeometry = null
|
|
42
|
+
host.colorFilter = null; host.setImageDrawable(null)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private fun applyTint(result: OneKeyNamedSVG.RenderResult, color: String) {
|
|
46
|
+
host.colorFilter = result.tintColor(color)?.let { PorterDuffColorFilter(it, PorterDuff.Mode.SRC_IN) }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private fun refresh() {
|
|
50
|
+
val scale = host.resources.displayMetrics.density
|
|
51
|
+
val next = "$iconName|$resolvedColor|${host.width}|${host.height}|$scale|${host.attached}"
|
|
52
|
+
if (identity == next) return
|
|
53
|
+
identity = next; generation++
|
|
54
|
+
val revision = generation
|
|
55
|
+
OneKeyNamedSVG.cancel(owner)
|
|
56
|
+
val name = iconName
|
|
57
|
+
val color = resolvedColor
|
|
58
|
+
if (!host.attached || host.width !in 1..4096 || host.height !in 1..4096 ||
|
|
59
|
+
host.width.toLong() * host.height > 4194304 || name.isNullOrEmpty() || color == null ||
|
|
60
|
+
!OneKeyNamedSVG.isValidColor(color)) { clearImage(); return }
|
|
61
|
+
val geometry = "$name|${host.width}|${host.height}|$scale"
|
|
62
|
+
val ready = displayed
|
|
63
|
+
if (displayedGeometry == geometry && ready != null && host.drawable != null) {
|
|
64
|
+
if (ready.canTint(color)) { applyTint(ready, color); return }
|
|
65
|
+
} else clearImage()
|
|
66
|
+
OneKeyNamedSVG.cachedRender(name, color, host.width, host.height, scale)?.let { hit ->
|
|
67
|
+
displayed = hit; displayedGeometry = geometry
|
|
68
|
+
applyTint(hit, color)
|
|
69
|
+
host.setImageBitmap(hit.bitmap)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
val weakHost = WeakReference(host)
|
|
73
|
+
val weakSelf = WeakReference(this)
|
|
74
|
+
OneKeyNamedSVG.requestRender(owner, host.context.applicationContext.assets, name, color,
|
|
75
|
+
host.width, host.height, scale) { result ->
|
|
76
|
+
val current = weakSelf.get()
|
|
77
|
+
val target = weakHost.get()
|
|
78
|
+
if (current?.generation != revision || target?.attached != true) return@requestRender
|
|
79
|
+
if (result == null) { current.clearImage(); return@requestRender }
|
|
80
|
+
current.displayed = result; current.displayedGeometry = geometry
|
|
81
|
+
current.applyTint(result, color)
|
|
82
|
+
target.setImageBitmap(result.bitmap)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun prepareForRecycle() {
|
|
87
|
+
iconName = null
|
|
88
|
+
resolvedColor = null
|
|
89
|
+
refresh()
|
|
90
|
+
}
|
|
91
|
+
override fun onDropView() { prepareForRecycle(); super.onDropView() }
|
|
92
|
+
override fun dispose() { prepareForRecycle(); host.changed = null; super.dispose() }
|
|
93
|
+
}
|
|
@@ -266,7 +266,7 @@ private class OneKeySvgDecoder : ResourceDecoder<InputStream, SVG> {
|
|
|
266
266
|
options: Options,
|
|
267
267
|
): Resource<SVG> {
|
|
268
268
|
val svg = try {
|
|
269
|
-
|
|
269
|
+
OneKeySVG.parse(source)
|
|
270
270
|
} catch (error: SVGParseException) {
|
|
271
271
|
throw IOException("Cannot decode SVG", error)
|
|
272
272
|
}
|
|
@@ -5,6 +5,7 @@ import com.facebook.react.bridge.NativeModule
|
|
|
5
5
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
6
|
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
7
7
|
import com.facebook.react.uimanager.ViewManager
|
|
8
|
+
import com.margelo.nitro.onekeyimage.views.HybridOneKeyIconManager
|
|
8
9
|
import com.margelo.nitro.onekeyimage.views.HybridOneKeyImageManager
|
|
9
10
|
|
|
10
11
|
class OneKeyImagePackage : BaseReactPackage() {
|
|
@@ -17,7 +18,7 @@ class OneKeyImagePackage : BaseReactPackage() {
|
|
|
17
18
|
reactContext: ReactApplicationContext,
|
|
18
19
|
): List<ViewManager<*, *>> {
|
|
19
20
|
OneKeyImageGlideRegistry.ensureRegistered(reactContext)
|
|
20
|
-
return listOf(HybridOneKeyImageManager())
|
|
21
|
+
return listOf(HybridOneKeyImageManager(), HybridOneKeyIconManager())
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
companion object {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
package com.margelo.nitro.onekeyimage
|
|
2
|
+
|
|
3
|
+
import android.content.res.AssetManager
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.Canvas
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.graphics.Paint
|
|
8
|
+
import android.graphics.PorterDuff
|
|
9
|
+
import android.graphics.PorterDuffColorFilter
|
|
10
|
+
import android.os.Handler
|
|
11
|
+
import android.os.Looper
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import org.json.JSONObject
|
|
14
|
+
import java.util.concurrent.Executors
|
|
15
|
+
import kotlin.math.roundToInt
|
|
16
|
+
|
|
17
|
+
/** Process-owned named resources; no React runtime or Activity is retained. */
|
|
18
|
+
object OneKeyNamedSVG {
|
|
19
|
+
enum class RecolorMode { FIXED, SINGLE, MULTIPLE, RASTER }
|
|
20
|
+
data class IconMetadata(
|
|
21
|
+
val width: Float,
|
|
22
|
+
val height: Float,
|
|
23
|
+
val mode: RecolorMode,
|
|
24
|
+
)
|
|
25
|
+
data class RenderResult(val bitmap: Bitmap, val mode: RecolorMode,
|
|
26
|
+
val sourceIdentity: String, val maskAlpha: Int) {
|
|
27
|
+
fun canTint(color: String): Boolean {
|
|
28
|
+
val value = parseColor(color) ?: return false
|
|
29
|
+
return mode == RecolorMode.FIXED || mode == RecolorMode.SINGLE ||
|
|
30
|
+
(mode == RecolorMode.MULTIPLE && Color.alpha(value) == maskAlpha)
|
|
31
|
+
}
|
|
32
|
+
fun tintColor(color: String): Int? {
|
|
33
|
+
val value = parseColor(color) ?: return null
|
|
34
|
+
return when(mode) {
|
|
35
|
+
RecolorMode.SINGLE -> value
|
|
36
|
+
RecolorMode.MULTIPLE -> value or (0xff shl 24)
|
|
37
|
+
else -> null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
internal fun finalBitmap(color: String): Bitmap {
|
|
41
|
+
val tint = tintColor(color) ?: return bitmap
|
|
42
|
+
if (tint == Color.BLACK) return bitmap
|
|
43
|
+
val output = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
|
|
44
|
+
val paint = Paint().apply { colorFilter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_IN) }
|
|
45
|
+
Canvas(output).drawBitmap(bitmap, 0f, 0f, paint)
|
|
46
|
+
return output
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private data class Request(val assets: AssetManager, val name: String, val color: String,
|
|
51
|
+
val width: Int, val height: Int, val scale: Float, val finalColor: Boolean, val complete: (RenderResult?) -> Unit)
|
|
52
|
+
private val lock = Any()
|
|
53
|
+
private val pending = LinkedHashMap<Any, Request>()
|
|
54
|
+
private val cache = LinkedHashMap<String, RenderResult>(128, 0.75f, true)
|
|
55
|
+
private var cacheBytes = 0L
|
|
56
|
+
private var activeWorkers = 0
|
|
57
|
+
private var manifest: JSONObject? = null
|
|
58
|
+
private val manifestLock = Any()
|
|
59
|
+
private val workers = Executors.newFixedThreadPool(2)
|
|
60
|
+
private val main = Handler(Looper.getMainLooper())
|
|
61
|
+
private val colorPattern = Regex("rgba\\(\\d{1,3},\\d{1,3},\\d{1,3},(?:0(?:\\.\\d+)?|1(?:\\.0+)?)\\)")
|
|
62
|
+
|
|
63
|
+
private var manifestMissingLogged = false
|
|
64
|
+
|
|
65
|
+
fun cancel(owner: Any) { synchronized(lock) { pending.remove(owner) } }
|
|
66
|
+
|
|
67
|
+
/** Caller holds manifestLock. Hosts own the resources, so a miss is logged once. */
|
|
68
|
+
private fun loadManifestLocked(assets: AssetManager): JSONObject? {
|
|
69
|
+
manifest?.let { return it }
|
|
70
|
+
return try {
|
|
71
|
+
assets.open("icons/manifest.json").bufferedReader().use { JSONObject(it.readText()) }
|
|
72
|
+
.also { manifest = it }
|
|
73
|
+
} catch (error: Exception) {
|
|
74
|
+
if (!manifestMissingLogged) {
|
|
75
|
+
manifestMissingLogged = true
|
|
76
|
+
Log.w("OneKeyIcon", "assets/icons/manifest.json is missing or invalid; named icons will not render", error)
|
|
77
|
+
}
|
|
78
|
+
null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun modeOf(metadata: JSONObject): RecolorMode = try {
|
|
83
|
+
RecolorMode.valueOf(metadata.optString("recolorMode", "raster").uppercase())
|
|
84
|
+
} catch (_: IllegalArgumentException) {
|
|
85
|
+
RecolorMode.RASTER
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private data class RenderKey(val identity: String, val key: String, val maskAlpha: Int)
|
|
89
|
+
|
|
90
|
+
private fun renderKey(name: String, digest: String, width: Int, height: Int, scale: Float,
|
|
91
|
+
mode: RecolorMode, requestColor: String, color: Int): RenderKey {
|
|
92
|
+
val identity = "$name|$digest|$width|$height|$scale"
|
|
93
|
+
val maskAlpha = if (mode == RecolorMode.MULTIPLE) Color.alpha(color) else 255
|
|
94
|
+
val colorKey = when(mode) {
|
|
95
|
+
RecolorMode.RASTER -> requestColor
|
|
96
|
+
RecolorMode.MULTIPLE -> maskAlpha.toString()
|
|
97
|
+
else -> ""
|
|
98
|
+
}
|
|
99
|
+
return RenderKey(identity, "$identity|$mode|$colorKey", maskAlpha)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Main-thread fast path: returns an already-decoded result without asset I/O or
|
|
104
|
+
* manifest loading, so remounted and recycled views paint in the same frame.
|
|
105
|
+
*/
|
|
106
|
+
fun cachedRender(name: String, color: String, width: Int, height: Int, scale: Float): RenderResult? {
|
|
107
|
+
val value = parseColor(color) ?: return null
|
|
108
|
+
val metadata = synchronized(manifestLock) { manifest?.optJSONObject(name) } ?: return null
|
|
109
|
+
val digest = metadata.optString("digest").takeIf { it.isNotEmpty() } ?: return null
|
|
110
|
+
val key = renderKey(name, digest, width, height, scale, modeOf(metadata), color, value).key
|
|
111
|
+
return synchronized(lock) { cache[key] }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun iconMetadata(assets: AssetManager, name: String): IconMetadata? {
|
|
115
|
+
val trimmed = name.trim()
|
|
116
|
+
if (trimmed.isEmpty()) return null
|
|
117
|
+
val metadata = try {
|
|
118
|
+
synchronized(manifestLock) {
|
|
119
|
+
loadManifestLocked(assets)?.optJSONObject(trimmed)
|
|
120
|
+
}
|
|
121
|
+
} catch (_: Exception) {
|
|
122
|
+
null
|
|
123
|
+
} ?: return null
|
|
124
|
+
val mode = modeOf(metadata)
|
|
125
|
+
val viewBox = metadata.optJSONArray("viewBox")
|
|
126
|
+
val vbWidth = if (viewBox != null && viewBox.length() >= 4) viewBox.optDouble(2, 24.0).toFloat() else 24f
|
|
127
|
+
val vbHeight = if (viewBox != null && viewBox.length() >= 4) viewBox.optDouble(3, 24.0).toFloat() else 24f
|
|
128
|
+
val width = if (vbWidth > 0f) vbWidth else 24f
|
|
129
|
+
val height = if (vbHeight > 0f) vbHeight else 24f
|
|
130
|
+
return IconMetadata(width = width, height = height, mode = mode)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fun request(owner: Any, assets: AssetManager, name: String, color: String,
|
|
134
|
+
width: Int, height: Int, scale: Float, complete: (Bitmap?) -> Unit) {
|
|
135
|
+
enqueue(owner, assets, name, color, width, height, scale, true) { complete(it?.bitmap) }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun requestRender(owner: Any, assets: AssetManager, name: String, color: String,
|
|
139
|
+
width: Int, height: Int, scale: Float, complete: (RenderResult?) -> Unit) {
|
|
140
|
+
enqueue(owner, assets, name, color, width, height, scale, false, complete)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private fun enqueue(owner: Any, assets: AssetManager, name: String, color: String,
|
|
144
|
+
width: Int, height: Int, scale: Float, finalColor: Boolean, complete: (RenderResult?) -> Unit) {
|
|
145
|
+
synchronized(lock) {
|
|
146
|
+
pending[owner] = Request(assets, name, color, width, height, scale, finalColor, complete)
|
|
147
|
+
while (activeWorkers < 2 && activeWorkers < pending.size) {
|
|
148
|
+
activeWorkers++
|
|
149
|
+
workers.execute { drain() }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private fun parseColor(color: String): Int? {
|
|
155
|
+
if (!colorPattern.matches(color)) return null
|
|
156
|
+
val channels = color.removePrefix("rgba(").removeSuffix(")").split(',')
|
|
157
|
+
val rgb = channels.take(3).map { it.toInt() }
|
|
158
|
+
if (rgb.any { it !in 0..255 }) return null
|
|
159
|
+
val alpha = (channels[3].toDouble() * 255).roundToInt()
|
|
160
|
+
return Color.argb(alpha, rgb[0], rgb[1], rgb[2])
|
|
161
|
+
}
|
|
162
|
+
fun isValidColor(color: String): Boolean = parseColor(color) != null
|
|
163
|
+
|
|
164
|
+
private fun drain() {
|
|
165
|
+
while (true) {
|
|
166
|
+
val request = synchronized(lock) {
|
|
167
|
+
val first = pending.entries.firstOrNull()
|
|
168
|
+
if (first == null) { activeWorkers--; null }
|
|
169
|
+
else { pending.remove(first.key); first.value }
|
|
170
|
+
} ?: return
|
|
171
|
+
val bitmap = try {
|
|
172
|
+
decode(request)?.let { result ->
|
|
173
|
+
if (request.finalColor) result.copy(bitmap = result.finalBitmap(request.color), mode = RecolorMode.RASTER)
|
|
174
|
+
else result
|
|
175
|
+
}
|
|
176
|
+
} catch (_: Throwable) { null } // OOM must not strand activeWorkers.
|
|
177
|
+
main.post { request.complete(bitmap) }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private fun decode(request: Request): RenderResult? {
|
|
182
|
+
if (request.width !in 1..4096 || request.height !in 1..4096 ||
|
|
183
|
+
request.width.toLong() * request.height > 4194304 || !request.scale.isFinite() ||
|
|
184
|
+
request.scale <= 0) return null
|
|
185
|
+
val color = parseColor(request.color) ?: return null
|
|
186
|
+
val metadata = synchronized(manifestLock) {
|
|
187
|
+
loadManifestLocked(request.assets)?.optJSONObject(request.name)
|
|
188
|
+
} ?: return null
|
|
189
|
+
val mode = modeOf(metadata)
|
|
190
|
+
val (sourceIdentity, key, maskAlpha) = renderKey(request.name, metadata.getString("digest"),
|
|
191
|
+
request.width, request.height, request.scale, mode, request.color, color)
|
|
192
|
+
val paintColor = when(mode) {
|
|
193
|
+
RecolorMode.FIXED, RecolorMode.SINGLE -> Color.BLACK
|
|
194
|
+
RecolorMode.MULTIPLE -> Color.argb(maskAlpha, 0, 0, 0)
|
|
195
|
+
RecolorMode.RASTER -> color
|
|
196
|
+
}
|
|
197
|
+
synchronized(lock) { cache[key]?.let { return it } }
|
|
198
|
+
// AndroidSVG rgba quantizes with 256; SVG hex alpha preserves the exact CSS 8-bit channel.
|
|
199
|
+
val svgColor = listOf(Color.red(paintColor), Color.green(paintColor), Color.blue(paintColor), Color.alpha(paintColor))
|
|
200
|
+
.joinToString(separator = "", prefix = "#") { it.toString(16).padStart(2, '0') }
|
|
201
|
+
val xml = request.assets.open("icons/${request.name}.svg").bufferedReader().use { it.readText() }
|
|
202
|
+
.replace(Regex("((?:fill|stroke)\\s*=\\s*[\"'])currentColor([\"'])"), "$1${svgColor}$2")
|
|
203
|
+
val bitmap = OneKeySVG.decode(xml.byteInputStream()).render(request.width, request.height)
|
|
204
|
+
val result = RenderResult(bitmap, mode, sourceIdentity, maskAlpha)
|
|
205
|
+
val bytes = bitmap.allocationByteCount.toLong()
|
|
206
|
+
if (bytes <= 8388608) synchronized(lock) {
|
|
207
|
+
cache.put(key, result)?.let { cacheBytes -= it.bitmap.allocationByteCount }
|
|
208
|
+
cacheBytes += bytes
|
|
209
|
+
while (cache.size > 128 || cacheBytes > 8388608) {
|
|
210
|
+
val first = cache.entries.first()
|
|
211
|
+
cacheBytes -= first.value.bitmap.allocationByteCount
|
|
212
|
+
cache.remove(first.key)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return result
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.margelo.nitro.onekeyimage
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import com.caverock.androidsvg.SVG
|
|
6
|
+
import java.io.InputStream
|
|
7
|
+
|
|
8
|
+
/** Synchronous per-request document; loaders retain scheduling and cancellation. */
|
|
9
|
+
object OneKeySVG {
|
|
10
|
+
fun decode(source: InputStream): OneKeySVGDocument = OneKeySVGDocument(parse(source))
|
|
11
|
+
internal fun parse(source: InputStream): SVG = SVG.getFromInputStream(source)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class OneKeySVGDocument internal constructor(private val svg: SVG) {
|
|
15
|
+
val viewBoxWidth: Float? = svg.documentViewBox?.width()
|
|
16
|
+
val viewBoxHeight: Float? = svg.documentViewBox?.height()
|
|
17
|
+
val intrinsicWidth: Float = svg.documentWidth
|
|
18
|
+
val intrinsicHeight: Float = svg.documentHeight
|
|
19
|
+
|
|
20
|
+
fun render(width: Int, height: Int): Bitmap {
|
|
21
|
+
require(width > 0 && height > 0)
|
|
22
|
+
if (svg.documentViewBox == null && intrinsicWidth > 0 && intrinsicHeight > 0) {
|
|
23
|
+
svg.setDocumentViewBox(0f, 0f, intrinsicWidth, intrinsicHeight)
|
|
24
|
+
}
|
|
25
|
+
svg.setDocumentWidth(width.toFloat())
|
|
26
|
+
svg.setDocumentHeight(height.toFloat())
|
|
27
|
+
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
28
|
+
svg.renderToCanvas(Canvas(bitmap))
|
|
29
|
+
return bitmap
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import NitroModules
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
private final class OneKeyIconHost: UIImageView {
|
|
5
|
+
var changed: (() -> Void)?
|
|
6
|
+
override func layoutSubviews() { super.layoutSubviews(); changed?() }
|
|
7
|
+
override func didMoveToWindow() { super.didMoveToWindow(); changed?() }
|
|
8
|
+
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
9
|
+
super.traitCollectionDidChange(previousTraitCollection)
|
|
10
|
+
if previousTraitCollection?.displayScale != traitCollection.displayScale { changed?() }
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
final class HybridOneKeyIcon: HybridOneKeyIconSpec, RecyclableView {
|
|
15
|
+
private let host = OneKeyIconHost()
|
|
16
|
+
private let owner = NSObject()
|
|
17
|
+
private var generation: UInt64 = 0
|
|
18
|
+
private var identity: String?
|
|
19
|
+
private var displayedGeometry: String?
|
|
20
|
+
private var displayed: OneKeyNamedSVG.RenderResult?
|
|
21
|
+
var iconName: String?
|
|
22
|
+
var resolvedColor: String?
|
|
23
|
+
var view: UIView { host }
|
|
24
|
+
|
|
25
|
+
override init() {
|
|
26
|
+
super.init()
|
|
27
|
+
host.contentMode = .scaleAspectFit
|
|
28
|
+
host.changed = { [weak self] in self?.refresh() }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func afterUpdate() { refresh() }
|
|
32
|
+
|
|
33
|
+
private func clearImage() {
|
|
34
|
+
displayed = nil; displayedGeometry = nil; host.image = nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private func apply(_ result: OneKeyNamedSVG.RenderResult, color: String) {
|
|
38
|
+
displayed = result
|
|
39
|
+
if let tint = result.tintColor(color) {
|
|
40
|
+
host.image = result.image.withRenderingMode(.alwaysTemplate)
|
|
41
|
+
host.tintColor = tint
|
|
42
|
+
} else {
|
|
43
|
+
host.image = result.image.withRenderingMode(.alwaysOriginal)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private func refresh() {
|
|
48
|
+
let scale = host.window?.screen.scale ?? 1
|
|
49
|
+
let width = ceil(host.bounds.width * scale), height = ceil(host.bounds.height * scale)
|
|
50
|
+
let attached = host.window != nil
|
|
51
|
+
let next = "\(iconName ?? "")|\(resolvedColor ?? "")|\(width)|\(height)|\(scale)|\(attached)"
|
|
52
|
+
guard next != identity else { return }
|
|
53
|
+
identity = next; generation &+= 1
|
|
54
|
+
let revision = generation
|
|
55
|
+
OneKeyNamedSVG.shared.cancel(owner: owner)
|
|
56
|
+
guard attached, width.isFinite, height.isFinite,
|
|
57
|
+
width > 0, height > 0, width <= 4096, height <= 4096, width * height <= 4_194_304,
|
|
58
|
+
let name = iconName, !name.isEmpty,
|
|
59
|
+
let color = resolvedColor, OneKeyNamedSVG.isValidColor(color) else { clearImage(); return }
|
|
60
|
+
let geometry = "\(name)|\(width)|\(height)|\(scale)"
|
|
61
|
+
if displayedGeometry == geometry, let result = displayed, host.image != nil {
|
|
62
|
+
if result.canTint(color) {
|
|
63
|
+
if let tint = result.tintColor(color) { host.tintColor = tint }
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// Keep only a successful same-source/geometry raster while new color decodes.
|
|
67
|
+
} else { clearImage() }
|
|
68
|
+
if let hit = OneKeyNamedSVG.shared.cachedRender(name: name, color: color,
|
|
69
|
+
width: Int(width), height: Int(height), scale: scale) {
|
|
70
|
+
displayedGeometry = geometry
|
|
71
|
+
apply(hit, color: color)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
OneKeyNamedSVG.shared.requestRender(owner: owner, name: name, color: color,
|
|
75
|
+
width: Int(width), height: Int(height), scale: scale) { [weak self] result in
|
|
76
|
+
guard let self, self.generation == revision, self.host.window != nil else { return }
|
|
77
|
+
guard let result else { self.clearImage(); return }
|
|
78
|
+
self.displayedGeometry = geometry
|
|
79
|
+
self.apply(result, color: color)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func prepareForRecycle() {
|
|
84
|
+
iconName = nil; resolvedColor = nil; refresh()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func onDropView() { prepareForRecycle() }
|
|
88
|
+
|
|
89
|
+
deinit { OneKeyNamedSVG.shared.cancel(owner: owner) }
|
|
90
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
2
3
|
|
|
3
4
|
NS_ASSUME_NONNULL_BEGIN
|
|
4
5
|
|
|
@@ -8,6 +9,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
8
9
|
|
|
9
10
|
+ (void)addCodersToManager:(id)manager NS_SWIFT_NAME(addCoders(to:));
|
|
10
11
|
+ (void)ensureWebPCoderRegistered;
|
|
12
|
+
// A zero target preserves the decoder's intrinsic/vector result.
|
|
13
|
+
+ (nullable UIImage *)decodeSVGData:(NSData *)data pixelSize:(CGSize)pixelSize
|
|
14
|
+
NS_SWIFT_NAME(decodeSVG(_:pixelSize:));
|
|
11
15
|
|
|
12
16
|
@end
|
|
13
17
|
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
@implementation OneKeyImageCoderBridge
|
|
8
8
|
|
|
9
|
+
+ (UIImage *)decodeSVGData:(NSData *)data pixelSize:(CGSize)pixelSize {
|
|
10
|
+
NSDictionary *options = nil;
|
|
11
|
+
if (isfinite(pixelSize.width) && isfinite(pixelSize.height) && pixelSize.width > 0 && pixelSize.height > 0) {
|
|
12
|
+
options = @{SDImageCoderDecodeThumbnailPixelSize: [NSValue valueWithCGSize:pixelSize],
|
|
13
|
+
SDImageCoderDecodePreserveAspectRatio: @YES};
|
|
14
|
+
}
|
|
15
|
+
return [SDImageSVGCoder.sharedCoder decodedImageWithData:data options:options];
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
+ (void)addCodersToManager:(id)manager {
|
|
10
19
|
SDImageCodersManager *coderManager = (SDImageCodersManager *)manager;
|
|
11
20
|
[coderManager addCoder:SDImageSVGCoder.sharedCoder];
|