@onekeyfe/react-native-bottom-tabs 6.7.0-alpha.412 → 6.7.0-alpha.414
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 +7 -58
- package/docs/SPEC.md +0 -32
- package/ios/RCTBottomAccessoryContainerView.swift +0 -2
- package/ios/RCTBottomTabsContainerView.swift +12 -216
- package/ios/SVG/CoreSVG.h +13 -0
- package/ios/SVG/CoreSVG.mm +177 -0
- package/ios/SVG/SvgDecoder.mm +3 -7
- package/ios/TabBarFontSize.swift +0 -2
- package/lib/module/BottomTabs.js +49 -67
- package/lib/typescript/src/BottomTabs.d.ts +5 -11
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/types.d.ts +2 -6
- package/package.json +1 -2
- package/react-native-bottom-tabs.podspec +0 -2
- package/src/BottomTabs.tsx +47 -88
- package/src/index.tsx +1 -7
- package/src/types.ts +2 -10
- package/android/src/main/java/com/onekey/bottomtabs/SharedSvgDecoder.kt +0 -51
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("io.coil-kt.coil3:coil-svg:${COIL_VERSION}")
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (isNewArchitectureEnabled()) {
|
|
@@ -9,7 +9,6 @@ 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
|
|
13
12
|
import android.graphics.drawable.ColorDrawable
|
|
14
13
|
import android.graphics.drawable.Drawable
|
|
15
14
|
import android.graphics.drawable.GradientDrawable
|
|
@@ -40,6 +39,7 @@ import androidx.core.view.forEachIndexed
|
|
|
40
39
|
import coil3.ImageLoader
|
|
41
40
|
import coil3.asDrawable
|
|
42
41
|
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,12 +55,9 @@ 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
|
|
59
58
|
import kotlin.math.abs
|
|
60
59
|
import kotlin.math.roundToInt
|
|
61
60
|
|
|
62
|
-
private const val ONEKEY_ICON_URI_PREFIX = "onekey-icon://"
|
|
63
|
-
|
|
64
61
|
private fun getMaterialContext(context: Context): Context {
|
|
65
62
|
return ContextThemeWrapper(context, com.google.android.material.R.style.Theme_MaterialComponents_DayNight)
|
|
66
63
|
}
|
|
@@ -196,7 +193,6 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
196
193
|
var items: MutableList<TabInfo> = mutableListOf()
|
|
197
194
|
private val iconSources: MutableMap<Int, ImageSource> = mutableMapOf()
|
|
198
195
|
private val drawableCache: MutableMap<ImageSource, Drawable> = mutableMapOf()
|
|
199
|
-
private val iconRequestTokens: MutableMap<Int, Any> = mutableMapOf()
|
|
200
196
|
|
|
201
197
|
private var isLayoutEnqueued = false
|
|
202
198
|
private var selectedItem: String? = null
|
|
@@ -264,7 +260,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
264
260
|
|
|
265
261
|
private val imageLoader = ImageLoader.Builder(context)
|
|
266
262
|
.components {
|
|
267
|
-
add(
|
|
263
|
+
add(SvgDecoder.Factory())
|
|
268
264
|
}
|
|
269
265
|
.build()
|
|
270
266
|
|
|
@@ -470,7 +466,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
470
466
|
drawableCache.clear()
|
|
471
467
|
iconSources.forEach { (index, source) ->
|
|
472
468
|
bottomNavigation.menu.findItem(index)?.let { menuItem ->
|
|
473
|
-
getDrawable(
|
|
469
|
+
getDrawable(source) { menuItem.icon = it }
|
|
474
470
|
}
|
|
475
471
|
}
|
|
476
472
|
}
|
|
@@ -1257,7 +1253,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1257
1253
|
|
|
1258
1254
|
menuItem.isVisible = !item.hidden
|
|
1259
1255
|
if (iconSources.containsKey(index)) {
|
|
1260
|
-
getDrawable(
|
|
1256
|
+
getDrawable(iconSources[index]!!) {
|
|
1261
1257
|
menuItem.icon = it
|
|
1262
1258
|
}
|
|
1263
1259
|
}
|
|
@@ -1327,7 +1323,7 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1327
1323
|
|
|
1328
1324
|
// Update existing item if exists.
|
|
1329
1325
|
bottomNavigation.menu.findItem(idx)?.let { menuItem ->
|
|
1330
|
-
getDrawable(
|
|
1326
|
+
getDrawable(imageSource) {
|
|
1331
1327
|
menuItem.icon = it
|
|
1332
1328
|
}
|
|
1333
1329
|
}
|
|
@@ -1359,50 +1355,11 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1359
1355
|
}
|
|
1360
1356
|
|
|
1361
1357
|
@SuppressLint("CheckResult")
|
|
1362
|
-
private fun getDrawable(
|
|
1363
|
-
iconRequestTokens.remove(index)?.let { OneKeyNamedSVG.cancel(it) }
|
|
1358
|
+
private fun getDrawable(imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
|
|
1364
1359
|
drawableCache[imageSource]?.let {
|
|
1365
1360
|
onDrawableReady(it)
|
|
1366
1361
|
return
|
|
1367
1362
|
}
|
|
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
|
|
1406
1363
|
val iconSizePx = bottomNavigation.itemIconSize
|
|
1407
1364
|
val request = ImageRequest.Builder(context)
|
|
1408
1365
|
.data(imageSource.getUri(context))
|
|
@@ -1411,15 +1368,9 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1411
1368
|
.precision(Precision.EXACT)
|
|
1412
1369
|
.target { drawable ->
|
|
1413
1370
|
post {
|
|
1414
|
-
if (iconRequestTokens[index] !== token) {
|
|
1415
|
-
return@post
|
|
1416
|
-
}
|
|
1417
|
-
iconRequestTokens.remove(index)
|
|
1418
1371
|
val stateDrawable = drawable.asDrawable(context.resources)
|
|
1419
1372
|
drawableCache[imageSource] = stateDrawable
|
|
1420
|
-
|
|
1421
|
-
onDrawableReady(stateDrawable)
|
|
1422
|
-
}
|
|
1373
|
+
onDrawableReady(stateDrawable)
|
|
1423
1374
|
}
|
|
1424
1375
|
}
|
|
1425
1376
|
.listener(
|
|
@@ -1489,8 +1440,6 @@ class BottomTabsView(context: Context) : LinearLayout(context) {
|
|
|
1489
1440
|
}
|
|
1490
1441
|
|
|
1491
1442
|
fun onDropViewInstance() {
|
|
1492
|
-
iconRequestTokens.values.forEach { OneKeyNamedSVG.cancel(it) }
|
|
1493
|
-
iconRequestTokens.clear()
|
|
1494
1443
|
androidBottomBlurEnabled = false
|
|
1495
1444
|
updateBlurAppearance()
|
|
1496
1445
|
releaseBottomBlur()
|
package/docs/SPEC.md
CHANGED
|
@@ -1,37 +1,5 @@
|
|
|
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
|
-
|
|
35
3
|
## Native left drawer
|
|
36
4
|
|
|
37
5
|
Status: implemented in module source. Package TypeScript, iOS/Android Fabric
|
|
@@ -1,71 +1,6 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import UIKit
|
|
3
|
-
#if canImport(React)
|
|
4
3
|
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
|
-
}
|
|
69
4
|
|
|
70
5
|
// MARK: - Data types for props from JS
|
|
71
6
|
|
|
@@ -142,10 +77,6 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
142
77
|
private var loadedSideAccessoryIcon: UIImage?
|
|
143
78
|
// Increments per icon load; a completion applies only to the latest one.
|
|
144
79
|
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()
|
|
149
80
|
private var isSideAccessoryLayoutScheduled = false
|
|
150
81
|
private static let sideAccessorySize: CGFloat = 48
|
|
151
82
|
private static let sideAccessorySpacing: CGFloat = 12
|
|
@@ -257,22 +188,6 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
257
188
|
fatalError("init(coder:) has not been implemented")
|
|
258
189
|
}
|
|
259
190
|
|
|
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
|
-
|
|
276
191
|
override func didAddSubview(_ subview: UIView) {
|
|
277
192
|
super.didAddSubview(subview)
|
|
278
193
|
if let tbcView = tabBarController?.view, subview !== tbcView {
|
|
@@ -302,7 +217,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
302
217
|
/// Where the system draws the tab bar: "bottom", or "leading"/"trailing" for
|
|
303
218
|
/// the vertical bar some devices (iPhone Duo) place in a side strip.
|
|
304
219
|
private func currentTabBarPlacement() -> String {
|
|
305
|
-
#if compiler(>=6.4)
|
|
220
|
+
#if compiler(>=6.4)
|
|
306
221
|
if #available(iOS 27.1, *), let tbc = tabBarController {
|
|
307
222
|
switch tbc.traitCollection.verticalBarEdge {
|
|
308
223
|
case .leading: return "leading"
|
|
@@ -315,7 +230,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
315
230
|
}
|
|
316
231
|
|
|
317
232
|
private func registerVerticalBarTraitChanges(_ tbc: UITabBarController) {
|
|
318
|
-
#if compiler(>=6.4)
|
|
233
|
+
#if compiler(>=6.4)
|
|
319
234
|
if #available(iOS 27.1, *) {
|
|
320
235
|
tbc.registerForTraitChanges(
|
|
321
236
|
UITraitCollection.systemTraitsAffectingVerticalBarEdge
|
|
@@ -425,7 +340,6 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
425
340
|
private func loadSideAccessoryIcon() {
|
|
426
341
|
sideAccessoryIconRequest += 1
|
|
427
342
|
let request = sideAccessoryIconRequest
|
|
428
|
-
OneKeyNamedSVGLoader.cancel(owner: sideAccessoryRequestToken)
|
|
429
343
|
// Drop the previous icon so a slower or failed load never leaves it shown
|
|
430
344
|
// for the new source.
|
|
431
345
|
if loadedSideAccessoryIcon != nil {
|
|
@@ -433,37 +347,7 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
433
347
|
scheduleSideAccessoryLayout()
|
|
434
348
|
}
|
|
435
349
|
let source = sideAccessoryIcon.map { IconSource(dict: $0) }
|
|
436
|
-
guard let source,
|
|
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 }
|
|
350
|
+
guard let source, let url = URL(string: source.uri) else { return }
|
|
467
351
|
// The image loader arrives with the first state update; setImageLoader
|
|
468
352
|
// calls this again.
|
|
469
353
|
guard let imageLoader else { return }
|
|
@@ -871,69 +755,21 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
871
755
|
|
|
872
756
|
@objc func setImageLoader(_ loader: RCTImageLoaderProtocol) {
|
|
873
757
|
self.imageLoader = loader
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
}
|
|
758
|
+
loadIconsFromSources()
|
|
759
|
+
loadSelectedIconsFromSources()
|
|
760
|
+
loadSideAccessoryIcon()
|
|
887
761
|
}
|
|
888
762
|
|
|
889
763
|
private func loadIconsFromSources() {
|
|
890
|
-
guard let iconDicts = icons else { return }
|
|
764
|
+
guard let imageLoader, let iconDicts = icons else { return }
|
|
891
765
|
|
|
892
766
|
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
|
-
}
|
|
900
767
|
|
|
901
768
|
for (index, source) in iconSources.enumerated() {
|
|
902
|
-
if let previous = iconRequestTokens.removeValue(forKey: index) {
|
|
903
|
-
OneKeyNamedSVGLoader.cancel(owner: previous)
|
|
904
|
-
}
|
|
905
769
|
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
|
-
}
|
|
935
770
|
|
|
936
|
-
|
|
771
|
+
let url = URL(string: source.uri)
|
|
772
|
+
guard let url else { continue }
|
|
937
773
|
|
|
938
774
|
let request = URLRequest(url: url)
|
|
939
775
|
let size = CGSize(width: source.width, height: source.height)
|
|
@@ -951,7 +787,6 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
951
787
|
if error != nil { return }
|
|
952
788
|
guard let image, let self else { return }
|
|
953
789
|
DispatchQueue.main.async {
|
|
954
|
-
guard self.iconRequestTokens[index] === token else { return }
|
|
955
790
|
self.loadedIcons[index] = image.resizeImageTo(size: self.iconSize)
|
|
956
791
|
self.rebuildViewControllers()
|
|
957
792
|
}
|
|
@@ -960,53 +795,15 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
960
795
|
}
|
|
961
796
|
|
|
962
797
|
private func loadSelectedIconsFromSources() {
|
|
963
|
-
guard let iconDicts = selectedIcons else { return }
|
|
798
|
+
guard let imageLoader, let iconDicts = selectedIcons else { return }
|
|
964
799
|
|
|
965
800
|
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
|
-
}
|
|
973
801
|
|
|
974
802
|
for (index, source) in iconSources.enumerated() {
|
|
975
|
-
if let previous = selectedIconRequestTokens.removeValue(forKey: index) {
|
|
976
|
-
OneKeyNamedSVGLoader.cancel(owner: previous)
|
|
977
|
-
}
|
|
978
803
|
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
|
-
}
|
|
1008
804
|
|
|
1009
|
-
|
|
805
|
+
let url = URL(string: source.uri)
|
|
806
|
+
guard let url else { continue }
|
|
1010
807
|
|
|
1011
808
|
let request = URLRequest(url: url)
|
|
1012
809
|
let size = CGSize(width: source.width, height: source.height)
|
|
@@ -1024,7 +821,6 @@ class RCTBottomTabsContainerView: UIView {
|
|
|
1024
821
|
if error != nil { return }
|
|
1025
822
|
guard let image, let self else { return }
|
|
1026
823
|
DispatchQueue.main.async {
|
|
1027
|
-
guard self.selectedIconRequestTokens[index] === token else { return }
|
|
1028
824
|
self.loadedSelectedIcons[index] = image.resizeImageTo(size: self.iconSize)
|
|
1029
825
|
self.rebuildViewControllers()
|
|
1030
826
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
package/ios/SVG/SvgDecoder.mm
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#import "SvgDecoder.h"
|
|
2
|
-
#import
|
|
2
|
+
#import "CoreSVG.h"
|
|
3
3
|
|
|
4
4
|
@implementation SvgDecoder
|
|
5
5
|
|
|
@@ -7,11 +7,7 @@ RCT_EXPORT_MODULE()
|
|
|
7
7
|
|
|
8
8
|
- (BOOL)canDecodeImageData:(NSData *)imageData
|
|
9
9
|
{
|
|
10
|
-
|
|
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;
|
|
10
|
+
return [CoreSVGWrapper isSVGData:imageData];
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
- (RCTImageLoaderCancellationBlock)decodeImageData:(NSData *)imageData
|
|
@@ -20,7 +16,7 @@ RCT_EXPORT_MODULE()
|
|
|
20
16
|
resizeMode:(RCTResizeMode)resizeMode
|
|
21
17
|
completionHandler:(RCTImageLoaderCompletionBlock)completionHandler
|
|
22
18
|
{
|
|
23
|
-
UIImage *image = [
|
|
19
|
+
UIImage *image = [CoreSVGWrapper.shared imageFromSVGData:imageData];
|
|
24
20
|
|
|
25
21
|
if (image) {
|
|
26
22
|
completionHandler(nil, image);
|
package/ios/TabBarFontSize.swift
CHANGED
package/lib/module/BottomTabs.js
CHANGED
|
@@ -3,74 +3,16 @@
|
|
|
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
|
+
|
|
6
9
|
import NativeBottomTabs from "./BottomTabsNativeComponent";
|
|
7
10
|
import useLatestCallback from "use-latest-callback";
|
|
8
11
|
import DelayedFreeze from "./DelayedFreeze.js";
|
|
9
12
|
import NativeBottomTabsDrawer from "./BottomTabsDrawerNativeComponent";
|
|
10
13
|
import { BottomAccessoryView } from "./BottomAccessoryView.js";
|
|
11
14
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
12
|
-
const isAppleSymbol = icon =>
|
|
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
|
-
};
|
|
15
|
+
const isAppleSymbol = icon => icon?.sfSymbol;
|
|
74
16
|
const ANDROID_MAX_TABS = 100;
|
|
75
17
|
const BottomTabs = ({
|
|
76
18
|
navigationState,
|
|
@@ -227,11 +169,45 @@ const BottomTabs = ({
|
|
|
227
169
|
})
|
|
228
170
|
};
|
|
229
171
|
}), [trimmedRoutes, icons, getLabelText, getBadge, getBadgeBackgroundColor, getBadgeTextColor, getActiveTintColor, getHidden, getTestID, getRole, getPreventsDefault]);
|
|
230
|
-
const resolvedIconAssets = React.useMemo(() => icons.map(icon =>
|
|
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]);
|
|
231
190
|
|
|
232
191
|
// Focused (solid) assets passed to iOS as selectedImage. iOS only — Android
|
|
233
192
|
// ignores selectedIcons and keeps swapping via the focusedKey array above.
|
|
234
|
-
const resolvedSelectedIconAssets = React.useMemo(() => focusedIcons.map(icon =>
|
|
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]);
|
|
235
211
|
const jumpTo = useLatestCallback(key => {
|
|
236
212
|
const index = trimmedRoutes.findIndex(route => route.key === key);
|
|
237
213
|
if (index === -1) {
|
|
@@ -282,9 +258,15 @@ const BottomTabs = ({
|
|
|
282
258
|
if (!isIOS || !sideAccessoryIcon) {
|
|
283
259
|
return undefined;
|
|
284
260
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
261
|
+
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
262
|
+
const resolved = Image.resolveAssetSource(sideAccessoryIcon);
|
|
263
|
+
return resolved?.uri ? {
|
|
264
|
+
uri: resolved.uri,
|
|
265
|
+
width: resolved.width ?? 0,
|
|
266
|
+
height: resolved.height ?? 0,
|
|
267
|
+
scale: resolved.scale ?? 1
|
|
268
|
+
} : undefined;
|
|
269
|
+
}, [sideAccessoryIcon]);
|
|
288
270
|
useLayoutEffect(() => {
|
|
289
271
|
if (renderCustomTabBar && customTabBarWrapperRef.current) {
|
|
290
272
|
customTabBarWrapperRef.current.measure((_x, _y, _width, height) => {
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type ColorValue, type ImageSourcePropType, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ImageSource } from "react-native/Libraries/Image/ImageSource";
|
|
4
|
+
import type { AppleIcon, BaseRoute, NavigationState, TabBarPlacement, TabRole } from "./types";
|
|
4
5
|
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;
|
|
12
6
|
interface Props<Route extends BaseRoute> {
|
|
13
7
|
labeled?: boolean;
|
|
14
8
|
/** Optional physical-left menu; all motion and input are owned by native. */
|
|
@@ -126,10 +120,10 @@ interface Props<Route extends BaseRoute> {
|
|
|
126
120
|
/**
|
|
127
121
|
* Get icon for the tab.
|
|
128
122
|
*/
|
|
129
|
-
getIcon?: (
|
|
123
|
+
getIcon?: (props: {
|
|
130
124
|
route: Route;
|
|
131
125
|
focused: boolean;
|
|
132
|
-
}) =>
|
|
126
|
+
}) => ImageSource | AppleIcon | undefined | null;
|
|
133
127
|
/**
|
|
134
128
|
* Get hidden for the tab.
|
|
135
129
|
*/
|
|
@@ -226,7 +220,7 @@ interface Props<Route extends BaseRoute> {
|
|
|
226
220
|
* Icon of a round button shown above a vertical tab bar. The button only
|
|
227
221
|
* appears while the tab bar is vertical and visible. iOS only.
|
|
228
222
|
*/
|
|
229
|
-
sideAccessoryIcon?: ImageSourcePropType
|
|
223
|
+
sideAccessoryIcon?: ImageSourcePropType;
|
|
230
224
|
/**
|
|
231
225
|
* Accessibility label of the side accessory button.
|
|
232
226
|
*/
|
|
@@ -12,5 +12,5 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
|
|
|
12
12
|
/**
|
|
13
13
|
* Types
|
|
14
14
|
*/
|
|
15
|
-
export type { AppleIcon,
|
|
15
|
+
export type { AppleIcon, TabBarPlacement, TabRole } from './types';
|
|
16
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -4,10 +4,6 @@ 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;
|
|
11
7
|
export type TabRole = 'search';
|
|
12
8
|
export type TabBarPlacement = 'bottom' | 'leading' | 'trailing';
|
|
13
9
|
export type BaseRoute = {
|
|
@@ -17,8 +13,8 @@ export type BaseRoute = {
|
|
|
17
13
|
badgeBackgroundColor?: string;
|
|
18
14
|
badgeTextColor?: string;
|
|
19
15
|
lazy?: boolean;
|
|
20
|
-
focusedIcon?:
|
|
21
|
-
unfocusedIcon?:
|
|
16
|
+
focusedIcon?: ImageSourcePropType | AppleIcon;
|
|
17
|
+
unfocusedIcon?: ImageSourcePropType | AppleIcon;
|
|
22
18
|
activeTintColor?: string;
|
|
23
19
|
hidden?: boolean;
|
|
24
20
|
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.414",
|
|
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,7 +71,6 @@
|
|
|
71
71
|
"typescript": "^5.9.2"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"@onekeyfe/react-native-image": "6.7.0-alpha.412",
|
|
75
74
|
"react": "*",
|
|
76
75
|
"react-native": "*"
|
|
77
76
|
},
|
package/src/BottomTabs.tsx
CHANGED
|
@@ -13,6 +13,9 @@ 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";
|
|
16
19
|
import NativeBottomTabs, {
|
|
17
20
|
type BottomTabsItems,
|
|
18
21
|
} from "./BottomTabsNativeComponent";
|
|
@@ -20,8 +23,6 @@ import useLatestCallback from "use-latest-callback";
|
|
|
20
23
|
import type {
|
|
21
24
|
AppleIcon,
|
|
22
25
|
BaseRoute,
|
|
23
|
-
BottomTabIconSource,
|
|
24
|
-
NamedIcon,
|
|
25
26
|
NavigationState,
|
|
26
27
|
TabBarPlacement,
|
|
27
28
|
TabRole,
|
|
@@ -33,81 +34,8 @@ import {
|
|
|
33
34
|
type RNBottomTabsAccessoryViewProps,
|
|
34
35
|
} from "./BottomAccessoryView";
|
|
35
36
|
|
|
36
|
-
const isAppleSymbol = (
|
|
37
|
-
icon
|
|
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
|
-
};
|
|
37
|
+
const isAppleSymbol = (icon: any): icon is { sfSymbol: string } =>
|
|
38
|
+
icon?.sfSymbol;
|
|
111
39
|
|
|
112
40
|
interface Props<Route extends BaseRoute> {
|
|
113
41
|
/*
|
|
@@ -214,13 +142,10 @@ interface Props<Route extends BaseRoute> {
|
|
|
214
142
|
/**
|
|
215
143
|
* Get icon for the tab.
|
|
216
144
|
*/
|
|
217
|
-
getIcon?: ({
|
|
218
|
-
route,
|
|
219
|
-
focused,
|
|
220
|
-
}: {
|
|
145
|
+
getIcon?: (props: {
|
|
221
146
|
route: Route;
|
|
222
147
|
focused: boolean;
|
|
223
|
-
}) =>
|
|
148
|
+
}) => ImageSource | AppleIcon | undefined | null;
|
|
224
149
|
|
|
225
150
|
/**
|
|
226
151
|
* Get hidden for the tab.
|
|
@@ -315,7 +240,7 @@ interface Props<Route extends BaseRoute> {
|
|
|
315
240
|
* Icon of a round button shown above a vertical tab bar. The button only
|
|
316
241
|
* appears while the tab bar is vertical and visible. iOS only.
|
|
317
242
|
*/
|
|
318
|
-
sideAccessoryIcon?: ImageSourcePropType
|
|
243
|
+
sideAccessoryIcon?: ImageSourcePropType;
|
|
319
244
|
/**
|
|
320
245
|
* Accessibility label of the side accessory button.
|
|
321
246
|
*/
|
|
@@ -490,14 +415,40 @@ const BottomTabs = <Route extends BaseRoute>({
|
|
|
490
415
|
);
|
|
491
416
|
|
|
492
417
|
const resolvedIconAssets = React.useMemo(
|
|
493
|
-
() =>
|
|
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
|
+
}),
|
|
494
432
|
[icons]
|
|
495
433
|
);
|
|
496
434
|
|
|
497
435
|
// Focused (solid) assets passed to iOS as selectedImage. iOS only — Android
|
|
498
436
|
// ignores selectedIcons and keeps swapping via the focusedKey array above.
|
|
499
437
|
const resolvedSelectedIconAssets = React.useMemo(
|
|
500
|
-
() =>
|
|
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
|
+
}),
|
|
501
452
|
[focusedIcons]
|
|
502
453
|
);
|
|
503
454
|
|
|
@@ -563,9 +514,17 @@ const BottomTabs = <Route extends BaseRoute>({
|
|
|
563
514
|
if (!isIOS || !sideAccessoryIcon) {
|
|
564
515
|
return undefined;
|
|
565
516
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
517
|
+
// @ts-ignore - resolveAssetSource accepts ImageSourcePropType
|
|
518
|
+
const resolved = Image.resolveAssetSource(sideAccessoryIcon);
|
|
519
|
+
return resolved?.uri
|
|
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]);
|
|
569
528
|
|
|
570
529
|
useLayoutEffect(() => {
|
|
571
530
|
if (renderCustomTabBar && customTabBarWrapperRef.current) {
|
package/src/index.tsx
CHANGED
|
@@ -15,10 +15,4 @@ export { BottomTabBarHeightContext } from './utils/BottomTabBarHeightContext';
|
|
|
15
15
|
/**
|
|
16
16
|
* Types
|
|
17
17
|
*/
|
|
18
|
-
export type {
|
|
19
|
-
AppleIcon,
|
|
20
|
-
BottomTabIconSource,
|
|
21
|
-
NamedIcon,
|
|
22
|
-
TabBarPlacement,
|
|
23
|
-
TabRole,
|
|
24
|
-
} from './types';
|
|
18
|
+
export type { AppleIcon, TabBarPlacement, TabRole } from './types';
|
package/src/types.ts
CHANGED
|
@@ -5,14 +5,6 @@ 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
|
-
|
|
16
8
|
export type TabRole = 'search';
|
|
17
9
|
|
|
18
10
|
// 'leading' and 'trailing' mean a vertical tab bar on that edge.
|
|
@@ -25,8 +17,8 @@ export type BaseRoute = {
|
|
|
25
17
|
badgeBackgroundColor?: string;
|
|
26
18
|
badgeTextColor?: string;
|
|
27
19
|
lazy?: boolean;
|
|
28
|
-
focusedIcon?:
|
|
29
|
-
unfocusedIcon?:
|
|
20
|
+
focusedIcon?: ImageSourcePropType | AppleIcon;
|
|
21
|
+
unfocusedIcon?: ImageSourcePropType | AppleIcon;
|
|
30
22
|
activeTintColor?: string;
|
|
31
23
|
hidden?: boolean;
|
|
32
24
|
testID?: string;
|
|
@@ -1,51 +0,0 @@
|
|
|
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
|
-
}
|