@capgo/capacitor-native-navigation 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/android/src/main/java/app/capgo/nativenavigation/NativeNavigationPlugin.java +511 -577
- package/dist/docs.json +112 -0
- package/dist/esm/definitions.d.ts +65 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +16 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +16 -3
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +16 -3
- package/dist/plugin.js.map +1 -1
- package/docs/demo-curved-tabbar.png +0 -0
- package/docs/native-screenshots/android-curved-tabbar.png +0 -0
- package/ios/Sources/NativeNavigationPlugin/NativeNavigationPlugin.swift +661 -41
- package/ios/Tests/NativeNavigationPluginTests/NativeNavigationTests.swift +8 -0
- package/package.json +4 -3
|
@@ -42,6 +42,8 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
42
42
|
private var tabContainer: UIView?
|
|
43
43
|
private var tabEffectView: UIVisualEffectView?
|
|
44
44
|
private var tabBar: UITabBar?
|
|
45
|
+
private var floatingTabBar: NativeNavigationFloatingTabBar?
|
|
46
|
+
private var tabbarStyle = NativeNavigationTabbarStyleConfig()
|
|
45
47
|
private var tabBarController: NativeNavigationTabController?
|
|
46
48
|
private var tabViewControllers: [UIViewController] = []
|
|
47
49
|
private weak var systemTabRootContainer: UIView?
|
|
@@ -51,10 +53,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
51
53
|
private var liftedWebViewOverlays: [NativeNavigationWeakView] = []
|
|
52
54
|
private var isWebViewHostedInSystemTabController = false
|
|
53
55
|
private var navbarHeight: CGFloat = 44
|
|
54
|
-
private var tabbarHeight: CGFloat =
|
|
55
|
-
private let floatingTabbarHorizontalMargin: CGFloat = 24
|
|
56
|
-
private let floatingTabbarMaxWidth: CGFloat = 430
|
|
57
|
-
private let floatingTabbarBottomGap: CGFloat = 10
|
|
56
|
+
private var tabbarHeight: CGFloat = NativeNavigationTabbarStyleConfig().totalHeight
|
|
58
57
|
private var navbarVisible = false
|
|
59
58
|
private var tabbarVisible = false
|
|
60
59
|
private var contentInsetMode = "css"
|
|
@@ -70,6 +69,10 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
70
69
|
private var activeTransitionDirection = "forward"
|
|
71
70
|
private var activeZoomSourceFrame: CGRect?
|
|
72
71
|
private var activeZoomCornerRadius: CGFloat = 0
|
|
72
|
+
private weak var activeTransitionContainer: UIView?
|
|
73
|
+
private var activeTransitionContainerBackgroundColor: UIColor?
|
|
74
|
+
private var activeTransitionContainerWasOpaque = false
|
|
75
|
+
private var activeTransitionContainerBackgroundCaptured = false
|
|
73
76
|
|
|
74
77
|
private var usesSystemLiquidGlass: Bool {
|
|
75
78
|
if #available(iOS 26.0, *) {
|
|
@@ -183,33 +186,56 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
183
186
|
return
|
|
184
187
|
}
|
|
185
188
|
|
|
186
|
-
|
|
189
|
+
self.tabbarStyle = self.makeTabbarStyle(from: call)
|
|
190
|
+
self.tabbarHeight = self.tabbarStyle.totalHeight
|
|
191
|
+
|
|
187
192
|
let tabs = call.getArray("tabs") as? [[String: Any]] ?? []
|
|
188
193
|
let selectedId = call.getString("selectedId")
|
|
189
194
|
let labels = call.getBool("labels", true)
|
|
190
195
|
let labelVisibilityMode = call.getString("labelVisibilityMode") ?? (labels ? "labeled" : "unlabeled")
|
|
191
196
|
let icons = call.getBool("icons", true)
|
|
192
197
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
if self.usesSystemLiquidGlass && self.tabbarStyle.shape != .curve {
|
|
199
|
+
let tabBar = self.ensureTabBar()
|
|
200
|
+
let (items, selectedIndex) = self.makeTabBarItems(
|
|
201
|
+
tabs,
|
|
202
|
+
selectedId: selectedId,
|
|
203
|
+
labelVisibilityMode: labelVisibilityMode,
|
|
204
|
+
icons: icons
|
|
205
|
+
)
|
|
206
|
+
self.floatingTabBar?.isHidden = true
|
|
207
|
+
self.tabContainer?.isHidden = true
|
|
201
208
|
self.applySystemTabBarItems(items, selectedIndex: selectedIndex, animated: call.getBool("animated", false))
|
|
209
|
+
self.applyTabBarAppearance(tabBar: tabBar, options: call)
|
|
210
|
+
self.showTabBarChrome(tabBar)
|
|
202
211
|
} else {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
212
|
+
self.restoreWebViewFromSystemTabController()
|
|
213
|
+
self.setSystemTabBarHidden(true)
|
|
214
|
+
self.tabBarController?.view.isHidden = true
|
|
215
|
+
let tabBar = self.ensureFloatingTabBar()
|
|
216
|
+
let (items, selectedIndex) = self.makeFloatingTabBarItems(
|
|
217
|
+
tabs,
|
|
218
|
+
selectedId: selectedId,
|
|
219
|
+
icons: icons
|
|
220
|
+
)
|
|
221
|
+
self.applyFloatingTabBarAppearance(tabBar: tabBar, options: call)
|
|
222
|
+
let resolvedSelectedIndex = selectedIndex ?? (items.indices.contains(tabBar.selectedIndex) ? tabBar.selectedIndex : 0)
|
|
223
|
+
tabBar.configure(
|
|
224
|
+
items: items,
|
|
225
|
+
selectedIndex: resolvedSelectedIndex,
|
|
226
|
+
labelVisibilityMode: labelVisibilityMode,
|
|
227
|
+
icons: icons,
|
|
228
|
+
style: self.tabbarStyle
|
|
229
|
+
)
|
|
230
|
+
tabBar.onSelect = { [weak self] index, item in
|
|
231
|
+
self?.notifyListeners("tabSelect", data: [
|
|
232
|
+
"id": item.id,
|
|
233
|
+
"index": index,
|
|
234
|
+
"title": item.title
|
|
235
|
+
])
|
|
208
236
|
}
|
|
237
|
+
self.showFloatingTabBarChrome(tabBar)
|
|
209
238
|
}
|
|
210
|
-
|
|
211
|
-
self.applyTabBarAppearance(tabBar: tabBar, options: call)
|
|
212
|
-
self.showTabBarChrome(tabBar)
|
|
213
239
|
self.layoutChrome()
|
|
214
240
|
self.updateInsetsAndNotify()
|
|
215
241
|
call.resolve(self.insetsResult())
|
|
@@ -232,9 +258,14 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
232
258
|
let cornerRadius = CGFloat(call.getDouble("cornerRadius") ?? 0)
|
|
233
259
|
|
|
234
260
|
self.transitionSnapshot?.removeFromSuperview()
|
|
261
|
+
self.restoreTransitionContainerBackground()
|
|
262
|
+
let transitionSurface = nativeNavigationFallbackBackground(for: webView).withAlphaComponent(1.0)
|
|
263
|
+
self.prepareTransitionContainerBackground(transitionContainer, surface: transitionSurface)
|
|
235
264
|
let snapshot = self.transitionSnapshotView(from: webView, sourceRect: zoomSourceRect)
|
|
236
265
|
snapshot.frame = zoomSourceFrame ?? webView.frame
|
|
237
266
|
snapshot.autoresizingMask = zoomSourceFrame == nil ? [.flexibleWidth, .flexibleHeight] : []
|
|
267
|
+
snapshot.backgroundColor = transitionSurface
|
|
268
|
+
snapshot.isOpaque = true
|
|
238
269
|
snapshot.layer.cornerRadius = cornerRadius
|
|
239
270
|
snapshot.clipsToBounds = cornerRadius > 0
|
|
240
271
|
transitionContainer.insertSubview(snapshot, aboveSubview: webView)
|
|
@@ -292,8 +323,9 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
292
323
|
private func finishStandardTransition(_ transition: NativeNavigationTransitionContext) {
|
|
293
324
|
let width = transition.webView.bounds.width
|
|
294
325
|
let transforms = standardTransitionTransforms(direction: transition.direction, width: width)
|
|
326
|
+
let usesStationaryCrossfade = nativeNavigationUsesStationaryTransitionCrossfade(direction: transition.direction)
|
|
295
327
|
transition.webView.transform = transforms.start
|
|
296
|
-
transition.webView.alpha =
|
|
328
|
+
transition.webView.alpha = usesStationaryCrossfade ? 1 : 0.01
|
|
297
329
|
|
|
298
330
|
UIView.animate(
|
|
299
331
|
withDuration: max(transition.duration, 0),
|
|
@@ -303,7 +335,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
303
335
|
transition.webView.transform = .identity
|
|
304
336
|
transition.webView.alpha = 1
|
|
305
337
|
transition.snapshot?.transform = transforms.snapshotEnd
|
|
306
|
-
transition.snapshot?.alpha =
|
|
338
|
+
transition.snapshot?.alpha = usesStationaryCrossfade ? 0 : 0.75
|
|
307
339
|
},
|
|
308
340
|
completion: { _ in
|
|
309
341
|
self.finishTransitionCleanup(transition)
|
|
@@ -407,6 +439,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
407
439
|
transition.webView.layer.cornerRadius = 0
|
|
408
440
|
transition.webView.clipsToBounds = false
|
|
409
441
|
transitionSnapshot = nil
|
|
442
|
+
restoreTransitionContainerBackground()
|
|
410
443
|
activeTransitionId = nil
|
|
411
444
|
activeZoomSourceFrame = nil
|
|
412
445
|
let event: [String: Any] = ["id": transition.id, "direction": transition.direction, "duration": transition.durationMs]
|
|
@@ -414,6 +447,29 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
414
447
|
transition.resolve(event)
|
|
415
448
|
}
|
|
416
449
|
|
|
450
|
+
private func prepareTransitionContainerBackground(_ container: UIView, surface: UIColor) {
|
|
451
|
+
activeTransitionContainer = container
|
|
452
|
+
activeTransitionContainerBackgroundColor = container.backgroundColor
|
|
453
|
+
activeTransitionContainerWasOpaque = container.isOpaque
|
|
454
|
+
activeTransitionContainerBackgroundCaptured = true
|
|
455
|
+
if nativeNavigationNeedsTransitionSurface(container.backgroundColor) {
|
|
456
|
+
container.backgroundColor = surface
|
|
457
|
+
container.isOpaque = true
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private func restoreTransitionContainerBackground() {
|
|
462
|
+
guard activeTransitionContainerBackgroundCaptured else {
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
activeTransitionContainer?.backgroundColor = activeTransitionContainerBackgroundColor
|
|
466
|
+
activeTransitionContainer?.isOpaque = activeTransitionContainerWasOpaque
|
|
467
|
+
activeTransitionContainer = nil
|
|
468
|
+
activeTransitionContainerBackgroundColor = nil
|
|
469
|
+
activeTransitionContainerWasOpaque = false
|
|
470
|
+
activeTransitionContainerBackgroundCaptured = false
|
|
471
|
+
}
|
|
472
|
+
|
|
417
473
|
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
418
474
|
call.resolve([
|
|
419
475
|
"version": implementation.getPluginVersion()
|
|
@@ -549,6 +605,40 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
549
605
|
return bar
|
|
550
606
|
}
|
|
551
607
|
|
|
608
|
+
private func ensureFloatingTabBar() -> NativeNavigationFloatingTabBar {
|
|
609
|
+
if let floatingTabBar = floatingTabBar {
|
|
610
|
+
return floatingTabBar
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
let container = NativeNavigationChromeContainer()
|
|
614
|
+
container.hitSlop = UIEdgeInsets(top: 32, left: 0, bottom: 24, right: 0)
|
|
615
|
+
container.isUserInteractionEnabled = true
|
|
616
|
+
container.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
|
|
617
|
+
container.backgroundColor = .clear
|
|
618
|
+
container.clipsToBounds = false
|
|
619
|
+
|
|
620
|
+
container.layer.shadowColor = UIColor.black.cgColor
|
|
621
|
+
container.layer.shadowOpacity = 0.14
|
|
622
|
+
container.layer.shadowRadius = 18
|
|
623
|
+
container.layer.shadowOffset = CGSize(width: 0, height: 10)
|
|
624
|
+
|
|
625
|
+
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemChromeMaterial))
|
|
626
|
+
effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
627
|
+
effectView.isUserInteractionEnabled = false
|
|
628
|
+
effectView.clipsToBounds = true
|
|
629
|
+
container.addSubview(effectView)
|
|
630
|
+
self.tabEffectView = effectView
|
|
631
|
+
|
|
632
|
+
let bar = NativeNavigationFloatingTabBar()
|
|
633
|
+
bar.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
|
|
634
|
+
bar.clipsToBounds = false
|
|
635
|
+
container.addSubview(bar)
|
|
636
|
+
bridge?.viewController?.view.addSubview(container)
|
|
637
|
+
self.tabContainer = container
|
|
638
|
+
self.floatingTabBar = bar
|
|
639
|
+
return bar
|
|
640
|
+
}
|
|
641
|
+
|
|
552
642
|
private func ensureSystemTabBar() -> UITabBar {
|
|
553
643
|
if let tabBarController = tabBarController {
|
|
554
644
|
hostWebViewInSelectedSystemTab()
|
|
@@ -676,20 +766,22 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
676
766
|
}
|
|
677
767
|
|
|
678
768
|
private func hideTabBarChrome() {
|
|
679
|
-
if usesSystemLiquidGlass {
|
|
769
|
+
if usesSystemLiquidGlass && tabbarStyle.shape != .curve {
|
|
680
770
|
setSystemTabBarHidden(true)
|
|
681
771
|
tabBarController?.view.isHidden = false
|
|
682
772
|
hostWebViewInSelectedSystemTab()
|
|
683
773
|
} else {
|
|
684
774
|
tabContainer?.isHidden = true
|
|
775
|
+
floatingTabBar?.isHidden = true
|
|
685
776
|
tabBar?.isHidden = true
|
|
686
777
|
}
|
|
687
778
|
}
|
|
688
779
|
|
|
689
780
|
private func showTabBarChrome(_ tabBar: UITabBar) {
|
|
690
|
-
tabContainer?.isHidden =
|
|
781
|
+
tabContainer?.isHidden = true
|
|
782
|
+
floatingTabBar?.isHidden = true
|
|
691
783
|
tabBarController?.view.isHidden = false
|
|
692
|
-
if usesSystemLiquidGlass {
|
|
784
|
+
if usesSystemLiquidGlass && tabbarStyle.shape != .curve {
|
|
693
785
|
setSystemTabBarHidden(false)
|
|
694
786
|
liftWebViewOverlaysAboveSystemTabs()
|
|
695
787
|
hostWebViewInSelectedSystemTab()
|
|
@@ -698,6 +790,13 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
698
790
|
}
|
|
699
791
|
}
|
|
700
792
|
|
|
793
|
+
private func showFloatingTabBarChrome(_ tabBar: NativeNavigationFloatingTabBar) {
|
|
794
|
+
restoreWebViewFromSystemTabController()
|
|
795
|
+
tabBarController?.view.isHidden = true
|
|
796
|
+
tabContainer?.isHidden = false
|
|
797
|
+
tabBar.isHidden = false
|
|
798
|
+
}
|
|
799
|
+
|
|
701
800
|
private func captureOriginalWebViewPlacementIfNeeded(_ webView: UIView) {
|
|
702
801
|
guard originalWebViewSuperview == nil, let superview = webView.superview else {
|
|
703
802
|
return
|
|
@@ -879,6 +978,33 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
879
978
|
return (items, selectedIndex)
|
|
880
979
|
}
|
|
881
980
|
|
|
981
|
+
private func makeFloatingTabBarItems(
|
|
982
|
+
_ tabs: [[String: Any]],
|
|
983
|
+
selectedId: String?,
|
|
984
|
+
icons: Bool
|
|
985
|
+
) -> ([NativeNavigationFloatingTabItem], Int?) {
|
|
986
|
+
var selectedIndex: Int?
|
|
987
|
+
let items = tabs.enumerated().map { index, tab -> NativeNavigationFloatingTabItem in
|
|
988
|
+
let id = tab["id"] as? String ?? "tab-\(index)"
|
|
989
|
+
let title = tab["title"] as? String ?? ""
|
|
990
|
+
let image = icons ? self.image(from: tab["icon"] as? [String: Any]) : nil
|
|
991
|
+
let selectedImage = icons ? self.image(from: tab["selectedIcon"] as? [String: Any]) : nil
|
|
992
|
+
if id == selectedId {
|
|
993
|
+
selectedIndex = index
|
|
994
|
+
}
|
|
995
|
+
return NativeNavigationFloatingTabItem(
|
|
996
|
+
id: id,
|
|
997
|
+
title: title,
|
|
998
|
+
accessibilityTitle: title.isEmpty ? id : title,
|
|
999
|
+
image: image,
|
|
1000
|
+
selectedImage: selectedImage,
|
|
1001
|
+
badge: tab["badge"].map { String(describing: $0) },
|
|
1002
|
+
enabled: tab["enabled"] as? Bool ?? true
|
|
1003
|
+
)
|
|
1004
|
+
}
|
|
1005
|
+
return (items, selectedIndex)
|
|
1006
|
+
}
|
|
1007
|
+
|
|
882
1008
|
private func tabTitle(
|
|
883
1009
|
_ title: String?,
|
|
884
1010
|
id: String,
|
|
@@ -1044,6 +1170,35 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1044
1170
|
return imageView
|
|
1045
1171
|
}
|
|
1046
1172
|
|
|
1173
|
+
private func makeTabbarStyle(from call: CAPPluginCall) -> NativeNavigationTabbarStyleConfig {
|
|
1174
|
+
let rawStyle = call.getObject("style") ?? [:]
|
|
1175
|
+
let requestedShape = (rawStyle["shape"] as? String)?.lowercased()
|
|
1176
|
+
let shape: NativeNavigationTabbarShape = requestedShape == "curve" ? .curve : .floating
|
|
1177
|
+
let isCurve = shape == .curve
|
|
1178
|
+
let centerDiameter = max(number(from: rawStyle["centerButtonDiameter"]) ?? 76, 44)
|
|
1179
|
+
let height = max(number(from: rawStyle["height"]) ?? (isCurve ? 76 : 64), 44)
|
|
1180
|
+
let centerLift = max(number(from: rawStyle["centerButtonLift"]) ?? (centerDiameter / 2), 0)
|
|
1181
|
+
let bottomGap = max(number(from: rawStyle["bottomGap"]) ?? (isCurve ? 0 : 10), 0)
|
|
1182
|
+
let horizontalMargin = max(number(from: rawStyle["horizontalMargin"]) ?? (isCurve ? 0 : 24), 0)
|
|
1183
|
+
let maxWidth = max(number(from: rawStyle["maxWidth"]) ?? (isCurve ? 0 : 430), 0)
|
|
1184
|
+
let cornerRadius = max(number(from: rawStyle["cornerRadius"]) ?? (isCurve ? 24 : height / 2), 0)
|
|
1185
|
+
let centerButtonColor = (rawStyle["centerButtonColor"] as? String).flatMap { UIColor(hexString: $0) }
|
|
1186
|
+
let centerButtonIconColor = (rawStyle["centerButtonIconColor"] as? String).flatMap { UIColor(hexString: $0) } ?? .white
|
|
1187
|
+
|
|
1188
|
+
return NativeNavigationTabbarStyleConfig(
|
|
1189
|
+
shape: shape,
|
|
1190
|
+
height: height,
|
|
1191
|
+
horizontalMargin: horizontalMargin,
|
|
1192
|
+
maxWidth: maxWidth,
|
|
1193
|
+
bottomGap: bottomGap,
|
|
1194
|
+
cornerRadius: cornerRadius,
|
|
1195
|
+
centerItemId: rawStyle["centerItemId"] as? String,
|
|
1196
|
+
centerButtonDiameter: centerDiameter,
|
|
1197
|
+
centerButtonLift: centerLift,
|
|
1198
|
+
centerButtonColor: centerButtonColor,
|
|
1199
|
+
centerButtonIconColor: centerButtonIconColor
|
|
1200
|
+
)
|
|
1201
|
+
}
|
|
1047
1202
|
private func applyNavBarAppearance(navBar: UINavigationBar, options call: CAPPluginCall) {
|
|
1048
1203
|
let appearance = UINavigationBarAppearance()
|
|
1049
1204
|
let transparent = call.getBool("transparent", false)
|
|
@@ -1190,6 +1345,37 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1190
1345
|
}
|
|
1191
1346
|
}
|
|
1192
1347
|
|
|
1348
|
+
private func applyFloatingTabBarAppearance(tabBar: NativeNavigationFloatingTabBar, options call: CAPPluginCall) {
|
|
1349
|
+
let colors = call.getObject("colors")
|
|
1350
|
+
let backgroundTint = colorValue(colors?["background"])
|
|
1351
|
+
let backgroundColor = (backgroundTint ?? .systemBackground).withAlphaComponent(tabbarStyle.shape == .curve ? 0.96 : 0.46)
|
|
1352
|
+
|
|
1353
|
+
if tabbarStyle.shape == .curve {
|
|
1354
|
+
tabEffectView?.isHidden = true
|
|
1355
|
+
} else if let effect = blurEffect(from: call.getString("blurEffect"), fallback: .systemChromeMaterial) {
|
|
1356
|
+
tabEffectView?.effect = effect
|
|
1357
|
+
tabEffectView?.isHidden = false
|
|
1358
|
+
tabEffectView?.contentView.backgroundColor = backgroundTint?.withAlphaComponent(0.12) ?? backgroundColor
|
|
1359
|
+
} else {
|
|
1360
|
+
tabEffectView?.isHidden = true
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
tabBar.backgroundFillColor = backgroundColor
|
|
1364
|
+
if let color = colorValue(colors?["tint"]) {
|
|
1365
|
+
tabBar.selectedTintColor = color
|
|
1366
|
+
}
|
|
1367
|
+
if let color = colorValue(colors?["inactiveTint"]) {
|
|
1368
|
+
tabBar.inactiveTintColor = color
|
|
1369
|
+
}
|
|
1370
|
+
if let color = colorValue(call.getString("badgeBackgroundColor")) ?? colorValue(colors?["badgeBackground"]) {
|
|
1371
|
+
tabBar.badgeBackgroundColor = color
|
|
1372
|
+
}
|
|
1373
|
+
if let color = colorValue(call.getString("badgeTextColor")) ?? colorValue(colors?["badgeText"]) {
|
|
1374
|
+
tabBar.badgeTextColor = color
|
|
1375
|
+
}
|
|
1376
|
+
tabBar.setNeedsLayout()
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1193
1379
|
private func applyTabBarBadgeOptions(_ appearance: UITabBarAppearance, options call: CAPPluginCall) {
|
|
1194
1380
|
if let color = colorValue(call.getString("badgeBackgroundColor")) {
|
|
1195
1381
|
applyTabItemAppearances(appearance) { itemAppearance in
|
|
@@ -1230,17 +1416,18 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1230
1416
|
}
|
|
1231
1417
|
|
|
1232
1418
|
if let container = tabContainer {
|
|
1233
|
-
let availableWidth = max(0, width - (
|
|
1234
|
-
let
|
|
1419
|
+
let availableWidth = max(0, width - (tabbarStyle.horizontalMargin * 2))
|
|
1420
|
+
let maxWidth = tabbarStyle.maxWidth > 0 ? tabbarStyle.maxWidth : availableWidth
|
|
1421
|
+
let tabbarWidth = min(availableWidth, maxWidth)
|
|
1235
1422
|
let originX = (width - tabbarWidth) / 2
|
|
1236
|
-
let originY = height - safeInsets.bottom -
|
|
1237
|
-
container.frame = CGRect(x: originX, y: originY, width: tabbarWidth, height:
|
|
1238
|
-
container.layer.cornerRadius =
|
|
1239
|
-
container.layer.shadowPath =
|
|
1423
|
+
let originY = height - safeInsets.bottom - tabbarStyle.bottomGap - tabbarStyle.totalHeight
|
|
1424
|
+
container.frame = CGRect(x: originX, y: originY, width: tabbarWidth, height: tabbarStyle.totalHeight)
|
|
1425
|
+
container.layer.cornerRadius = tabbarStyle.shape == .floating ? tabbarStyle.cornerRadius : 0
|
|
1426
|
+
container.layer.shadowPath = NativeNavigationTabbarBackgroundPath.path(in: container.bounds, style: tabbarStyle).cgPath
|
|
1240
1427
|
tabEffectView?.frame = container.bounds
|
|
1241
|
-
tabEffectView?.layer.cornerRadius =
|
|
1242
|
-
|
|
1243
|
-
|
|
1428
|
+
tabEffectView?.layer.cornerRadius = tabbarStyle.cornerRadius
|
|
1429
|
+
floatingTabBar?.frame = container.bounds
|
|
1430
|
+
floatingTabBar?.layer.cornerRadius = 0
|
|
1244
1431
|
}
|
|
1245
1432
|
|
|
1246
1433
|
if let tabBarController = tabBarController {
|
|
@@ -1253,7 +1440,7 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1253
1440
|
}
|
|
1254
1441
|
|
|
1255
1442
|
private func bringChromeToFront() {
|
|
1256
|
-
if usesSystemLiquidGlass {
|
|
1443
|
+
if usesSystemLiquidGlass && tabbarStyle.shape != .curve {
|
|
1257
1444
|
if let navContainer = navContainer {
|
|
1258
1445
|
bridge?.viewController?.view.bringSubviewToFront(navContainer)
|
|
1259
1446
|
}
|
|
@@ -1352,10 +1539,10 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
|
|
|
1352
1539
|
private func currentInsets() -> [String: Any] {
|
|
1353
1540
|
let safeInsets = bridge?.viewController?.view.safeAreaInsets ?? .zero
|
|
1354
1541
|
let navHeight = isEnabled && navbarVisible ? navbarHeight + safeInsets.top : 0
|
|
1542
|
+
let usesSystemTabbar = usesSystemLiquidGlass && tabbarStyle.shape != .curve
|
|
1355
1543
|
let nativeTabHeight = max(tabBar?.frame.height ?? 0, 49 + safeInsets.bottom)
|
|
1356
|
-
let
|
|
1357
|
-
|
|
1358
|
-
: 0
|
|
1544
|
+
let customTabHeight = tabbarHeight + safeInsets.bottom + tabbarStyle.bottomGap
|
|
1545
|
+
let tabHeight = isEnabled && tabbarVisible ? (usesSystemTabbar ? nativeTabHeight : customTabHeight) : 0
|
|
1359
1546
|
return [
|
|
1360
1547
|
"top": navHeight,
|
|
1361
1548
|
"right": safeInsets.right,
|
|
@@ -1455,6 +1642,428 @@ private struct SVGRenderStyle {
|
|
|
1455
1642
|
}
|
|
1456
1643
|
}
|
|
1457
1644
|
|
|
1645
|
+
private struct NativeNavigationFloatingTabItem {
|
|
1646
|
+
let id: String
|
|
1647
|
+
let title: String
|
|
1648
|
+
let accessibilityTitle: String
|
|
1649
|
+
let image: UIImage?
|
|
1650
|
+
let selectedImage: UIImage?
|
|
1651
|
+
let badge: String?
|
|
1652
|
+
let enabled: Bool
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
private enum NativeNavigationTabbarShape {
|
|
1656
|
+
case floating
|
|
1657
|
+
case curve
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
private struct NativeNavigationTabbarStyleConfig {
|
|
1661
|
+
var shape: NativeNavigationTabbarShape = .floating
|
|
1662
|
+
var height: CGFloat = 64
|
|
1663
|
+
var horizontalMargin: CGFloat = 24
|
|
1664
|
+
var maxWidth: CGFloat = 430
|
|
1665
|
+
var bottomGap: CGFloat = 10
|
|
1666
|
+
var cornerRadius: CGFloat = 32
|
|
1667
|
+
var centerItemId: String?
|
|
1668
|
+
var centerButtonDiameter: CGFloat = 76
|
|
1669
|
+
var centerButtonLift: CGFloat = 38
|
|
1670
|
+
var centerButtonColor: UIColor?
|
|
1671
|
+
var centerButtonIconColor: UIColor = .white
|
|
1672
|
+
|
|
1673
|
+
var barTop: CGFloat {
|
|
1674
|
+
shape == .curve ? centerButtonLift : 0
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
var totalHeight: CGFloat {
|
|
1678
|
+
height + barTop
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
private enum NativeNavigationTabbarBackgroundPath {
|
|
1683
|
+
static func path(in bounds: CGRect, style: NativeNavigationTabbarStyleConfig) -> UIBezierPath {
|
|
1684
|
+
guard style.shape == .curve else {
|
|
1685
|
+
return UIBezierPath(roundedRect: bounds, cornerRadius: style.cornerRadius)
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
let barRect = CGRect(x: 0, y: style.barTop, width: bounds.width, height: max(style.height, 1))
|
|
1689
|
+
let cornerRadius = min(style.cornerRadius, barRect.height / 2)
|
|
1690
|
+
let centerX = bounds.midX
|
|
1691
|
+
let notchRadius = (style.centerButtonDiameter / 2) + 7
|
|
1692
|
+
let notchDepth = min(barRect.height * 0.78, notchRadius)
|
|
1693
|
+
let leftShoulder = max(barRect.minX + cornerRadius, centerX - notchRadius)
|
|
1694
|
+
let rightShoulder = min(barRect.maxX - cornerRadius, centerX + notchRadius)
|
|
1695
|
+
let control = notchDepth * 0.55228475
|
|
1696
|
+
let path = UIBezierPath()
|
|
1697
|
+
|
|
1698
|
+
path.move(to: CGPoint(x: barRect.minX + cornerRadius, y: barRect.minY))
|
|
1699
|
+
path.addLine(to: CGPoint(x: leftShoulder, y: barRect.minY))
|
|
1700
|
+
path.addCurve(
|
|
1701
|
+
to: CGPoint(x: centerX, y: barRect.minY + notchDepth),
|
|
1702
|
+
controlPoint1: CGPoint(x: leftShoulder, y: barRect.minY + control),
|
|
1703
|
+
controlPoint2: CGPoint(x: centerX - control, y: barRect.minY + notchDepth)
|
|
1704
|
+
)
|
|
1705
|
+
path.addCurve(
|
|
1706
|
+
to: CGPoint(x: rightShoulder, y: barRect.minY),
|
|
1707
|
+
controlPoint1: CGPoint(x: centerX + control, y: barRect.minY + notchDepth),
|
|
1708
|
+
controlPoint2: CGPoint(x: rightShoulder, y: barRect.minY + control)
|
|
1709
|
+
)
|
|
1710
|
+
path.addLine(to: CGPoint(x: barRect.maxX - cornerRadius, y: barRect.minY))
|
|
1711
|
+
path.addQuadCurve(to: CGPoint(x: barRect.maxX, y: barRect.minY + cornerRadius), controlPoint: CGPoint(x: barRect.maxX, y: barRect.minY))
|
|
1712
|
+
path.addLine(to: CGPoint(x: barRect.maxX, y: barRect.maxY - cornerRadius))
|
|
1713
|
+
path.addQuadCurve(to: CGPoint(x: barRect.maxX - cornerRadius, y: barRect.maxY), controlPoint: CGPoint(x: barRect.maxX, y: barRect.maxY))
|
|
1714
|
+
path.addLine(to: CGPoint(x: barRect.minX + cornerRadius, y: barRect.maxY))
|
|
1715
|
+
path.addQuadCurve(to: CGPoint(x: barRect.minX, y: barRect.maxY - cornerRadius), controlPoint: CGPoint(x: barRect.minX, y: barRect.maxY))
|
|
1716
|
+
path.addLine(to: CGPoint(x: barRect.minX, y: barRect.minY + cornerRadius))
|
|
1717
|
+
path.addQuadCurve(to: CGPoint(x: barRect.minX + cornerRadius, y: barRect.minY), controlPoint: CGPoint(x: barRect.minX, y: barRect.minY))
|
|
1718
|
+
path.close()
|
|
1719
|
+
return path
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
private struct NativeNavigationFloatingTabStyle {
|
|
1724
|
+
let selected: Bool
|
|
1725
|
+
let labels: Bool
|
|
1726
|
+
let icons: Bool
|
|
1727
|
+
let isCenter: Bool
|
|
1728
|
+
let selectedTint: UIColor
|
|
1729
|
+
let inactiveTint: UIColor
|
|
1730
|
+
let centerButtonColor: UIColor
|
|
1731
|
+
let centerButtonIconColor: UIColor
|
|
1732
|
+
let badgeBackgroundColor: UIColor
|
|
1733
|
+
let badgeTextColor: UIColor
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
private final class NativeNavigationFloatingTabBar: UIView {
|
|
1737
|
+
private var items: [NativeNavigationFloatingTabItem] = []
|
|
1738
|
+
private var buttons: [NativeNavigationFloatingTabButton] = []
|
|
1739
|
+
private var labelVisibilityMode = "labeled"
|
|
1740
|
+
private var iconsVisible = true
|
|
1741
|
+
private let backgroundShapeView = NativeNavigationTabbarBackgroundView()
|
|
1742
|
+
private var tabbarStyle = NativeNavigationTabbarStyleConfig()
|
|
1743
|
+
|
|
1744
|
+
var selectedIndex = 0
|
|
1745
|
+
var selectedTintColor = UIColor.systemBlue {
|
|
1746
|
+
didSet { updateButtons() }
|
|
1747
|
+
}
|
|
1748
|
+
var inactiveTintColor = UIColor.secondaryLabel {
|
|
1749
|
+
didSet { updateButtons() }
|
|
1750
|
+
}
|
|
1751
|
+
var badgeBackgroundColor = UIColor.systemRed {
|
|
1752
|
+
didSet { updateButtons() }
|
|
1753
|
+
}
|
|
1754
|
+
var badgeTextColor = UIColor.white {
|
|
1755
|
+
didSet { updateButtons() }
|
|
1756
|
+
}
|
|
1757
|
+
var backgroundFillColor = UIColor.systemBackground.withAlphaComponent(0.46) {
|
|
1758
|
+
didSet {
|
|
1759
|
+
backgroundShapeView.fillColor = backgroundFillColor
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
var onSelect: ((Int, NativeNavigationFloatingTabItem) -> Void)?
|
|
1763
|
+
|
|
1764
|
+
override init(frame: CGRect) {
|
|
1765
|
+
super.init(frame: frame)
|
|
1766
|
+
isOpaque = false
|
|
1767
|
+
backgroundShapeView.isUserInteractionEnabled = false
|
|
1768
|
+
backgroundShapeView.fillColor = backgroundFillColor
|
|
1769
|
+
addSubview(backgroundShapeView)
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
required init?(coder: NSCoder) {
|
|
1773
|
+
fatalError("init(coder:) has not been implemented")
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
func configure(
|
|
1777
|
+
items: [NativeNavigationFloatingTabItem],
|
|
1778
|
+
selectedIndex: Int,
|
|
1779
|
+
labelVisibilityMode: String,
|
|
1780
|
+
icons: Bool,
|
|
1781
|
+
style: NativeNavigationTabbarStyleConfig
|
|
1782
|
+
) {
|
|
1783
|
+
self.items = items
|
|
1784
|
+
self.labelVisibilityMode = labelVisibilityMode
|
|
1785
|
+
self.iconsVisible = icons
|
|
1786
|
+
self.tabbarStyle = style
|
|
1787
|
+
self.selectedIndex = items.indices.contains(selectedIndex) ? selectedIndex : 0
|
|
1788
|
+
backgroundShapeView.style = style
|
|
1789
|
+
rebuildButtons()
|
|
1790
|
+
setNeedsLayout()
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
override func layoutSubviews() {
|
|
1794
|
+
super.layoutSubviews()
|
|
1795
|
+
backgroundShapeView.frame = bounds
|
|
1796
|
+
guard !buttons.isEmpty else {
|
|
1797
|
+
return
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
if let centerIndex = centerButtonIndex(), buttons.indices.contains(centerIndex) {
|
|
1801
|
+
let buttonDiameter = tabbarStyle.centerButtonDiameter
|
|
1802
|
+
let centerGap = min(buttonDiameter + 22, bounds.width * 0.46)
|
|
1803
|
+
let barFrame = CGRect(x: 0, y: tabbarStyle.barTop, width: bounds.width, height: tabbarStyle.height)
|
|
1804
|
+
let leftWidth = max(0, bounds.midX - centerGap / 2)
|
|
1805
|
+
let rightX = min(bounds.width, bounds.midX + centerGap / 2)
|
|
1806
|
+
let centerFrame = CGRect(
|
|
1807
|
+
x: bounds.midX - buttonDiameter / 2,
|
|
1808
|
+
y: 0,
|
|
1809
|
+
width: buttonDiameter,
|
|
1810
|
+
height: buttonDiameter
|
|
1811
|
+
)
|
|
1812
|
+
|
|
1813
|
+
buttons[centerIndex].frame = centerFrame
|
|
1814
|
+
layoutButtons(Array(0..<centerIndex), in: CGRect(x: 0, y: barFrame.minY, width: leftWidth, height: barFrame.height))
|
|
1815
|
+
layoutButtons(
|
|
1816
|
+
Array((centerIndex + 1)..<buttons.count),
|
|
1817
|
+
in: CGRect(x: rightX, y: barFrame.minY, width: bounds.width - rightX, height: barFrame.height)
|
|
1818
|
+
)
|
|
1819
|
+
return
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
layoutButtons(Array(buttons.indices), in: bounds)
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
private func layoutButtons(_ indices: [Int], in rect: CGRect) {
|
|
1826
|
+
guard !indices.isEmpty else {
|
|
1827
|
+
return
|
|
1828
|
+
}
|
|
1829
|
+
let itemWidth = rect.width / CGFloat(indices.count)
|
|
1830
|
+
for (position, index) in indices.enumerated() where buttons.indices.contains(index) {
|
|
1831
|
+
buttons[index].frame = CGRect(
|
|
1832
|
+
x: rect.minX + CGFloat(position) * itemWidth,
|
|
1833
|
+
y: rect.minY,
|
|
1834
|
+
width: itemWidth,
|
|
1835
|
+
height: rect.height
|
|
1836
|
+
)
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
private func centerButtonIndex() -> Int? {
|
|
1841
|
+
guard tabbarStyle.shape == .curve, !items.isEmpty else {
|
|
1842
|
+
return nil
|
|
1843
|
+
}
|
|
1844
|
+
if let centerItemId = tabbarStyle.centerItemId,
|
|
1845
|
+
let index = items.firstIndex(where: { $0.id == centerItemId }) {
|
|
1846
|
+
return index
|
|
1847
|
+
}
|
|
1848
|
+
return items.count / 2
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
private func rebuildButtons() {
|
|
1852
|
+
buttons.forEach { $0.removeFromSuperview() }
|
|
1853
|
+
buttons = items.enumerated().map { index, item in
|
|
1854
|
+
let button = NativeNavigationFloatingTabButton()
|
|
1855
|
+
button.tag = index
|
|
1856
|
+
button.configure(
|
|
1857
|
+
item: item,
|
|
1858
|
+
style: style(for: index)
|
|
1859
|
+
)
|
|
1860
|
+
button.addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
|
|
1861
|
+
addSubview(button)
|
|
1862
|
+
return button
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
private func updateButtons() {
|
|
1867
|
+
for (index, button) in buttons.enumerated() {
|
|
1868
|
+
guard items.indices.contains(index) else {
|
|
1869
|
+
continue
|
|
1870
|
+
}
|
|
1871
|
+
button.configure(
|
|
1872
|
+
item: items[index],
|
|
1873
|
+
style: style(for: index)
|
|
1874
|
+
)
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
private func style(for index: Int) -> NativeNavigationFloatingTabStyle {
|
|
1879
|
+
let isCenter = centerButtonIndex() == index
|
|
1880
|
+
return NativeNavigationFloatingTabStyle(
|
|
1881
|
+
selected: index == selectedIndex,
|
|
1882
|
+
labels: showsLabel(for: index),
|
|
1883
|
+
icons: iconsVisible,
|
|
1884
|
+
isCenter: isCenter,
|
|
1885
|
+
selectedTint: selectedTintColor,
|
|
1886
|
+
inactiveTint: inactiveTintColor,
|
|
1887
|
+
centerButtonColor: tabbarStyle.centerButtonColor ?? selectedTintColor,
|
|
1888
|
+
centerButtonIconColor: tabbarStyle.centerButtonIconColor,
|
|
1889
|
+
badgeBackgroundColor: badgeBackgroundColor,
|
|
1890
|
+
badgeTextColor: badgeTextColor
|
|
1891
|
+
)
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
private func showsLabel(for index: Int) -> Bool {
|
|
1895
|
+
switch labelVisibilityMode {
|
|
1896
|
+
case "unlabeled":
|
|
1897
|
+
return false
|
|
1898
|
+
case "selected":
|
|
1899
|
+
return index == selectedIndex
|
|
1900
|
+
case "auto":
|
|
1901
|
+
let compact = traitCollection.horizontalSizeClass == .compact || UIDevice.current.userInterfaceIdiom == .phone
|
|
1902
|
+
return !compact || index == selectedIndex
|
|
1903
|
+
default:
|
|
1904
|
+
return true
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
@objc private func handleTap(_ sender: NativeNavigationFloatingTabButton) {
|
|
1909
|
+
let index = sender.tag
|
|
1910
|
+
guard items.indices.contains(index), items[index].enabled else {
|
|
1911
|
+
return
|
|
1912
|
+
}
|
|
1913
|
+
selectedIndex = index
|
|
1914
|
+
updateButtons()
|
|
1915
|
+
onSelect?(index, items[index])
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
private final class NativeNavigationTabbarBackgroundView: UIView {
|
|
1920
|
+
var style = NativeNavigationTabbarStyleConfig() {
|
|
1921
|
+
didSet { setNeedsDisplay() }
|
|
1922
|
+
}
|
|
1923
|
+
var fillColor = UIColor.systemBackground.withAlphaComponent(0.46) {
|
|
1924
|
+
didSet { setNeedsDisplay() }
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
override init(frame: CGRect) {
|
|
1928
|
+
super.init(frame: frame)
|
|
1929
|
+
isOpaque = false
|
|
1930
|
+
backgroundColor = .clear
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
required init?(coder: NSCoder) {
|
|
1934
|
+
fatalError("init(coder:) has not been implemented")
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
override func draw(_ rect: CGRect) {
|
|
1938
|
+
fillColor.setFill()
|
|
1939
|
+
NativeNavigationTabbarBackgroundPath.path(in: bounds, style: style).fill()
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
private final class NativeNavigationFloatingTabButton: UIControl {
|
|
1944
|
+
private let selectedView = UIView()
|
|
1945
|
+
private let imageView = UIImageView()
|
|
1946
|
+
private let titleLabel = UILabel()
|
|
1947
|
+
private let badgeLabel = UILabel()
|
|
1948
|
+
private var hasIcon = true
|
|
1949
|
+
private var hasLabel = true
|
|
1950
|
+
private var isCenterButton = false
|
|
1951
|
+
private var badgeText: String?
|
|
1952
|
+
|
|
1953
|
+
override init(frame: CGRect) {
|
|
1954
|
+
super.init(frame: frame)
|
|
1955
|
+
isAccessibilityElement = true
|
|
1956
|
+
|
|
1957
|
+
selectedView.isUserInteractionEnabled = false
|
|
1958
|
+
selectedView.alpha = 0
|
|
1959
|
+
addSubview(selectedView)
|
|
1960
|
+
|
|
1961
|
+
imageView.contentMode = .scaleAspectFit
|
|
1962
|
+
imageView.isUserInteractionEnabled = false
|
|
1963
|
+
addSubview(imageView)
|
|
1964
|
+
|
|
1965
|
+
titleLabel.textAlignment = .center
|
|
1966
|
+
titleLabel.font = .systemFont(ofSize: 11, weight: .semibold)
|
|
1967
|
+
titleLabel.adjustsFontSizeToFitWidth = true
|
|
1968
|
+
titleLabel.minimumScaleFactor = 0.78
|
|
1969
|
+
titleLabel.isUserInteractionEnabled = false
|
|
1970
|
+
addSubview(titleLabel)
|
|
1971
|
+
|
|
1972
|
+
badgeLabel.textAlignment = .center
|
|
1973
|
+
badgeLabel.font = .systemFont(ofSize: 11, weight: .bold)
|
|
1974
|
+
badgeLabel.textColor = .white
|
|
1975
|
+
badgeLabel.backgroundColor = .systemRed
|
|
1976
|
+
badgeLabel.layer.masksToBounds = true
|
|
1977
|
+
badgeLabel.isUserInteractionEnabled = false
|
|
1978
|
+
addSubview(badgeLabel)
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
required init?(coder: NSCoder) {
|
|
1982
|
+
fatalError("init(coder:) has not been implemented")
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
func configure(
|
|
1986
|
+
item: NativeNavigationFloatingTabItem,
|
|
1987
|
+
style: NativeNavigationFloatingTabStyle
|
|
1988
|
+
) {
|
|
1989
|
+
isEnabled = item.enabled
|
|
1990
|
+
alpha = item.enabled ? 1 : 0.38
|
|
1991
|
+
isCenterButton = style.isCenter
|
|
1992
|
+
hasIcon = style.icons && (item.image != nil || item.selectedImage != nil)
|
|
1993
|
+
hasLabel = style.isCenter ? (!hasIcon && !item.title.isEmpty) : (style.labels && !item.title.isEmpty)
|
|
1994
|
+
badgeText = item.badge
|
|
1995
|
+
|
|
1996
|
+
let color = style.isCenter ? style.centerButtonIconColor : (style.selected ? style.selectedTint : style.inactiveTint)
|
|
1997
|
+
selectedView.backgroundColor = style.isCenter ? style.centerButtonColor : style.selectedTint.withAlphaComponent(style.selected ? 0.16 : 0)
|
|
1998
|
+
selectedView.alpha = style.isCenter || style.selected ? 1 : 0
|
|
1999
|
+
|
|
2000
|
+
layer.shadowColor = UIColor.black.cgColor
|
|
2001
|
+
layer.shadowOpacity = style.isCenter ? 0.2 : 0
|
|
2002
|
+
layer.shadowRadius = style.isCenter ? 14 : 0
|
|
2003
|
+
layer.shadowOffset = CGSize(width: 0, height: 8)
|
|
2004
|
+
|
|
2005
|
+
let image = style.selected ? (item.selectedImage ?? item.image) : item.image
|
|
2006
|
+
imageView.image = image
|
|
2007
|
+
imageView.tintColor = color
|
|
2008
|
+
imageView.isHidden = !hasIcon
|
|
2009
|
+
|
|
2010
|
+
titleLabel.text = item.title
|
|
2011
|
+
titleLabel.textColor = color
|
|
2012
|
+
titleLabel.font = .systemFont(ofSize: style.isCenter ? 12 : 11, weight: style.selected ? .bold : .semibold)
|
|
2013
|
+
titleLabel.isHidden = !hasLabel
|
|
2014
|
+
|
|
2015
|
+
badgeLabel.text = item.badge
|
|
2016
|
+
badgeLabel.textColor = style.badgeTextColor
|
|
2017
|
+
badgeLabel.backgroundColor = style.badgeBackgroundColor
|
|
2018
|
+
badgeLabel.isHidden = item.badge == nil || item.badge == "0"
|
|
2019
|
+
accessibilityLabel = item.accessibilityTitle
|
|
2020
|
+
accessibilityTraits = style.selected ? [.button, .selected] : .button
|
|
2021
|
+
setNeedsLayout()
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
override func layoutSubviews() {
|
|
2025
|
+
super.layoutSubviews()
|
|
2026
|
+
if isCenterButton {
|
|
2027
|
+
selectedView.frame = bounds
|
|
2028
|
+
} else {
|
|
2029
|
+
let selectedSize: CGFloat = 58
|
|
2030
|
+
selectedView.frame = CGRect(x: (bounds.width - selectedSize) / 2, y: (bounds.height - selectedSize) / 2, width: selectedSize, height: selectedSize)
|
|
2031
|
+
}
|
|
2032
|
+
selectedView.layer.cornerRadius = selectedView.bounds.height / 2
|
|
2033
|
+
|
|
2034
|
+
let iconSize: CGFloat = isCenterButton ? 32 : 23
|
|
2035
|
+
if isCenterButton {
|
|
2036
|
+
if hasIcon {
|
|
2037
|
+
imageView.frame = CGRect(x: (bounds.width - iconSize) / 2, y: (bounds.height - iconSize) / 2, width: iconSize, height: iconSize)
|
|
2038
|
+
titleLabel.frame = .zero
|
|
2039
|
+
} else {
|
|
2040
|
+
imageView.frame = .zero
|
|
2041
|
+
titleLabel.frame = CGRect(x: 8, y: (bounds.height - 18) / 2, width: bounds.width - 16, height: 18)
|
|
2042
|
+
}
|
|
2043
|
+
} else if hasIcon && hasLabel {
|
|
2044
|
+
imageView.frame = CGRect(x: (bounds.width - iconSize) / 2, y: 10, width: iconSize, height: iconSize)
|
|
2045
|
+
titleLabel.frame = CGRect(x: 5, y: bounds.height - 23, width: bounds.width - 10, height: 15)
|
|
2046
|
+
} else if hasIcon {
|
|
2047
|
+
imageView.frame = CGRect(x: (bounds.width - iconSize) / 2, y: (bounds.height - iconSize) / 2, width: iconSize, height: iconSize)
|
|
2048
|
+
titleLabel.frame = .zero
|
|
2049
|
+
} else {
|
|
2050
|
+
imageView.frame = .zero
|
|
2051
|
+
titleLabel.frame = CGRect(x: 5, y: (bounds.height - 18) / 2, width: bounds.width - 10, height: 18)
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
let badgeHeight: CGFloat = 18
|
|
2055
|
+
let badgeWidth = max(badgeHeight, CGFloat((badgeText ?? "").count * 7 + 11))
|
|
2056
|
+
let anchor = hasIcon ? imageView.frame : CGRect(x: bounds.midX - 10, y: bounds.midY - 10, width: 20, height: 20)
|
|
2057
|
+
badgeLabel.frame = CGRect(
|
|
2058
|
+
x: min(bounds.width - badgeWidth - 8, anchor.midX + 7),
|
|
2059
|
+
y: max(6, anchor.minY - 6),
|
|
2060
|
+
width: badgeWidth,
|
|
2061
|
+
height: badgeHeight
|
|
2062
|
+
)
|
|
2063
|
+
badgeLabel.layer.cornerRadius = badgeHeight / 2
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1458
2067
|
private final class NativeNavigationChromeContainer: UIView {
|
|
1459
2068
|
var hitSlop = UIEdgeInsets.zero
|
|
1460
2069
|
|
|
@@ -1611,9 +2220,20 @@ final class NativeNavigationTabContentController: UIViewController {
|
|
|
1611
2220
|
private func nativeNavigationFallbackBackground(for view: UIView) -> UIColor {
|
|
1612
2221
|
if let color = view.backgroundColor,
|
|
1613
2222
|
color.cgColor.alpha > 0 {
|
|
1614
|
-
return color
|
|
2223
|
+
return color.withAlphaComponent(1)
|
|
2224
|
+
}
|
|
2225
|
+
return UIColor.systemBackground.withAlphaComponent(1.0)
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
func nativeNavigationUsesStationaryTransitionCrossfade(direction: String) -> Bool {
|
|
2229
|
+
direction == "tab" || direction == "root" || direction == "none"
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
private func nativeNavigationNeedsTransitionSurface(_ color: UIColor?) -> Bool {
|
|
2233
|
+
guard let color else {
|
|
2234
|
+
return true
|
|
1615
2235
|
}
|
|
1616
|
-
return .
|
|
2236
|
+
return color.cgColor.alpha < 1
|
|
1617
2237
|
}
|
|
1618
2238
|
|
|
1619
2239
|
private func nativeNavigationSnapshotPlaceholder(for view: UIView) -> UIView {
|