@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.
@@ -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 project(":onekeyfe_react-native-image")
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(SharedSvgDecoder.Factory())
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(index, source) { menuItem.icon = it }
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(index, iconSources[index]!!) {
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(idx, imageSource) {
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(index: Int, imageSource: ImageSource, onDrawableReady: (Drawable?) -> Unit) {
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
- if (iconSources[index] == imageSource) {
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,8 +1,6 @@
1
1
  import Foundation
2
2
  import UIKit
3
- #if canImport(React)
4
3
  import React
5
- #endif
6
4
 
7
5
  extension Notification.Name {
8
6
  static let bottomAccessoryPlacementChanged = Notification.Name("bottomAccessoryPlacementChanged")
@@ -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) && !targetEnvironment(macCatalyst)
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) && !targetEnvironment(macCatalyst)
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, !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 }
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
- 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
- }
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
- guard let imageLoader, let url = URL(string: source.uri) else { continue }
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
- guard let imageLoader, let url = URL(string: source.uri) else { continue }
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
@@ -1,5 +1,5 @@
1
1
  #import "SvgDecoder.h"
2
- #import <OneKeyImage/OneKeyImageCoderBridge.h>
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
- 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;
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 = [OneKeyImageCoderBridge decodeSVGData:imageData pixelSize:CGSizeZero];
19
+ UIImage *image = [CoreSVGWrapper.shared imageFromSVGData:imageData];
24
20
 
25
21
  if (image) {
26
22
  completionHandler(nil, image);
@@ -1,6 +1,4 @@
1
- #if canImport(React)
2
1
  import React
3
- #endif
4
2
  import UIKit
5
3
 
6
4
  enum TabBarFontSize {
@@ -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 => 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
- };
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 => resolveTabIconSource(icon)), [icons]);
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 => resolveTabIconSource(icon)), [focusedIcons]);
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
- const resolved = resolveTabIconSource(sideAccessoryIcon);
286
- return resolved.uri ? resolved : undefined;
287
- }, [isIOS, sideAccessoryIcon]);
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 { BaseRoute, BottomTabIconSource, NamedIcon, NavigationState, TabBarPlacement, TabRole } from "./types";
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?: ({ route, focused, }: {
123
+ getIcon?: (props: {
130
124
  route: Route;
131
125
  focused: boolean;
132
- }) => BottomTabIconSource | undefined | null;
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 | NamedIcon | string;
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, BottomTabIconSource, NamedIcon, TabBarPlacement, TabRole, } from './types';
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?: BottomTabIconSource;
21
- unfocusedIcon?: BottomTabIconSource;
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.412",
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
  },
@@ -32,7 +32,5 @@ Pod::Spec.new do |s|
32
32
  'DEFINES_MODULE' => 'YES'
33
33
  }
34
34
 
35
- s.dependency "OneKeyImage", package["peerDependencies"]["@onekeyfe/react-native-image"]
36
-
37
35
  install_modules_dependencies(s)
38
36
  end
@@ -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: 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
- };
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
- }) => BottomTabIconSource | undefined | null;
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 | NamedIcon | string;
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
- () => icons.map((icon) => resolveTabIconSource(icon)),
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
- () => focusedIcons.map((icon) => resolveTabIconSource(icon)),
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
- const resolved = resolveTabIconSource(sideAccessoryIcon);
567
- return resolved.uri ? resolved : undefined;
568
- }, [isIOS, sideAccessoryIcon]);
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?: BottomTabIconSource;
29
- unfocusedIcon?: BottomTabIconSource;
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
- }