@onekeyfe/react-native-bottom-tabs 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/android/build.gradle +1 -1
- package/android/src/main/java/com/onekey/bottomtabs/BottomTabsView.kt +58 -7
- package/android/src/main/java/com/onekey/bottomtabs/SharedSvgDecoder.kt +51 -0
- package/docs/SPEC.md +32 -0
- package/ios/RCTBottomAccessoryContainerView.swift +2 -0
- package/ios/RCTBottomTabsContainerView.swift +216 -12
- package/ios/SVG/SvgDecoder.mm +7 -3
- package/ios/TabBarFontSize.swift +2 -0
- package/lib/module/BottomTabs.js +67 -49
- package/lib/typescript/src/BottomTabs.d.ts +11 -5
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/types.d.ts +6 -2
- package/package.json +2 -1
- package/react-native-bottom-tabs.podspec +2 -0
- package/src/BottomTabs.tsx +88 -47
- package/src/index.tsx +7 -1
- package/src/types.ts +10 -2
- package/ios/SVG/CoreSVG.h +0 -13
- package/ios/SVG/CoreSVG.mm +0 -177
package/android/build.gradle
CHANGED
|
@@ -107,7 +107,7 @@ dependencies {
|
|
|
107
107
|
|
|
108
108
|
implementation("io.coil-kt.coil3:coil:${COIL_VERSION}")
|
|
109
109
|
implementation("io.coil-kt.coil3:coil-network-okhttp:${COIL_VERSION}")
|
|
110
|
-
implementation("
|
|
110
|
+
implementation project(":onekeyfe_react-native-image")
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (isNewArchitectureEnabled()) {
|
|
@@ -9,6 +9,7 @@ import androidx.appcompat.view.ContextThemeWrapper
|
|
|
9
9
|
import android.graphics.Color
|
|
10
10
|
import android.graphics.Canvas
|
|
11
11
|
import android.graphics.Outline
|
|
12
|
+
import android.graphics.drawable.BitmapDrawable
|
|
12
13
|
import android.graphics.drawable.ColorDrawable
|
|
13
14
|
import android.graphics.drawable.Drawable
|
|
14
15
|
import android.graphics.drawable.GradientDrawable
|
|
@@ -39,7 +40,6 @@ import androidx.core.view.forEachIndexed
|
|
|
39
40
|
import coil3.ImageLoader
|
|
40
41
|
import coil3.asDrawable
|
|
41
42
|
import coil3.request.ImageRequest
|
|
42
|
-
import coil3.svg.SvgDecoder
|
|
43
43
|
import coil3.size.Precision
|
|
44
44
|
import coil3.size.Size as CoilSize
|
|
45
45
|
import coil3.size.Scale
|
|
@@ -55,9 +55,12 @@ import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY
|
|
|
55
55
|
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_LABELED
|
|
56
56
|
import com.google.android.material.navigation.NavigationBarView.LABEL_VISIBILITY_UNLABELED
|
|
57
57
|
import com.google.android.material.transition.platform.MaterialFadeThrough
|
|
58
|
+
import com.margelo.nitro.onekeyimage.OneKeyNamedSVG
|
|
58
59
|
import kotlin.math.abs
|
|
59
60
|
import kotlin.math.roundToInt
|
|
60
61
|
|
|
62
|
+
private const val ONEKEY_ICON_URI_PREFIX = "onekey-icon://"
|
|
63
|
+
|
|
61
64
|
private fun getMaterialContext(context: Context): Context {
|
|
62
65
|
return ContextThemeWrapper(context, com.google.android.material.R.style.Theme_MaterialComponents_DayNight)
|
|
63
66
|
}
|
|
@@ -193,6 +196,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
193
196
|
var items: MutableList<TabInfo> = mutableListOf()
|
|
194
197
|
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
|
|
195
198
|
private val drawableCache: MutableMap<ImageSource, Drawable> = mutableMapOf()
|
|
199
|
+
private val iconRequestTokens: MutableMap<Int, Any> = mutableMapOf()
|
|
196
200
|
|
|
197
201
|
private var isLayoutEnqueued = false
|
|
198
202
|
private var selectedItem: String? = null
|
|
@@ -260,7 +264,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
260
264
|
|
|
261
265
|
private val imageLoader = ImageLoader.Builder(context)
|
|
262
266
|
.components {
|
|
263
|
-
add(
|
|
267
|
+
add(SharedSvgDecoder.Factory())
|
|
264
268
|
}
|
|
265
269
|
.build()
|
|
266
270
|
|
|
@@ -466,7 +470,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
466
470
|
drawableCache.clear()
|
|
467
471
|
iconSources.forEach { (index, source) ->
|
|
468
472
|
bottomNavigation.menu.findItem(index)?.let { menuItem ->
|
|
469
|
-
getDrawable(source) { menuItem.icon = it }
|
|
473
|
+
getDrawable(index, source) { menuItem.icon = it }
|
|
470
474
|
}
|
|
471
475
|
}
|
|
472
476
|
}
|
|
@@ -1253,7 +1257,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1253
1257
|
|
|
1254
1258
|
menuItem.isVisible = !item.hidden
|
|
1255
1259
|
if (iconSources.containsKey(index)) {
|
|
1256
|
-
getDrawable(iconSources[index]!!) {
|
|
1260
|
+
getDrawable(index, iconSources[index]!!) {
|
|
1257
1261
|
menuItem.icon = it
|
|
1258
1262
|
}
|
|
1259
1263
|
}
|
|
@@ -1323,7 +1327,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1323
1327
|
|
|
1324
1328
|
// Update existing item if exists.
|
|
1325
1329
|
bottomNavigation.menu.findItem(idx)?.let { menuItem ->
|
|
1326
|
-
getDrawable(imageSource) {
|
|
1330
|
+
getDrawable(idx, imageSource) {
|
|
1327
1331
|
menuItem.icon = it
|
|
1328
1332
|
}
|
|
1329
1333
|
}
|
|
@@ -1355,11 +1359,50 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1355
1359
|
}
|
|
1356
1360
|
|
|
1357
1361
|
@SuppressLint("CheckResult")
|
|
1358
|
-
private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
|
|
1362
|
+
private fun getDrawable(index: Int, imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
|
|
1363
|
+
iconRequestTokens.remove(index)?.let { OneKeyNamedSVG.cancel(it) }
|
|
1359
1364
|
drawableCache[imageSource]?.let {
|
|
1360
1365
|
onDrawableReady(it)
|
|
1361
1366
|
return
|
|
1362
1367
|
}
|
|
1368
|
+
val rawSource = imageSource.uri.orEmpty()
|
|
1369
|
+
if (rawSource.startsWith(ONEKEY_ICON_URI_PREFIX)) {
|
|
1370
|
+
val iconName = rawSource.removePrefix(ONEKEY_ICON_URI_PREFIX).trim()
|
|
1371
|
+
if (iconName.isEmpty()) {
|
|
1372
|
+
onDrawableReady(null)
|
|
1373
|
+
return
|
|
1374
|
+
}
|
|
1375
|
+
val token = Any()
|
|
1376
|
+
iconRequestTokens[index] = token
|
|
1377
|
+
val iconSizePx = bottomNavigation.itemIconSize.coerceAtLeast(1)
|
|
1378
|
+
val density = resources.displayMetrics.density
|
|
1379
|
+
OneKeyNamedSVG.request(
|
|
1380
|
+
owner = token,
|
|
1381
|
+
assets = context.applicationContext.assets,
|
|
1382
|
+
name = iconName,
|
|
1383
|
+
color = "rgba(0,0,0,1)",
|
|
1384
|
+
width = iconSizePx,
|
|
1385
|
+
height = iconSizePx,
|
|
1386
|
+
scale = density,
|
|
1387
|
+
) { bitmap ->
|
|
1388
|
+
if (iconRequestTokens[index] !== token) {
|
|
1389
|
+
return@request
|
|
1390
|
+
}
|
|
1391
|
+
iconRequestTokens.remove(index)
|
|
1392
|
+
if (bitmap == null) {
|
|
1393
|
+
onDrawableReady(null)
|
|
1394
|
+
return@request
|
|
1395
|
+
}
|
|
1396
|
+
val stateDrawable = BitmapDrawable(context.resources, bitmap)
|
|
1397
|
+
drawableCache[imageSource] = stateDrawable
|
|
1398
|
+
if (iconSources[index] == imageSource) {
|
|
1399
|
+
onDrawableReady(stateDrawable)
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return
|
|
1403
|
+
}
|
|
1404
|
+
val token = Any()
|
|
1405
|
+
iconRequestTokens[index] = token
|
|
1363
1406
|
val iconSizePx = bottomNavigation.itemIconSize
|
|
1364
1407
|
val request = ImageRequest.Builder(context)
|
|
1365
1408
|
.data(imageSource.getUri(context))
|
|
@@ -1368,9 +1411,15 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1368
1411
|
.precision(Precision.EXACT)
|
|
1369
1412
|
.target { drawable ->
|
|
1370
1413
|
post {
|
|
1414
|
+
if (iconRequestTokens[index] !== token) {
|
|
1415
|
+
return@post
|
|
1416
|
+
}
|
|
1417
|
+
iconRequestTokens.remove(index)
|
|
1371
1418
|
val stateDrawable = drawable.asDrawable(context.resources)
|
|
1372
1419
|
drawableCache[imageSource] = stateDrawable
|
|
1373
|
-
|
|
1420
|
+
if (iconSources[index] == imageSource) {
|
|
1421
|
+
onDrawableReady(stateDrawable)
|
|
1422
|
+
}
|
|
1374
1423
|
}
|
|
1375
1424
|
}
|
|
1376
1425
|
.listener(
|
|
@@ -1440,6 +1489,8 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1440
1489
|
}
|
|
1441
1490
|
|
|
1442
1491
|
fun onDropViewInstance() {
|
|
1492
|
+
iconRequestTokens.values.forEach { OneKeyNamedSVG.cancel(it) }
|
|
1493
|
+
iconRequestTokens.clear()
|
|
1443
1494
|
androidBottomBlurEnabled = false
|
|
1444
1495
|
updateBlurAppearance()
|
|
1445
1496
|
releaseBottomBlur()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package com.onekey.bottomtabs
|
|
2
|
+
|
|
3
|
+
import coil3.ImageLoader
|
|
4
|
+
import coil3.asImage
|
|
5
|
+
import coil3.decode.DecodeResult
|
|
6
|
+
import coil3.decode.DecodeUtils
|
|
7
|
+
import coil3.decode.Decoder
|
|
8
|
+
import coil3.fetch.SourceFetchResult
|
|
9
|
+
import coil3.request.Options
|
|
10
|
+
import coil3.request.maxBitmapSize
|
|
11
|
+
import com.margelo.nitro.onekeyimage.OneKeySVG
|
|
12
|
+
import kotlinx.coroutines.runInterruptible
|
|
13
|
+
import okio.ByteString.Companion.encodeUtf8
|
|
14
|
+
import kotlin.math.roundToInt
|
|
15
|
+
|
|
16
|
+
/** Coil retains fetching, cancellation and sizing; image owns SVG parsing/rendering. */
|
|
17
|
+
internal class SharedSvgDecoder(private val result: SourceFetchResult, private val options: Options) : Decoder {
|
|
18
|
+
override suspend fun decode(): DecodeResult = runInterruptible {
|
|
19
|
+
val document = result.source.source().inputStream().use { OneKeySVG.decode(it) }
|
|
20
|
+
val width = document.viewBoxWidth ?: document.intrinsicWidth
|
|
21
|
+
val height = document.viewBoxHeight ?: document.intrinsicHeight
|
|
22
|
+
val destination = DecodeUtils.computeDstSize(
|
|
23
|
+
if (width > 0) width.roundToInt() else 512,
|
|
24
|
+
if (height > 0) height.roundToInt() else 512,
|
|
25
|
+
options.size, options.scale, options.maxBitmapSize)
|
|
26
|
+
val dstWidth = destination.first
|
|
27
|
+
val dstHeight = destination.second
|
|
28
|
+
val (outputWidth, outputHeight) = if (width > 0 && height > 0) {
|
|
29
|
+
val multiplier = DecodeUtils.computeSizeMultiplier(width, height, dstWidth.toFloat(), dstHeight.toFloat(), options.scale)
|
|
30
|
+
(width * multiplier).toInt() to (height * multiplier).toInt()
|
|
31
|
+
} else dstWidth to dstHeight
|
|
32
|
+
DecodeResult(document.render(outputWidth, outputHeight).asImage(), isSampled = true)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class Factory : Decoder.Factory {
|
|
36
|
+
override fun create(result: SourceFetchResult, options: Options, imageLoader: ImageLoader): Decoder? {
|
|
37
|
+
if (result.mimeType != "image/svg+xml") {
|
|
38
|
+
val source = result.source.source()
|
|
39
|
+
if (!source.rangeEquals(0, "<".encodeUtf8())) return null
|
|
40
|
+
val tag = "<svg".encodeUtf8()
|
|
41
|
+
var found = false
|
|
42
|
+
for (index in 0L until (1024L - tag.size)) {
|
|
43
|
+
if (!source.request(index + tag.size)) break
|
|
44
|
+
if (source.rangeEquals(index, tag)) { found = true; break }
|
|
45
|
+
}
|
|
46
|
+
if (!found) return null
|
|
47
|
+
}
|
|
48
|
+
return SharedSvgDecoder(result, options)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/docs/SPEC.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Native Tab View behavioral contract
|
|
2
2
|
|
|
3
|
+
## Shared image SVG decoder — Implemented
|
|
4
|
+
|
|
5
|
+
BottomTabs depends on the matching image SDK peer/native module and delegates
|
|
6
|
+
SVG byte decoding to its native facade. No new third-party dependency or SVG
|
|
7
|
+
renderer is introduced. JavaScript props, source resolution and existing tab
|
|
8
|
+
behavior remain unchanged. There is no independent Web SVG reader to migrate.
|
|
9
|
+
|
|
10
|
+
iOS keeps the existing SvgDecoder RCTImageDataDecoder registration (which is
|
|
11
|
+
available to the React image loader beyond tabs), source detection, failure
|
|
12
|
+
delivery and its no-op cancellation block; only data-to-image implementation
|
|
13
|
+
moves out of the local CoreSVG fork. Existing URI loaders, selected/unselected
|
|
14
|
+
and side-accessory paths, SF Symbols, resize/tint and source ordering remain.
|
|
15
|
+
Raw decoding may retain intrinsic/vector dimensions where the old decoder
|
|
16
|
+
ignored target dimensions; it does not use named-icon workers or wait for a
|
|
17
|
+
main-thread callback.
|
|
18
|
+
|
|
19
|
+
Android retains Coil fetching, cache and ordinary image decoders. Its SVG factory
|
|
20
|
+
uses image's parse/render facility with the existing requested dimensions,
|
|
21
|
+
Scale.FILL and Precision.EXACT semantics. The independent coil-svg dependency is
|
|
22
|
+
removed once that route is replaced; no URI loading is moved to Glide. Decoder
|
|
23
|
+
execution remains in Coil's existing worker/cancellation/error lifecycle, with
|
|
24
|
+
no additional pool, cache or main-queue hop. Mutable drawable bounds are owned
|
|
25
|
+
per request, while shared raster data remains immutable.
|
|
26
|
+
|
|
27
|
+
Focused SDK fixtures cover raw SVG and ordinary images through actual RCTImageLoader
|
|
28
|
+
and Coil/BottomTabs consumers, source sizing/non-square art and iOS SF Symbols.
|
|
29
|
+
The current iOS 18.6 fixture does not accept iOS 27.1 side placement; complete
|
|
30
|
+
App styling, platform navigation and signed release acceptance remain separate. Existing source callback races are
|
|
31
|
+
not silently repaired as part of decoding unification. Builds and runtime
|
|
32
|
+
evidence are reported separately. This implementation does not change global image
|
|
33
|
+
loader registration or default-device configuration.
|
|
34
|
+
|
|
3
35
|
## Native left drawer
|
|
4
36
|
|
|
5
37
|
Status: implemented in module source. Package TypeScript, iOS/Android Fabric
|
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import UIKit
|
|
3
|
+
#if canImport(React)
|
|
3
4
|
import React
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
@objc private protocol OneKeyNamedSVGBridgeProtocol: AnyObject {
|
|
8
|
+
@objc(cancelWithOwner:)
|
|
9
|
+
func cancel(owner: AnyObject)
|
|
10
|
+
|
|
11
|
+
@objc(requestWithOwner:name:color:width:height:scale:complete:)
|
|
12
|
+
func request(
|
|
13
|
+
owner: AnyObject,
|
|
14
|
+
name: String,
|
|
15
|
+
color: String,
|
|
16
|
+
width: Int,
|
|
17
|
+
height: Int,
|
|
18
|
+
scale: Double,
|
|
19
|
+
complete: @escaping (UIImage?) -> Void
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Bridges to OneKeyNamedSVG.shared in OneKeyImage without importing the OneKeyImage
|
|
24
|
+
/// Swift module (which transitively imports NitroModules C++ headers).
|
|
25
|
+
private enum OneKeyNamedSVGLoader {
|
|
26
|
+
private static var bridge: OneKeyNamedSVGBridgeProtocol? {
|
|
27
|
+
guard let cls: AnyClass = NSClassFromString("OneKeyNamedSVGBridge")
|
|
28
|
+
?? NSClassFromString("OneKeyImage.OneKeyNamedSVGBridge")
|
|
29
|
+
else {
|
|
30
|
+
return nil
|
|
31
|
+
}
|
|
32
|
+
let meta = cls as AnyObject
|
|
33
|
+
guard meta.responds(to: #selector(OneKeyNamedSVGBridgeProtocol.cancel(owner:))),
|
|
34
|
+
meta.responds(to: #selector(OneKeyNamedSVGBridgeProtocol.request(owner:name:color:width:height:scale:complete:)))
|
|
35
|
+
else {
|
|
36
|
+
return nil
|
|
37
|
+
}
|
|
38
|
+
return unsafeBitCast(meta, to: OneKeyNamedSVGBridgeProtocol.self)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func cancel(owner: AnyObject) {
|
|
42
|
+
bridge?.cancel(owner: owner)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static func request(
|
|
46
|
+
owner: AnyObject,
|
|
47
|
+
name: String,
|
|
48
|
+
color: String,
|
|
49
|
+
width: Int,
|
|
50
|
+
height: Int,
|
|
51
|
+
scale: CGFloat,
|
|
52
|
+
complete: @escaping (UIImage?) -> Void
|
|
53
|
+
) {
|
|
54
|
+
guard let bridge else {
|
|
55
|
+
complete(nil)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
bridge.request(
|
|
59
|
+
owner: owner,
|
|
60
|
+
name: name,
|
|
61
|
+
color: color,
|
|
62
|
+
width: width,
|
|
63
|
+
height: height,
|
|
64
|
+
scale: Double(scale),
|
|
65
|
+
complete: complete
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
4
69
|
|
|
5
70
|
// MARK: - Data types for props from JS
|
|
6
71
|
|
|
@@ -77,6 +142,10 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
77
142
|
private var loadedSideAccessoryIcon: UIImage?
|
|
78
143
|
// Increments per icon load; a completion applies only to the latest one.
|
|
79
144
|
private var sideAccessoryIconRequest = 0
|
|
145
|
+
private static let oneKeyIconSchemePrefix = "onekey-icon://"
|
|
146
|
+
private var iconRequestTokens: [Int: NSObject] = [:]
|
|
147
|
+
private var selectedIconRequestTokens: [Int: NSObject] = [:]
|
|
148
|
+
private var sideAccessoryRequestToken = NSObject()
|
|
80
149
|
private var isSideAccessoryLayoutScheduled = false
|
|
81
150
|
private static let sideAccessorySize: CGFloat = 48
|
|
82
151
|
private static let sideAccessorySpacing: CGFloat = 12
|
|
@@ -188,6 +257,22 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
188
257
|
fatalError("init(coder:) has not been implemented")
|
|
189
258
|
}
|
|
190
259
|
|
|
260
|
+
deinit {
|
|
261
|
+
for token in iconRequestTokens.values {
|
|
262
|
+
OneKeyNamedSVGLoader.cancel(owner: token)
|
|
263
|
+
}
|
|
264
|
+
for token in selectedIconRequestTokens.values {
|
|
265
|
+
OneKeyNamedSVGLoader.cancel(owner: token)
|
|
266
|
+
}
|
|
267
|
+
OneKeyNamedSVGLoader.cancel(owner: sideAccessoryRequestToken)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private static func oneKeyIconName(from uri: String) -> String? {
|
|
271
|
+
guard uri.hasPrefix(oneKeyIconSchemePrefix) else { return nil }
|
|
272
|
+
let name = String(uri.dropFirst(oneKeyIconSchemePrefix.count)).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
273
|
+
return name.isEmpty ? nil : name
|
|
274
|
+
}
|
|
275
|
+
|
|
191
276
|
override func didAddSubview(_ subview: UIView) {
|
|
192
277
|
super.didAddSubview(subview)
|
|
193
278
|
if let tbcView = tabBarController?.view, subview !== tbcView {
|
|
@@ -217,7 +302,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
217
302
|
/// Where the system draws the tab bar: "bottom", or "leading"/"trailing" for
|
|
218
303
|
/// the vertical bar some devices (iPhone Duo) place in a side strip.
|
|
219
304
|
private func currentTabBarPlacement() -> String {
|
|
220
|
-
#if compiler(>=6.4)
|
|
305
|
+
#if compiler(>=6.4) && !targetEnvironment(macCatalyst)
|
|
221
306
|
if #available(iOS 27.1, *), let tbc = tabBarController {
|
|
222
307
|
switch tbc.traitCollection.verticalBarEdge {
|
|
223
308
|
case .leading: return "leading"
|
|
@@ -230,7 +315,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
230
315
|
}
|
|
231
316
|
|
|
232
317
|
private func registerVerticalBarTraitChanges(_ tbc: UITabBarController) {
|
|
233
|
-
#if compiler(>=6.4)
|
|
318
|
+
#if compiler(>=6.4) && !targetEnvironment(macCatalyst)
|
|
234
319
|
if #available(iOS 27.1, *) {
|
|
235
320
|
tbc.registerForTraitChanges(
|
|
236
321
|
UITraitCollection.systemTraitsAffectingVerticalBarEdge
|
|
@@ -340,6 +425,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
340
425
|
private func loadSideAccessoryIcon() {
|
|
341
426
|
sideAccessoryIconRequest += 1
|
|
342
427
|
let request = sideAccessoryIconRequest
|
|
428
|
+
OneKeyNamedSVGLoader.cancel(owner: sideAccessoryRequestToken)
|
|
343
429
|
// Drop the previous icon so a slower or failed load never leaves it shown
|
|
344
430
|
// for the new source.
|
|
345
431
|
if loadedSideAccessoryIcon != nil {
|
|
@@ -347,7 +433,37 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
347
433
|
scheduleSideAccessoryLayout()
|
|
348
434
|
}
|
|
349
435
|
let source = sideAccessoryIcon.map { IconSource(dict: $0) }
|
|
350
|
-
guard let source,
|
|
436
|
+
guard let source, !source.uri.isEmpty else { return }
|
|
437
|
+
if let iconName = Self.oneKeyIconName(from: source.uri) {
|
|
438
|
+
let token = NSObject()
|
|
439
|
+
sideAccessoryRequestToken = token
|
|
440
|
+
let scale = max(1.0, window?.screen.scale ?? UIScreen.main.scale)
|
|
441
|
+
let pxW = max(1, Int(ceil(iconSize.width * scale)))
|
|
442
|
+
let pxH = max(1, Int(ceil(iconSize.height * scale)))
|
|
443
|
+
OneKeyNamedSVGLoader.request(
|
|
444
|
+
owner: token,
|
|
445
|
+
name: iconName,
|
|
446
|
+
color: "rgba(0,0,0,1)",
|
|
447
|
+
width: pxW,
|
|
448
|
+
height: pxH,
|
|
449
|
+
scale: scale
|
|
450
|
+
) { [weak self] image in
|
|
451
|
+
guard let self,
|
|
452
|
+
request == self.sideAccessoryIconRequest,
|
|
453
|
+
self.sideAccessoryRequestToken === token,
|
|
454
|
+
let image else { return }
|
|
455
|
+
let scaledImage: UIImage
|
|
456
|
+
if let cgImage = image.cgImage {
|
|
457
|
+
scaledImage = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
|
|
458
|
+
} else {
|
|
459
|
+
scaledImage = image
|
|
460
|
+
}
|
|
461
|
+
self.loadedSideAccessoryIcon = scaledImage.withRenderingMode(.alwaysTemplate)
|
|
462
|
+
self.scheduleSideAccessoryLayout()
|
|
463
|
+
}
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
guard let url = URL(string: source.uri) else { return }
|
|
351
467
|
// The image loader arrives with the first state update; setImageLoader
|
|
352
468
|
// calls this again.
|
|
353
469
|
guard let imageLoader else { return }
|
|
@@ -755,21 +871,69 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
755
871
|
|
|
756
872
|
@objc func setImageLoader(_ loader: RCTImageLoaderProtocol) {
|
|
757
873
|
self.imageLoader = loader
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
874
|
+
let iconSources = (icons ?? []).map { IconSource(dict: $0) }
|
|
875
|
+
if iconSources.contains(where: { !$0.uri.isEmpty && Self.oneKeyIconName(from: $0.uri) == nil }) {
|
|
876
|
+
loadIconsFromSources()
|
|
877
|
+
}
|
|
878
|
+
let selectedIconSources = (selectedIcons ?? []).map { IconSource(dict: $0) }
|
|
879
|
+
if selectedIconSources.contains(where: { !$0.uri.isEmpty && Self.oneKeyIconName(from: $0.uri) == nil }) {
|
|
880
|
+
loadSelectedIconsFromSources()
|
|
881
|
+
}
|
|
882
|
+
if let sideSource = sideAccessoryIcon.map({ IconSource(dict: $0) }),
|
|
883
|
+
!sideSource.uri.isEmpty,
|
|
884
|
+
Self.oneKeyIconName(from: sideSource.uri) == nil {
|
|
885
|
+
loadSideAccessoryIcon()
|
|
886
|
+
}
|
|
761
887
|
}
|
|
762
888
|
|
|
763
889
|
private func loadIconsFromSources() {
|
|
764
|
-
guard let
|
|
890
|
+
guard let iconDicts = icons else { return }
|
|
765
891
|
|
|
766
892
|
let iconSources = iconDicts.map { IconSource(dict: $0) }
|
|
893
|
+
let imageLoader = self.imageLoader
|
|
894
|
+
|
|
895
|
+
for staleIndex in iconRequestTokens.keys where staleIndex >= iconSources.count {
|
|
896
|
+
if let previous = iconRequestTokens.removeValue(forKey: staleIndex) {
|
|
897
|
+
OneKeyNamedSVGLoader.cancel(owner: previous)
|
|
898
|
+
}
|
|
899
|
+
}
|
|
767
900
|
|
|
768
901
|
for (index, source) in iconSources.enumerated() {
|
|
902
|
+
if let previous = iconRequestTokens.removeValue(forKey: index) {
|
|
903
|
+
OneKeyNamedSVGLoader.cancel(owner: previous)
|
|
904
|
+
}
|
|
769
905
|
guard !source.uri.isEmpty else { continue }
|
|
906
|
+
let token = NSObject()
|
|
907
|
+
iconRequestTokens[index] = token
|
|
908
|
+
|
|
909
|
+
if let iconName = Self.oneKeyIconName(from: source.uri) {
|
|
910
|
+
let scale = max(1.0, window?.screen.scale ?? UIScreen.main.scale)
|
|
911
|
+
let pxW = max(1, Int(ceil(iconSize.width * scale)))
|
|
912
|
+
let pxH = max(1, Int(ceil(iconSize.height * scale)))
|
|
913
|
+
OneKeyNamedSVGLoader.request(
|
|
914
|
+
owner: token,
|
|
915
|
+
name: iconName,
|
|
916
|
+
color: "rgba(0,0,0,1)",
|
|
917
|
+
width: pxW,
|
|
918
|
+
height: pxH,
|
|
919
|
+
scale: scale
|
|
920
|
+
) { [weak self] image in
|
|
921
|
+
guard let self,
|
|
922
|
+
self.iconRequestTokens[index] === token,
|
|
923
|
+
let image else { return }
|
|
924
|
+
let scaledImage: UIImage
|
|
925
|
+
if let cgImage = image.cgImage {
|
|
926
|
+
scaledImage = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
|
|
927
|
+
} else {
|
|
928
|
+
scaledImage = image
|
|
929
|
+
}
|
|
930
|
+
self.loadedIcons[index] = scaledImage
|
|
931
|
+
self.rebuildViewControllers()
|
|
932
|
+
}
|
|
933
|
+
continue
|
|
934
|
+
}
|
|
770
935
|
|
|
771
|
-
let url = URL(string: source.uri)
|
|
772
|
-
guard let url else { continue }
|
|
936
|
+
guard let imageLoader, let url = URL(string: source.uri) else { continue }
|
|
773
937
|
|
|
774
938
|
let request = URLRequest(url: url)
|
|
775
939
|
let size = CGSize(width: source.width, height: source.height)
|
|
@@ -787,6 +951,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
787
951
|
if error != nil { return }
|
|
788
952
|
guard let image, let self else { return }
|
|
789
953
|
DispatchQueue.main.async {
|
|
954
|
+
guard self.iconRequestTokens[index] === token else { return }
|
|
790
955
|
self.loadedIcons[index] = image.resizeImageTo(size: self.iconSize)
|
|
791
956
|
self.rebuildViewControllers()
|
|
792
957
|
}
|
|
@@ -795,15 +960,53 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
795
960
|
}
|
|
796
961
|
|
|
797
962
|
private func loadSelectedIconsFromSources() {
|
|
798
|
-
guard let
|
|
963
|
+
guard let iconDicts = selectedIcons else { return }
|
|
799
964
|
|
|
800
965
|
let iconSources = iconDicts.map { IconSource(dict: $0) }
|
|
966
|
+
let imageLoader = self.imageLoader
|
|
967
|
+
|
|
968
|
+
for staleIndex in selectedIconRequestTokens.keys where staleIndex >= iconSources.count {
|
|
969
|
+
if let previous = selectedIconRequestTokens.removeValue(forKey: staleIndex) {
|
|
970
|
+
OneKeyNamedSVGLoader.cancel(owner: previous)
|
|
971
|
+
}
|
|
972
|
+
}
|
|
801
973
|
|
|
802
974
|
for (index, source) in iconSources.enumerated() {
|
|
975
|
+
if let previous = selectedIconRequestTokens.removeValue(forKey: index) {
|
|
976
|
+
OneKeyNamedSVGLoader.cancel(owner: previous)
|
|
977
|
+
}
|
|
803
978
|
guard !source.uri.isEmpty else { continue }
|
|
979
|
+
let token = NSObject()
|
|
980
|
+
selectedIconRequestTokens[index] = token
|
|
981
|
+
|
|
982
|
+
if let iconName = Self.oneKeyIconName(from: source.uri) {
|
|
983
|
+
let scale = max(1.0, window?.screen.scale ?? UIScreen.main.scale)
|
|
984
|
+
let pxW = max(1, Int(ceil(iconSize.width * scale)))
|
|
985
|
+
let pxH = max(1, Int(ceil(iconSize.height * scale)))
|
|
986
|
+
OneKeyNamedSVGLoader.request(
|
|
987
|
+
owner: token,
|
|
988
|
+
name: iconName,
|
|
989
|
+
color: "rgba(0,0,0,1)",
|
|
990
|
+
width: pxW,
|
|
991
|
+
height: pxH,
|
|
992
|
+
scale: scale
|
|
993
|
+
) { [weak self] image in
|
|
994
|
+
guard let self,
|
|
995
|
+
self.selectedIconRequestTokens[index] === token,
|
|
996
|
+
let image else { return }
|
|
997
|
+
let scaledImage: UIImage
|
|
998
|
+
if let cgImage = image.cgImage {
|
|
999
|
+
scaledImage = UIImage(cgImage: cgImage, scale: scale, orientation: .up)
|
|
1000
|
+
} else {
|
|
1001
|
+
scaledImage = image
|
|
1002
|
+
}
|
|
1003
|
+
self.loadedSelectedIcons[index] = scaledImage
|
|
1004
|
+
self.rebuildViewControllers()
|
|
1005
|
+
}
|
|
1006
|
+
continue
|
|
1007
|
+
}
|
|
804
1008
|
|
|
805
|
-
let url = URL(string: source.uri)
|
|
806
|
-
guard let url else { continue }
|
|
1009
|
+
guard let imageLoader, let url = URL(string: source.uri) else { continue }
|
|
807
1010
|
|
|
808
1011
|
let request = URLRequest(url: url)
|
|
809
1012
|
let size = CGSize(width: source.width, height: source.height)
|
|
@@ -821,6 +1024,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
821
1024
|
if error != nil { return }
|
|
822
1025
|
guard let image, let self else { return }
|
|
823
1026
|
DispatchQueue.main.async {
|
|
1027
|
+
guard self.selectedIconRequestTokens[index] === token else { return }
|
|
824
1028
|
self.loadedSelectedIcons[index] = image.resizeImageTo(size: self.iconSize)
|
|
825
1029
|
self.rebuildViewControllers()
|
|
826
1030
|
}
|
package/ios/SVG/SvgDecoder.mm
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#import "SvgDecoder.h"
|
|
2
|
-
#import
|
|
2
|
+
#import <OneKeyImage/OneKeyImageCoderBridge.h>
|
|
3
3
|
|
|
4
4
|
@implementation SvgDecoder
|
|
5
5
|
|
|
@@ -7,7 +7,11 @@ RCT_EXPORT_MODULE()
|
|
|
7
7
|
|
|
8
8
|
- (BOOL)canDecodeImageData:(NSData *)imageData
|
|
9
9
|
{
|
|
10
|
-
return
|
|
10
|
+
if (!imageData) return NO;
|
|
11
|
+
// Preserve the existing global React image-decoder selection boundary.
|
|
12
|
+
return [imageData rangeOfData:[@"</svg>" dataUsingEncoding:NSUTF8StringEncoding]
|
|
13
|
+
options:NSDataSearchBackwards
|
|
14
|
+
range:NSMakeRange(imageData.length - MIN(100, imageData.length), MIN(100, imageData.length))].location != NSNotFound;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
|
|
@@ -16,7 +20,7 @@ RCT_EXPORT_MODULE()
|
|
|
16
20
|
resizeMode:(RCTResizeMode)resizeMode
|
|
17
21
|
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
|
|
18
22
|
{
|
|
19
|
-
UIImage *image = [
|
|
23
|
+
UIImage *image = [OneKeyImageCoderBridge decodeSVGData:imageData pixelSize:CGSizeZero];
|
|
20
24
|
|
|
21
25
|
if (image) {
|
|
22
26
|
completionHandler(nil, image);
|
package/ios/TabBarFontSize.swift
CHANGED
package/lib/module/BottomTabs.js
CHANGED
|
@@ -3,16 +3,74 @@
|
|
|
3
3
|
import React, { useLayoutEffect, useRef } from "react";
|
|
4
4
|
import { Image, Platform, StyleSheet, View, processColor } from "react-native";
|
|
5
5
|
import { BottomTabBarHeightContext } from "./utils/BottomTabBarHeightContext.js";
|
|
6
|
-
|
|
7
|
-
// eslint-disable-next-line @react-native/no-deep-imports
|
|
8
|
-
|
|
9
6
|
import NativeBottomTabs from "./BottomTabsNativeComponent";
|
|
10
7
|
import useLatestCallback from "use-latest-callback";
|
|
11
8
|
import DelayedFreeze from "./DelayedFreeze.js";
|
|
12
9
|
import NativeBottomTabsDrawer from "./BottomTabsDrawerNativeComponent";
|
|
13
10
|
import { BottomAccessoryView } from "./BottomAccessoryView.js";
|
|
14
11
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
15
|
-
const isAppleSymbol = icon => icon
|
|
12
|
+
const isAppleSymbol = icon => Boolean(typeof icon === "object" && icon !== null && "sfSymbol" in icon && typeof icon.sfSymbol === "string");
|
|
13
|
+
const ONEKEY_ICON_URI_PREFIX = "onekey-icon://";
|
|
14
|
+
export const resolveTabIconSource = icon => {
|
|
15
|
+
if (!icon || isAppleSymbol(icon)) {
|
|
16
|
+
return {
|
|
17
|
+
uri: "",
|
|
18
|
+
width: 0,
|
|
19
|
+
height: 0,
|
|
20
|
+
scale: 1
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (typeof icon === "string") {
|
|
24
|
+
const trimmed = icon.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
return {
|
|
27
|
+
uri: "",
|
|
28
|
+
width: 0,
|
|
29
|
+
height: 0,
|
|
30
|
+
scale: 1
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const uri = trimmed.startsWith(ONEKEY_ICON_URI_PREFIX) ? trimmed : `${ONEKEY_ICON_URI_PREFIX}${trimmed}`;
|
|
34
|
+
return {
|
|
35
|
+
uri,
|
|
36
|
+
width: 24,
|
|
37
|
+
height: 24,
|
|
38
|
+
scale: 1
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (typeof icon === "object" && !Array.isArray(icon) && "iconName" in icon && typeof icon.iconName === "string") {
|
|
42
|
+
const trimmed = icon.iconName.trim();
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
return {
|
|
45
|
+
uri: "",
|
|
46
|
+
width: 0,
|
|
47
|
+
height: 0,
|
|
48
|
+
scale: 1
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
uri: `${ONEKEY_ICON_URI_PREFIX}${trimmed}`,
|
|
53
|
+
width: 24,
|
|
54
|
+
height: 24,
|
|
55
|
+
scale: 1
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (typeof icon === "object" && !Array.isArray(icon) && "uri" in icon && typeof icon.uri === "string" && icon.uri.startsWith(ONEKEY_ICON_URI_PREFIX)) {
|
|
59
|
+
return {
|
|
60
|
+
uri: icon.uri,
|
|
61
|
+
width: typeof icon.width === "number" && icon.width > 0 ? icon.width : 24,
|
|
62
|
+
height: typeof icon.height === "number" && icon.height > 0 ? icon.height : 24,
|
|
63
|
+
scale: typeof icon.scale === "number" && icon.scale > 0 ? icon.scale : 1
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const resolved = Image.resolveAssetSource(icon);
|
|
67
|
+
return {
|
|
68
|
+
uri: resolved?.uri ?? "",
|
|
69
|
+
width: resolved?.width ?? 0,
|
|
70
|
+
height: resolved?.height ?? 0,
|
|
71
|
+
scale: resolved?.scale ?? 1
|
|
72
|
+
};
|
|
73
|
+
};
|
|
16
74
|
const ANDROID_MAX_TABS = 100;
|
|
17
75
|
const BottomTabs = ({
|
|
18
76
|
navigationState,
|
|
@@ -169,45 +227,11 @@ const BottomTabs = ({
|
|
|
169
227
|
})
|
|
170
228
|
};
|
|
171
229
|
}), [trimmedRoutes, icons, getLabelText, getBadge, getBadgeBackgroundColor, getBadgeTextColor, getActiveTintColor, getHidden, getTestID, getRole, getPreventsDefault]);
|
|
172
|
-
const resolvedIconAssets = React.useMemo(() => icons.map(icon =>
|
|
173
|
-
if (icon && !isAppleSymbol(icon)) {
|
|
174
|
-
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
175
|
-
const resolved = Image.resolveAssetSource(icon);
|
|
176
|
-
return {
|
|
177
|
-
uri: resolved?.uri ?? "",
|
|
178
|
-
width: resolved?.width ?? 0,
|
|
179
|
-
height: resolved?.height ?? 0,
|
|
180
|
-
scale: resolved?.scale ?? 1
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
uri: "",
|
|
185
|
-
width: 0,
|
|
186
|
-
height: 0,
|
|
187
|
-
scale: 1
|
|
188
|
-
};
|
|
189
|
-
}), [icons]);
|
|
230
|
+
const resolvedIconAssets = React.useMemo(() => icons.map(icon => resolveTabIconSource(icon)), [icons]);
|
|
190
231
|
|
|
191
232
|
// Focused (solid) assets passed to iOS as selectedImage. iOS only — Android
|
|
192
233
|
// ignores selectedIcons and keeps swapping via the focusedKey array above.
|
|
193
|
-
const resolvedSelectedIconAssets = React.useMemo(() => focusedIcons.map(icon =>
|
|
194
|
-
if (icon && !isAppleSymbol(icon)) {
|
|
195
|
-
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
196
|
-
const resolved = Image.resolveAssetSource(icon);
|
|
197
|
-
return {
|
|
198
|
-
uri: resolved?.uri ?? "",
|
|
199
|
-
width: resolved?.width ?? 0,
|
|
200
|
-
height: resolved?.height ?? 0,
|
|
201
|
-
scale: resolved?.scale ?? 1
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return {
|
|
205
|
-
uri: "",
|
|
206
|
-
width: 0,
|
|
207
|
-
height: 0,
|
|
208
|
-
scale: 1
|
|
209
|
-
};
|
|
210
|
-
}), [focusedIcons]);
|
|
234
|
+
const resolvedSelectedIconAssets = React.useMemo(() => focusedIcons.map(icon => resolveTabIconSource(icon)), [focusedIcons]);
|
|
211
235
|
const jumpTo = useLatestCallback(key => {
|
|
212
236
|
const index = trimmedRoutes.findIndex(route => route.key === key);
|
|
213
237
|
if (index === -1) {
|
|
@@ -258,15 +282,9 @@ const BottomTabs = ({
|
|
|
258
282
|
if (!isIOS || !sideAccessoryIcon) {
|
|
259
283
|
return undefined;
|
|
260
284
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
uri: resolved.uri,
|
|
265
|
-
width: resolved.width ?? 0,
|
|
266
|
-
height: resolved.height ?? 0,
|
|
267
|
-
scale: resolved.scale ?? 1
|
|
268
|
-
} : undefined;
|
|
269
|
-
}, [sideAccessoryIcon]);
|
|
285
|
+
const resolved = resolveTabIconSource(sideAccessoryIcon);
|
|
286
|
+
return resolved.uri ? resolved : undefined;
|
|
287
|
+
}, [isIOS, sideAccessoryIcon]);
|
|
270
288
|
useLayoutEffect(() => {
|
|
271
289
|
if (renderCustomTabBar && customTabBarWrapperRef.current) {
|
|
272
290
|
customTabBarWrapperRef.current.measure((_x, _y, _width, height) => {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ColorValue, type ImageSourcePropType, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import type {
|
|
4
|
-
import type { AppleIcon, BaseRoute, NavigationState, TabBarPlacement, TabRole } from "./types";
|
|
3
|
+
import type { BaseRoute, BottomTabIconSource, NamedIcon, NavigationState, TabBarPlacement, TabRole } from "./types";
|
|
5
4
|
import { type RNBottomTabsAccessoryViewProps } from "./BottomAccessoryView";
|
|
5
|
+
type ResolvedTabIconSource = {
|
|
6
|
+
uri: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
scale: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const resolveTabIconSource: (icon: BottomTabIconSource | undefined | null) => ResolvedTabIconSource;
|
|
6
12
|
interface Props<Route extends BaseRoute> {
|
|
7
13
|
labeled?: boolean;
|
|
8
14
|
/** Optional physical-left menu; all motion and input are owned by native. */
|
|
@@ -120,10 +126,10 @@ interface Props<Route extends BaseRoute> {
|
|
|
120
126
|
/**
|
|
121
127
|
* Get icon for the tab.
|
|
122
128
|
*/
|
|
123
|
-
getIcon?: (
|
|
129
|
+
getIcon?: ({ route, focused, }: {
|
|
124
130
|
route: Route;
|
|
125
131
|
focused: boolean;
|
|
126
|
-
}) =>
|
|
132
|
+
}) => BottomTabIconSource | undefined | null;
|
|
127
133
|
/**
|
|
128
134
|
* Get hidden for the tab.
|
|
129
135
|
*/
|
|
@@ -220,7 +226,7 @@ interface Props<Route extends BaseRoute> {
|
|
|
220
226
|
* Icon of a round button shown above a vertical tab bar. The button only
|
|
221
227
|
* appears while the tab bar is vertical and visible. iOS only.
|
|
222
228
|
*/
|
|
223
|
-
sideAccessoryIcon?: ImageSourcePropType;
|
|
229
|
+
sideAccessoryIcon?: ImageSourcePropType | NamedIcon | string;
|
|
224
230
|
/**
|
|
225
231
|
* Accessibility label of the side accessory button.
|
|
226
232
|
*/
|
|
@@ -12,5 +12,5 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
|
|
|
12
12
|
/**
|
|
13
13
|
* Types
|
|
14
14
|
*/
|
|
15
|
-
export type { AppleIcon, TabBarPlacement, TabRole } from './types';
|
|
15
|
+
export type { AppleIcon, BottomTabIconSource, NamedIcon, TabBarPlacement, TabRole, } from './types';
|
|
16
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,6 +4,10 @@ export type IconSource = string | ImageSourcePropType;
|
|
|
4
4
|
export type AppleIcon = {
|
|
5
5
|
sfSymbol: SFSymbol;
|
|
6
6
|
};
|
|
7
|
+
export type NamedIcon = {
|
|
8
|
+
iconName: string;
|
|
9
|
+
};
|
|
10
|
+
export type BottomTabIconSource = ImageSourcePropType | AppleIcon | NamedIcon | string;
|
|
7
11
|
export type TabRole = 'search';
|
|
8
12
|
export type TabBarPlacement = 'bottom' | 'leading' | 'trailing';
|
|
9
13
|
export type BaseRoute = {
|
|
@@ -13,8 +17,8 @@ export type BaseRoute = {
|
|
|
13
17
|
badgeBackgroundColor?: string;
|
|
14
18
|
badgeTextColor?: string;
|
|
15
19
|
lazy?: boolean;
|
|
16
|
-
focusedIcon?:
|
|
17
|
-
unfocusedIcon?:
|
|
20
|
+
focusedIcon?: BottomTabIconSource;
|
|
21
|
+
unfocusedIcon?: BottomTabIconSource;
|
|
18
22
|
activeTintColor?: string;
|
|
19
23
|
hidden?: boolean;
|
|
20
24
|
testID?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/react-native-bottom-tabs",
|
|
3
|
-
"version": "6.7.0-alpha.
|
|
3
|
+
"version": "6.7.0-alpha.412",
|
|
4
4
|
"description": "Native bottom tabs (tab bar + scene container) for React Native on iOS and Android",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/module/index.js",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"typescript": "^5.9.2"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
+
"@onekeyfe/react-native-image": "6.7.0-alpha.412",
|
|
74
75
|
"react": "*",
|
|
75
76
|
"react-native": "*"
|
|
76
77
|
},
|
package/src/BottomTabs.tsx
CHANGED
|
@@ -13,9 +13,6 @@ import {
|
|
|
13
13
|
processColor,
|
|
14
14
|
} from "react-native";
|
|
15
15
|
import { BottomTabBarHeightContext } from "./utils/BottomTabBarHeightContext";
|
|
16
|
-
|
|
17
|
-
// eslint-disable-next-line @react-native/no-deep-imports
|
|
18
|
-
import type { ImageSource } from "react-native/Libraries/Image/ImageSource";
|
|
19
16
|
import NativeBottomTabs, {
|
|
20
17
|
type BottomTabsItems,
|
|
21
18
|
} from "./BottomTabsNativeComponent";
|
|
@@ -23,6 +20,8 @@ import useLatestCallback from "use-latest-callback";
|
|
|
23
20
|
import type {
|
|
24
21
|
AppleIcon,
|
|
25
22
|
BaseRoute,
|
|
23
|
+
BottomTabIconSource,
|
|
24
|
+
NamedIcon,
|
|
26
25
|
NavigationState,
|
|
27
26
|
TabBarPlacement,
|
|
28
27
|
TabRole,
|
|
@@ -34,8 +33,81 @@ import {
|
|
|
34
33
|
type RNBottomTabsAccessoryViewProps,
|
|
35
34
|
} from "./BottomAccessoryView";
|
|
36
35
|
|
|
37
|
-
const isAppleSymbol = (
|
|
38
|
-
icon
|
|
36
|
+
const isAppleSymbol = (
|
|
37
|
+
icon: BottomTabIconSource | undefined | null
|
|
38
|
+
): icon is AppleIcon =>
|
|
39
|
+
Boolean(
|
|
40
|
+
typeof icon === "object" &&
|
|
41
|
+
icon !== null &&
|
|
42
|
+
"sfSymbol" in icon &&
|
|
43
|
+
typeof icon.sfSymbol === "string"
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const ONEKEY_ICON_URI_PREFIX = "onekey-icon://";
|
|
47
|
+
|
|
48
|
+
type ResolvedTabIconSource = {
|
|
49
|
+
uri: string;
|
|
50
|
+
width: number;
|
|
51
|
+
height: number;
|
|
52
|
+
scale: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const resolveTabIconSource = (
|
|
56
|
+
icon: BottomTabIconSource | undefined | null
|
|
57
|
+
): ResolvedTabIconSource => {
|
|
58
|
+
if (!icon || isAppleSymbol(icon)) {
|
|
59
|
+
return { uri: "", width: 0, height: 0, scale: 1 };
|
|
60
|
+
}
|
|
61
|
+
if (typeof icon === "string") {
|
|
62
|
+
const trimmed = icon.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
return { uri: "", width: 0, height: 0, scale: 1 };
|
|
65
|
+
}
|
|
66
|
+
const uri = trimmed.startsWith(ONEKEY_ICON_URI_PREFIX)
|
|
67
|
+
? trimmed
|
|
68
|
+
: `${ONEKEY_ICON_URI_PREFIX}${trimmed}`;
|
|
69
|
+
return { uri, width: 24, height: 24, scale: 1 };
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
typeof icon === "object" &&
|
|
73
|
+
!Array.isArray(icon) &&
|
|
74
|
+
"iconName" in icon &&
|
|
75
|
+
typeof icon.iconName === "string"
|
|
76
|
+
) {
|
|
77
|
+
const trimmed = icon.iconName.trim();
|
|
78
|
+
if (!trimmed) {
|
|
79
|
+
return { uri: "", width: 0, height: 0, scale: 1 };
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
uri: `${ONEKEY_ICON_URI_PREFIX}${trimmed}`,
|
|
83
|
+
width: 24,
|
|
84
|
+
height: 24,
|
|
85
|
+
scale: 1,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
typeof icon === "object" &&
|
|
90
|
+
!Array.isArray(icon) &&
|
|
91
|
+
"uri" in icon &&
|
|
92
|
+
typeof icon.uri === "string" &&
|
|
93
|
+
icon.uri.startsWith(ONEKEY_ICON_URI_PREFIX)
|
|
94
|
+
) {
|
|
95
|
+
return {
|
|
96
|
+
uri: icon.uri,
|
|
97
|
+
width: typeof icon.width === "number" && icon.width > 0 ? icon.width : 24,
|
|
98
|
+
height:
|
|
99
|
+
typeof icon.height === "number" && icon.height > 0 ? icon.height : 24,
|
|
100
|
+
scale: typeof icon.scale === "number" && icon.scale > 0 ? icon.scale : 1,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const resolved = Image.resolveAssetSource(icon as ImageSourcePropType);
|
|
104
|
+
return {
|
|
105
|
+
uri: resolved?.uri ?? "",
|
|
106
|
+
width: resolved?.width ?? 0,
|
|
107
|
+
height: resolved?.height ?? 0,
|
|
108
|
+
scale: resolved?.scale ?? 1,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
39
111
|
|
|
40
112
|
interface Props<Route extends BaseRoute> {
|
|
41
113
|
/*
|
|
@@ -142,10 +214,13 @@ interface Props<Route extends BaseRoute> {
|
|
|
142
214
|
/**
|
|
143
215
|
* Get icon for the tab.
|
|
144
216
|
*/
|
|
145
|
-
getIcon?: (
|
|
217
|
+
getIcon?: ({
|
|
218
|
+
route,
|
|
219
|
+
focused,
|
|
220
|
+
}: {
|
|
146
221
|
route: Route;
|
|
147
222
|
focused: boolean;
|
|
148
|
-
}) =>
|
|
223
|
+
}) => BottomTabIconSource | undefined | null;
|
|
149
224
|
|
|
150
225
|
/**
|
|
151
226
|
* Get hidden for the tab.
|
|
@@ -240,7 +315,7 @@ interface Props<Route extends BaseRoute> {
|
|
|
240
315
|
* Icon of a round button shown above a vertical tab bar. The button only
|
|
241
316
|
* appears while the tab bar is vertical and visible. iOS only.
|
|
242
317
|
*/
|
|
243
|
-
sideAccessoryIcon?: ImageSourcePropType;
|
|
318
|
+
sideAccessoryIcon?: ImageSourcePropType | NamedIcon | string;
|
|
244
319
|
/**
|
|
245
320
|
* Accessibility label of the side accessory button.
|
|
246
321
|
*/
|
|
@@ -415,40 +490,14 @@ const BottomTabs = <Route extends BaseRoute>({
|
|
|
415
490
|
);
|
|
416
491
|
|
|
417
492
|
const resolvedIconAssets = React.useMemo(
|
|
418
|
-
() =>
|
|
419
|
-
icons.map((icon) => {
|
|
420
|
-
if (icon && !isAppleSymbol(icon)) {
|
|
421
|
-
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
422
|
-
const resolved = Image.resolveAssetSource(icon);
|
|
423
|
-
return {
|
|
424
|
-
uri: resolved?.uri ?? "",
|
|
425
|
-
width: resolved?.width ?? 0,
|
|
426
|
-
height: resolved?.height ?? 0,
|
|
427
|
-
scale: resolved?.scale ?? 1,
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
return { uri: "", width: 0, height: 0, scale: 1 };
|
|
431
|
-
}),
|
|
493
|
+
() => icons.map((icon) => resolveTabIconSource(icon)),
|
|
432
494
|
[icons]
|
|
433
495
|
);
|
|
434
496
|
|
|
435
497
|
// Focused (solid) assets passed to iOS as selectedImage. iOS only — Android
|
|
436
498
|
// ignores selectedIcons and keeps swapping via the focusedKey array above.
|
|
437
499
|
const resolvedSelectedIconAssets = React.useMemo(
|
|
438
|
-
() =>
|
|
439
|
-
focusedIcons.map((icon) => {
|
|
440
|
-
if (icon && !isAppleSymbol(icon)) {
|
|
441
|
-
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
442
|
-
const resolved = Image.resolveAssetSource(icon);
|
|
443
|
-
return {
|
|
444
|
-
uri: resolved?.uri ?? "",
|
|
445
|
-
width: resolved?.width ?? 0,
|
|
446
|
-
height: resolved?.height ?? 0,
|
|
447
|
-
scale: resolved?.scale ?? 1,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
return { uri: "", width: 0, height: 0, scale: 1 };
|
|
451
|
-
}),
|
|
500
|
+
() => focusedIcons.map((icon) => resolveTabIconSource(icon)),
|
|
452
501
|
[focusedIcons]
|
|
453
502
|
);
|
|
454
503
|
|
|
@@ -514,17 +563,9 @@ const BottomTabs = <Route extends BaseRoute>({
|
|
|
514
563
|
if (!isIOS || !sideAccessoryIcon) {
|
|
515
564
|
return undefined;
|
|
516
565
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
? {
|
|
521
|
-
uri: resolved.uri,
|
|
522
|
-
width: resolved.width ?? 0,
|
|
523
|
-
height: resolved.height ?? 0,
|
|
524
|
-
scale: resolved.scale ?? 1,
|
|
525
|
-
}
|
|
526
|
-
: undefined;
|
|
527
|
-
}, [sideAccessoryIcon]);
|
|
566
|
+
const resolved = resolveTabIconSource(sideAccessoryIcon);
|
|
567
|
+
return resolved.uri ? resolved : undefined;
|
|
568
|
+
}, [isIOS, sideAccessoryIcon]);
|
|
528
569
|
|
|
529
570
|
useLayoutEffect(() => {
|
|
530
571
|
if (renderCustomTabBar && customTabBarWrapperRef.current) {
|
package/src/index.tsx
CHANGED
|
@@ -15,4 +15,10 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
|
|
|
15
15
|
/**
|
|
16
16
|
* Types
|
|
17
17
|
*/
|
|
18
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
AppleIcon,
|
|
20
|
+
BottomTabIconSource,
|
|
21
|
+
NamedIcon,
|
|
22
|
+
TabBarPlacement,
|
|
23
|
+
TabRole,
|
|
24
|
+
} from './types';
|
package/src/types.ts
CHANGED
|
@@ -5,6 +5,14 @@ export type IconSource = string | ImageSourcePropType;
|
|
|
5
5
|
|
|
6
6
|
export type AppleIcon = { sfSymbol: SFSymbol };
|
|
7
7
|
|
|
8
|
+
export type NamedIcon = { iconName: string };
|
|
9
|
+
|
|
10
|
+
export type BottomTabIconSource =
|
|
11
|
+
| ImageSourcePropType
|
|
12
|
+
| AppleIcon
|
|
13
|
+
| NamedIcon
|
|
14
|
+
| string;
|
|
15
|
+
|
|
8
16
|
export type TabRole = 'search';
|
|
9
17
|
|
|
10
18
|
// 'leading' and 'trailing' mean a vertical tab bar on that edge.
|
|
@@ -17,8 +25,8 @@ export type BaseRoute = {
|
|
|
17
25
|
badgeBackgroundColor?: string;
|
|
18
26
|
badgeTextColor?: string;
|
|
19
27
|
lazy?: boolean;
|
|
20
|
-
focusedIcon?:
|
|
21
|
-
unfocusedIcon?:
|
|
28
|
+
focusedIcon?: BottomTabIconSource;
|
|
29
|
+
unfocusedIcon?: BottomTabIconSource;
|
|
22
30
|
activeTintColor?: string;
|
|
23
31
|
hidden?: boolean;
|
|
24
32
|
testID?: string;
|
package/ios/SVG/CoreSVG.h
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
#import <UIKit/UIKit.h>
|
|
2
|
-
typedef UIImage PlatformImage;
|
|
3
|
-
|
|
4
|
-
@interface CoreSVGWrapper : NSObject
|
|
5
|
-
|
|
6
|
-
+ (instancetype)shared;
|
|
7
|
-
|
|
8
|
-
- (PlatformImage *)imageFromSVGData:(NSData *)data;
|
|
9
|
-
|
|
10
|
-
+ (BOOL)isSVGData:(NSData *)data;
|
|
11
|
-
+ (BOOL)supportsVectorSVGImage;
|
|
12
|
-
|
|
13
|
-
@end
|
package/ios/SVG/CoreSVG.mm
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
#import "CoreSVG.h"
|
|
2
|
-
#import <dlfcn.h>
|
|
3
|
-
#import <objc/runtime.h>
|
|
4
|
-
|
|
5
|
-
#define kSVGTagEnd @"</svg>"
|
|
6
|
-
|
|
7
|
-
typedef struct CF_BRIDGED_TYPE(id) CGSVGDocument *CGSVGDocumentRef;
|
|
8
|
-
|
|
9
|
-
static CGSVGDocumentRef (*CoreSVGDocumentRetain)(CGSVGDocumentRef);
|
|
10
|
-
static void (*CoreSVGDocumentRelease)(CGSVGDocumentRef);
|
|
11
|
-
static CGSVGDocumentRef (*CoreSVGDocumentCreateFromData)(CFDataRef data, CFDictionaryRef options);
|
|
12
|
-
static void (*CoreSVGContextDrawSVGDocument)(CGContextRef context, CGSVGDocumentRef document);
|
|
13
|
-
static CGSize (*CoreSVGDocumentGetCanvasSize)(CGSVGDocumentRef document);
|
|
14
|
-
|
|
15
|
-
#if !TARGET_OS_OSX
|
|
16
|
-
static SEL CoreSVGImageWithDocumentSEL = NULL;
|
|
17
|
-
static SEL CoreSVGDocumentSEL = NULL;
|
|
18
|
-
#endif
|
|
19
|
-
#if TARGET_OS_OSX
|
|
20
|
-
static Class CoreSVGImageRepClass = NULL;
|
|
21
|
-
static Ivar CoreSVGImageRepDocumentIvar = NULL;
|
|
22
|
-
#endif
|
|
23
|
-
|
|
24
|
-
static inline NSString *Base64DecodedString(NSString *base64String) {
|
|
25
|
-
NSData *data = [[NSData alloc] initWithBase64EncodedString:base64String options:NSDataBase64DecodingIgnoreUnknownCharacters];
|
|
26
|
-
if (!data) {
|
|
27
|
-
return nil;
|
|
28
|
-
}
|
|
29
|
-
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@implementation CoreSVGWrapper
|
|
33
|
-
|
|
34
|
-
+ (instancetype)shared {
|
|
35
|
-
static dispatch_once_t onceToken;
|
|
36
|
-
static CoreSVGWrapper *wrapper;
|
|
37
|
-
dispatch_once(&onceToken, ^{
|
|
38
|
-
wrapper = [[CoreSVGWrapper alloc] init];
|
|
39
|
-
});
|
|
40
|
-
return wrapper;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
+ (void)initialize {
|
|
44
|
-
CoreSVGDocumentRetain = (CGSVGDocumentRef (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJldGFpbg==").UTF8String);
|
|
45
|
-
CoreSVGDocumentRelease = (void (*)(CGSVGDocumentRef))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudFJlbGVhc2U=").UTF8String);
|
|
46
|
-
CoreSVGDocumentCreateFromData = (CGSVGDocumentRef (*)(CFDataRef data, CFDictionaryRef options))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudENyZWF0ZUZyb21EYXRh").UTF8String);
|
|
47
|
-
CoreSVGContextDrawSVGDocument = (void (*)(CGContextRef context, CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dDb250ZXh0RHJhd1NWR0RvY3VtZW50").UTF8String);
|
|
48
|
-
CoreSVGDocumentGetCanvasSize = (CGSize (*)(CGSVGDocumentRef document))dlsym(RTLD_DEFAULT, Base64DecodedString(@"Q0dTVkdEb2N1bWVudEdldENhbnZhc1NpemU=").UTF8String);
|
|
49
|
-
|
|
50
|
-
#if !TARGET_OS_OSX
|
|
51
|
-
CoreSVGImageWithDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X2ltYWdlV2l0aENHU1ZHRG9jdW1lbnQ6"));
|
|
52
|
-
CoreSVGDocumentSEL = NSSelectorFromString(Base64DecodedString(@"X0NHU1ZHRG9jdW1lbnQ="));
|
|
53
|
-
#endif
|
|
54
|
-
#if TARGET_OS_OSX
|
|
55
|
-
CoreSVGImageRepClass = NSClassFromString(Base64DecodedString(@"X05TU1ZHSW1hZ2VSZXA="));
|
|
56
|
-
if (CoreSVGImageRepClass) {
|
|
57
|
-
CoreSVGImageRepDocumentIvar = class_getInstanceVariable(CoreSVGImageRepClass, Base64DecodedString(@"X2RvY3VtZW50").UTF8String);
|
|
58
|
-
}
|
|
59
|
-
#endif
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
- (PlatformImage *)imageFromSVGData:(NSData *)data {
|
|
63
|
-
if (!data) {
|
|
64
|
-
return nil;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (![self.class supportsVectorSVGImage]) {
|
|
68
|
-
return nil;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return [self createVectorSVGWithData:data];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
- (PlatformImage *)createVectorSVGWithData:(NSData *)data {
|
|
75
|
-
if (!data) return nil;
|
|
76
|
-
|
|
77
|
-
PlatformImage *image;
|
|
78
|
-
|
|
79
|
-
#if TARGET_OS_OSX
|
|
80
|
-
if (!CoreSVGImageRepClass) {
|
|
81
|
-
return nil;
|
|
82
|
-
}
|
|
83
|
-
Class imageRepClass = CoreSVGImageRepClass;
|
|
84
|
-
NSImageRep *imageRep = [[imageRepClass alloc] initWithData:data];
|
|
85
|
-
if (!imageRep) {
|
|
86
|
-
return nil;
|
|
87
|
-
}
|
|
88
|
-
image = [[NSImage alloc] initWithSize:imageRep.size];
|
|
89
|
-
[image addRepresentation:imageRep];
|
|
90
|
-
#else
|
|
91
|
-
if (!CoreSVGDocumentCreateFromData || !CoreSVGDocumentRelease) {
|
|
92
|
-
return nil;
|
|
93
|
-
}
|
|
94
|
-
CGSVGDocumentRef document = CoreSVGDocumentCreateFromData((__bridge CFDataRef)data, NULL);
|
|
95
|
-
|
|
96
|
-
if (!document) {
|
|
97
|
-
return nil;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
image = ((UIImage *(*)(id,SEL,CGSVGDocumentRef))[UIImage.class methodForSelector:CoreSVGImageWithDocumentSEL])(UIImage.class, CoreSVGImageWithDocumentSEL, document);
|
|
101
|
-
CoreSVGDocumentRelease(document);
|
|
102
|
-
#endif
|
|
103
|
-
|
|
104
|
-
#if TARGET_OS_OSX
|
|
105
|
-
// Test render to catch potential CoreSVG crashes on macOS
|
|
106
|
-
NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc]
|
|
107
|
-
initWithBitmapDataPlanes:NULL
|
|
108
|
-
pixelsWide:1
|
|
109
|
-
pixelsHigh:1
|
|
110
|
-
bitsPerSample:8
|
|
111
|
-
samplesPerPixel:4
|
|
112
|
-
hasAlpha:YES
|
|
113
|
-
isPlanar:NO
|
|
114
|
-
colorSpaceName:NSCalibratedRGBColorSpace
|
|
115
|
-
bytesPerRow:0
|
|
116
|
-
bitsPerPixel:0];
|
|
117
|
-
|
|
118
|
-
NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap];
|
|
119
|
-
[NSGraphicsContext saveGraphicsState];
|
|
120
|
-
[NSGraphicsContext setCurrentContext:context];
|
|
121
|
-
|
|
122
|
-
@try {
|
|
123
|
-
[image drawInRect:NSMakeRect(0, 0, 1, 1)];
|
|
124
|
-
} @catch (...) {
|
|
125
|
-
[NSGraphicsContext restoreGraphicsState];
|
|
126
|
-
return nil;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
[NSGraphicsContext restoreGraphicsState];
|
|
130
|
-
#else
|
|
131
|
-
// Test render to catch potential CoreSVG crashes
|
|
132
|
-
UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1.0);
|
|
133
|
-
@try {
|
|
134
|
-
[image drawInRect:CGRectMake(0, 0, 1, 1)];
|
|
135
|
-
} @catch (...) {
|
|
136
|
-
UIGraphicsEndImageContext();
|
|
137
|
-
return nil;
|
|
138
|
-
}
|
|
139
|
-
UIGraphicsEndImageContext();
|
|
140
|
-
#endif
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return image;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
+ (BOOL)isSVGData:(NSData *)data {
|
|
147
|
-
if (!data) {
|
|
148
|
-
return NO;
|
|
149
|
-
}
|
|
150
|
-
// Check end with SVG tag
|
|
151
|
-
return [data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range: NSMakeRange(data.length - MIN(100, data.length), MIN(100, data.length))].location != NSNotFound;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
+ (BOOL)supportsVectorSVGImage {
|
|
155
|
-
static dispatch_once_t onceToken;
|
|
156
|
-
static BOOL supports;
|
|
157
|
-
dispatch_once(&onceToken, ^{
|
|
158
|
-
#if TARGET_OS_OSX
|
|
159
|
-
// macOS 10.15+ supports SVG built-in rendering, use selector to check is more accurate
|
|
160
|
-
if (CoreSVGImageRepClass) {
|
|
161
|
-
supports = YES;
|
|
162
|
-
} else {
|
|
163
|
-
supports = NO;
|
|
164
|
-
}
|
|
165
|
-
#else
|
|
166
|
-
// iOS 13+ supports SVG built-in rendering, use selector to check is more accurate
|
|
167
|
-
if ([UIImage respondsToSelector:CoreSVGImageWithDocumentSEL]) {
|
|
168
|
-
supports = YES;
|
|
169
|
-
} else {
|
|
170
|
-
supports = NO;
|
|
171
|
-
}
|
|
172
|
-
#endif
|
|
173
|
-
});
|
|
174
|
-
return supports;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
@end
|