@capgo/capacitor-native-navigation 8.5.0 → 8.6.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  <a href="https://capgo.app/?ref=plugin_native_navigation"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-native-navigation" alt="Capgo - Instant updates for Capacitor" /></a>
4
4
 
5
5
  <div align="center">
6
- <p><b>Capgo</b>: open-source live updates for Ionic and Capacitor apps. Ship OTA fixes and features instantly, without waiting for app store review.</p>
6
+ <p><b>Capgo</b>: push fixes to your Capacitor users in minutes, build signed iOS and Android apps without a Mac, and roll back in one click.</p>
7
7
  <h2><a href="https://capgo.app/register/?ref=plugin_native_navigation">➡️ Get started for free</a></h2>
8
8
  <p>14-day unlimited free trial. No credit card required</p>
9
9
  <p><a href="https://capgo.app/consulting/?ref=plugin_native_navigation">Missing a feature? We'll build the plugin for you 💪</a></p>
@@ -36,6 +36,7 @@ import androidx.annotation.RequiresApi;
36
36
  import androidx.appcompat.content.res.AppCompatResources;
37
37
  import androidx.appcompat.widget.Toolbar;
38
38
  import androidx.core.graphics.PathParser;
39
+ import androidx.core.widget.TextViewCompat;
39
40
  import com.getcapacitor.JSArray;
40
41
  import com.getcapacitor.JSObject;
41
42
  import com.getcapacitor.Plugin;
@@ -61,6 +62,11 @@ public class NativeNavigationPlugin extends Plugin {
61
62
 
62
63
  private static final int DEFAULT_NAVBAR_DP = 56;
63
64
  private static final int DEFAULT_TABBAR_DP = 64;
65
+ private static final int TAB_LABEL_TEXT_SP = 12;
66
+ private static final int TAB_CONTENT_PADDING_DP = 4;
67
+ private static final int INDICATOR_HORIZONTAL_PADDING_DP = 12;
68
+ private static final int INDICATOR_VERTICAL_PADDING_DP = 6;
69
+ private static final int INDICATOR_SLOT_INSET_DP = 2;
64
70
  private static final int DEFAULT_TRANSITION_MS = 350;
65
71
  private static final int MENU_ITEM_BASE = 10_000;
66
72
  private static final int DEFAULT_TABBAR_BACKGROUND_COLOR = Color.WHITE;
@@ -130,7 +136,7 @@ public class NativeNavigationPlugin extends Plugin {
130
136
  configureSpecifiedGlassEffect = configureGlass != null && configureGlass.has("effect");
131
137
  defaultGlassOptions = GlassOptions.from(configureGlass, defaultGlassOptions);
132
138
  navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
133
- tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
139
+ tabbarGlassOptions = resolveTabbarGlassOptions(call);
134
140
  Double duration = call.getDouble("animationDuration");
135
141
  if (duration != null) {
136
142
  defaultTransitionMs = Math.max(0, duration.intValue());
@@ -838,7 +844,7 @@ public class NativeNavigationPlugin extends Plugin {
838
844
  }
839
845
 
840
846
  private FrameLayout makeTabButton(NativeTabItem item, boolean selected, boolean showLabel, boolean icons, boolean center) {
841
- FrameLayout button = new FrameLayout(getContext());
847
+ TabButton button = new TabButton(getContext());
842
848
  button.setClipChildren(false);
843
849
  button.setClipToPadding(false);
844
850
  button.setForeground(selectableItemBackground());
@@ -852,25 +858,33 @@ public class NativeNavigationPlugin extends Plugin {
852
858
  button.addView(centerFill, new FrameLayout.LayoutParams(centerFillDiameter, centerFillDiameter, Gravity.CENTER));
853
859
  }
854
860
 
861
+ Drawable currentIcon = selected && item.selectedIcon != null ? item.selectedIcon : item.icon;
862
+ boolean labelVisible = center ? showLabel && (currentIcon == null || !icons) : showLabel;
863
+ boolean iconVisible = icons && currentIcon != null;
864
+ // Measured with the bold selected style for every tab so the slots don't shift when the selection changes.
865
+ int contentWidth = indicatorContentWidth(item.title, labelVisible, iconVisible);
866
+ button.preferredWidth = contentWidth + dp(INDICATOR_HORIZONTAL_PADDING_DP + INDICATOR_SLOT_INSET_DP) * 2;
867
+
855
868
  if (!center && selected && !tabbarDisableIndicator) {
856
869
  GradientDrawable selectedBackground = new GradientDrawable();
857
- selectedBackground.setShape(GradientDrawable.OVAL);
870
+ selectedBackground.setShape(GradientDrawable.RECTANGLE);
858
871
  selectedBackground.setColor(TabbarChromeSupport.resolveSelectedIndicatorColor(tabbarIndicatorColor, tintColor));
859
- View selectedCircle = new View(getContext());
860
- selectedCircle.setBackground(selectedBackground);
861
- button.addView(selectedCircle, new FrameLayout.LayoutParams(dp(58), dp(58), Gravity.CENTER));
872
+ TabIndicatorView indicator = new TabIndicatorView(getContext(), contentWidth, indicatorContentHeight(labelVisible, iconVisible));
873
+ indicator.setBackground(selectedBackground);
874
+ button.addView(
875
+ indicator,
876
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)
877
+ );
862
878
  }
863
879
 
864
880
  LinearLayout content = new LinearLayout(getContext());
865
881
  content.setOrientation(LinearLayout.VERTICAL);
866
882
  content.setGravity(Gravity.CENTER);
867
- content.setPadding(dp(4), dp(4), dp(4), dp(4));
883
+ content.setPadding(dp(TAB_CONTENT_PADDING_DP), dp(TAB_CONTENT_PADDING_DP), dp(TAB_CONTENT_PADDING_DP), dp(TAB_CONTENT_PADDING_DP));
868
884
  button.addView(content, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
869
885
 
870
- Drawable currentIcon = selected && item.selectedIcon != null ? item.selectedIcon : item.icon;
871
- boolean labelVisible = center ? showLabel && (currentIcon == null || !icons) : showLabel;
872
886
  int itemColor = center ? tabbarStyle.centerButtonIconColor : (selected ? tintColor : inactiveTintColor);
873
- if (icons && currentIcon != null) {
887
+ if (iconVisible) {
874
888
  Drawable icon = currentIcon.mutate();
875
889
  icon.setTint(itemColor);
876
890
  ImageView image = new ImageView(getContext());
@@ -886,10 +900,13 @@ public class NativeNavigationPlugin extends Plugin {
886
900
  TextView label = new TextView(getContext());
887
901
  label.setText(item.title);
888
902
  label.setTextColor(itemColor);
889
- label.setTextSize(12);
903
+ label.setTextSize(TAB_LABEL_TEXT_SP);
890
904
  label.setTypeface(Typeface.DEFAULT, selected ? Typeface.BOLD : Typeface.NORMAL);
891
905
  label.setGravity(Gravity.CENTER);
892
- label.setSingleLine(true);
906
+ // maxLines rather than singleLine: autosize is ignored on a single-line (horizontally scrolling) TextView.
907
+ label.setMaxLines(1);
908
+ // Shrink long titles to fit the tab instead of cutting them off.
909
+ TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(label, 9, TAB_LABEL_TEXT_SP, 1, TypedValue.COMPLEX_UNIT_SP);
893
910
  content.addView(label, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(18)));
894
911
  }
895
912
 
@@ -919,6 +936,75 @@ public class NativeNavigationPlugin extends Plugin {
919
936
  return button;
920
937
  }
921
938
 
939
+ /** Width of the selected item's icon or label (whichever is wider), before the indicator padding. */
940
+ private int indicatorContentWidth(String title, boolean labelVisible, boolean iconVisible) {
941
+ int width = iconVisible ? dp(24) : 0;
942
+ if (labelVisible && title != null) {
943
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
944
+ paint.setTypeface(Typeface.DEFAULT_BOLD);
945
+ paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, TAB_LABEL_TEXT_SP, getContext().getResources().getDisplayMetrics()));
946
+ width = Math.max(width, (int) Math.ceil(paint.measureText(title)));
947
+ }
948
+ return width;
949
+ }
950
+
951
+ private int indicatorContentHeight(boolean labelVisible, boolean iconVisible) {
952
+ int height = 0;
953
+ if (iconVisible) {
954
+ height += dp(24);
955
+ }
956
+ if (labelVisible) {
957
+ height += dp(18) + (iconVisible ? dp(2) : 0);
958
+ }
959
+ return height;
960
+ }
961
+
962
+ /** Tab item that reports how wide it wants to be, so the tabbar can size slots to their content. */
963
+ private static final class TabButton extends FrameLayout {
964
+
965
+ int preferredWidth = 0;
966
+
967
+ TabButton(Context context) {
968
+ super(context);
969
+ }
970
+ }
971
+
972
+ /** Selected-tab capsule that wraps the item's icon and label with padding, kept inside its tab slot. */
973
+ private static final class TabIndicatorView extends View {
974
+
975
+ private final int contentWidth;
976
+ private final int contentHeight;
977
+
978
+ TabIndicatorView(Context context, int contentWidth, int contentHeight) {
979
+ super(context);
980
+ this.contentWidth = contentWidth;
981
+ this.contentHeight = contentHeight;
982
+ }
983
+
984
+ @Override
985
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
986
+ int slotWidth = MeasureSpec.getSize(widthMeasureSpec);
987
+ int slotHeight = MeasureSpec.getSize(heightMeasureSpec);
988
+ int height = contentHeight + dp(INDICATOR_VERTICAL_PADDING_DP) * 2;
989
+ if (slotHeight > 0) {
990
+ height = Math.min(height, slotHeight);
991
+ }
992
+ int width = Math.max(height, contentWidth + dp(INDICATOR_HORIZONTAL_PADDING_DP) * 2);
993
+ if (slotWidth > 0) {
994
+ width = Math.min(width, slotWidth - dp(INDICATOR_SLOT_INSET_DP) * 2);
995
+ }
996
+ setMeasuredDimension(width, height);
997
+ Drawable background = getBackground();
998
+ if (background instanceof GradientDrawable) {
999
+ ((GradientDrawable) background).setCornerRadius(height / 2f);
1000
+ }
1001
+ }
1002
+
1003
+ private int dp(int value) {
1004
+ return Math.round(value * getResources().getDisplayMetrics().density);
1005
+ }
1006
+ }
1007
+
922
1008
  private Drawable iconFrom(JSONObject descriptor) {
923
1009
  if (descriptor == null) {
924
1010
  return null;
@@ -1213,11 +1299,7 @@ public class NativeNavigationPlugin extends Plugin {
1213
1299
  return;
1214
1300
  }
1215
1301
 
1216
- int count = Math.max(getChildCount(), 1);
1217
- int childWidth = width / count;
1218
- for (int index = 0; index < getChildCount(); index++) {
1219
- measureChildExact(getChildAt(index), childWidth, height);
1220
- }
1302
+ measureRange(0, getChildCount(), width, height);
1221
1303
  setMeasuredDimension(width, height);
1222
1304
  }
1223
1305
 
@@ -1280,28 +1362,56 @@ public class NativeNavigationPlugin extends Plugin {
1280
1362
  }
1281
1363
 
1282
1364
  private void measureRange(int start, int end, int width, int height) {
1283
- int count = Math.max(0, end - start);
1284
- if (count == 0) {
1285
- return;
1286
- }
1287
- int childWidth = Math.max(0, width / count);
1365
+ int[] widths = slotWidths(start, end, width);
1288
1366
  for (int index = start; index < end; index++) {
1289
- measureChildExact(getChildAt(index), childWidth, height);
1367
+ measureChildExact(getChildAt(index), widths[index - start], height);
1290
1368
  }
1291
1369
  }
1292
1370
 
1293
1371
  private void layoutRange(int start, int end, int left, int top, int width, int height) {
1294
- int count = Math.max(0, end - start);
1295
- if (count == 0) {
1296
- return;
1297
- }
1298
- int childWidth = Math.max(0, width / count);
1372
+ int[] widths = slotWidths(start, end, width);
1373
+ int childLeft = left;
1299
1374
  for (int index = start; index < end; index++) {
1300
- int childLeft = left + (index - start) * childWidth;
1375
+ int childWidth = widths[index - start];
1301
1376
  getChildAt(index).layout(childLeft, top, childLeft + childWidth, top + height);
1377
+ childLeft += childWidth;
1302
1378
  }
1303
1379
  }
1304
1380
 
1381
+ /**
1382
+ * Each tab gets the width its content needs; leftover space is shared equally so the gaps stay even. When the
1383
+ * bar is too narrow, slots shrink in proportion and long labels autosize down.
1384
+ */
1385
+ private int[] slotWidths(int start, int end, int width) {
1386
+ int count = Math.max(0, end - start);
1387
+ int[] widths = new int[count];
1388
+ if (count == 0) {
1389
+ return widths;
1390
+ }
1391
+ int total = 0;
1392
+ for (int index = 0; index < count; index++) {
1393
+ View child = getChildAt(start + index);
1394
+ int preferred = child instanceof TabButton ? ((TabButton) child).preferredWidth : 0;
1395
+ widths[index] = Math.max(preferred, 0);
1396
+ total += widths[index];
1397
+ }
1398
+ if (total <= 0) {
1399
+ for (int index = 0; index < count; index++) {
1400
+ widths[index] = width / count;
1401
+ }
1402
+ } else if (total <= width) {
1403
+ int extra = (width - total) / count;
1404
+ for (int index = 0; index < count; index++) {
1405
+ widths[index] += extra;
1406
+ }
1407
+ } else {
1408
+ for (int index = 0; index < count; index++) {
1409
+ widths[index] = (int) ((long) widths[index] * width / total);
1410
+ }
1411
+ }
1412
+ return widths;
1413
+ }
1414
+
1305
1415
  private void measureChildExact(View child, int width, int height) {
1306
1416
  child.measure(
1307
1417
  MeasureSpec.makeMeasureSpec(Math.max(0, width), MeasureSpec.EXACTLY),
@@ -1645,11 +1755,14 @@ public class NativeNavigationPlugin extends Plugin {
1645
1755
  if (tabbarContainer != null) {
1646
1756
  tabbarContainer.setBackgroundColor(Color.TRANSPARENT);
1647
1757
  }
1648
- hideGlassBackground(null, tabbarGlassSurface);
1758
+ if (tabbarGlassSurface != null) {
1759
+ tabbarGlassSurface.setBackground(chromeBackgroundDrawable(drawColor, 0f));
1760
+ tabbarGlassSurface.setVisibility(View.VISIBLE);
1761
+ }
1649
1762
  if (tabbarGlassBackdrop != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1650
1763
  View webView = getBridge() == null ? null : getBridge().getWebView();
1651
1764
  tabbarGlassBackdrop.setClipPathProvider((width, height) -> tabbar.backgroundPath(width, height));
1652
- tabbarGlassBackdrop.configure(webView, dp(resolvedGlassOptions.blurRadiusDp), drawColor);
1765
+ tabbarGlassBackdrop.configure(webView, dp(resolvedGlassOptions.blurRadiusDp), Color.TRANSPARENT);
1653
1766
  tabbarGlassBackdrop.setVisibility(View.VISIBLE);
1654
1767
  } else if (tabbarGlassBackdrop != null) {
1655
1768
  tabbarGlassBackdrop.setClipPathProvider((width, height) -> tabbar.backgroundPath(width, height));
@@ -2187,26 +2300,37 @@ public class NativeNavigationPlugin extends Plugin {
2187
2300
  return Math.max(0, rawNavigationBarInset() - contentRootBottomOffset());
2188
2301
  }
2189
2302
 
2303
+ // API 30+ reads the currently visible bars so a hidden navigation bar (immersive mode) gives the space back.
2190
2304
  private int rawStatusBarInset() {
2191
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
2192
- WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
2193
- if (insets != null) {
2194
- return insets.getStableInsetTop();
2305
+ WindowInsets insets = rootWindowInsets();
2306
+ if (insets != null) {
2307
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
2308
+ return insets.getInsets(WindowInsets.Type.statusBars() | WindowInsets.Type.displayCutout()).top;
2195
2309
  }
2310
+ return insets.getStableInsetTop();
2196
2311
  }
2197
2312
  return systemDimension("status_bar_height");
2198
2313
  }
2199
2314
 
2200
2315
  private int rawNavigationBarInset() {
2201
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
2202
- WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
2203
- if (insets != null) {
2204
- return insets.getStableInsetBottom();
2316
+ WindowInsets insets = rootWindowInsets();
2317
+ if (insets != null) {
2318
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
2319
+ return insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
2205
2320
  }
2321
+ return insets.getStableInsetBottom();
2206
2322
  }
2207
2323
  return systemDimension("navigation_bar_height");
2208
2324
  }
2209
2325
 
2326
+ private WindowInsets rootWindowInsets() {
2327
+ Activity activity = getActivity();
2328
+ if (activity == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
2329
+ return null;
2330
+ }
2331
+ return activity.getWindow().getDecorView().getRootWindowInsets();
2332
+ }
2333
+
2210
2334
  private int contentRootTopOffset() {
2211
2335
  FrameLayout root = contentRoot();
2212
2336
  if (root == null || !root.isLaidOut()) {
@@ -2244,6 +2368,16 @@ public class NativeNavigationPlugin extends Plugin {
2244
2368
  view.post(this::updateInsetsAndNotify);
2245
2369
  }
2246
2370
  });
2371
+ // Showing or hiding a system bar changes the insets without necessarily moving the content root
2372
+ // (edge-to-edge apps). A zero-size probe hears every inset dispatch without replacing anyone's listener.
2373
+ View insetsProbe = new View(getContext());
2374
+ insetsProbe.setOnApplyWindowInsetsListener((view, insets) -> {
2375
+ if (navbarVisible || tabbarVisible) {
2376
+ view.post(this::updateInsetsAndNotify);
2377
+ }
2378
+ return insets;
2379
+ });
2380
+ root.addView(insetsProbe, new FrameLayout.LayoutParams(0, 0));
2247
2381
  }
2248
2382
 
2249
2383
  private int systemDimension(String name) {
@@ -2,7 +2,6 @@
2
2
 
3
3
  import Foundation
4
4
  import Capacitor
5
- import ObjectiveC
6
5
  import UIKit
7
6
 
8
7
  private struct NativeNavigationTransitionContext {
@@ -2052,44 +2051,25 @@ public class NativeNavigationPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarContro
2052
2051
  }
2053
2052
 
2054
2053
  private func liquidGlassEffect() -> UIVisualEffect? {
2055
- guard usesSystemLiquidGlass,
2056
- let effectClass = NSClassFromString("UIGlassEffect") else {
2054
+ guard usesSystemLiquidGlass else {
2057
2055
  return nil
2058
2056
  }
2059
-
2060
- let styleSelector = NSSelectorFromString("effectWithStyle:")
2061
- if let method = class_getClassMethod(effectClass, styleSelector) {
2062
- typealias EffectWithStyle = @convention(c) (AnyClass, Selector, Int) -> AnyObject?
2063
- let factory = unsafeBitCast(method_getImplementation(method), to: EffectWithStyle.self)
2064
- if let effect = factory(effectClass, styleSelector, 0) as? UIVisualEffect {
2065
- return effect
2066
- }
2067
- }
2068
-
2069
- if let objectType = effectClass as? NSObject.Type {
2070
- return objectType.init() as? UIVisualEffect
2057
+ #if compiler(>=6.2)
2058
+ if #available(iOS 26.0, *) {
2059
+ return UIGlassEffect(style: .regular)
2071
2060
  }
2072
-
2061
+ #endif
2073
2062
  return nil
2074
2063
  }
2075
2064
 
2076
2065
  private func configureGlassBarButtonItem(_ item: UIBarButtonItem, id: String) {
2077
- guard #available(iOS 26.0, *) else {
2078
- return
2079
- }
2080
-
2081
- // Keep older SDK builds working while adopting the native iOS 26 bar
2082
- // button Liquid Glass grouping APIs when the runtime exposes them.
2083
- let object = item as NSObject
2084
- if object.responds(to: NSSelectorFromString("setIdentifier:")) {
2085
- object.setValue(id, forKey: "identifier")
2086
- }
2087
- if object.responds(to: NSSelectorFromString("setSharesBackground:")) {
2088
- object.setValue(true, forKey: "sharesBackground")
2089
- }
2090
- if object.responds(to: NSSelectorFromString("setHidesSharedBackground:")) {
2091
- object.setValue(false, forKey: "hidesSharedBackground")
2066
+ #if compiler(>=6.2)
2067
+ if #available(iOS 26.0, *) {
2068
+ item.identifier = id
2069
+ item.sharesBackground = true
2070
+ item.hidesSharedBackground = false
2092
2071
  }
2072
+ #endif
2093
2073
  }
2094
2074
 
2095
2075
  private func currentInsets() -> [String: Any] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-native-navigation",
3
- "version": "8.5.0",
3
+ "version": "8.6.0",
4
4
  "description": "Capacitor plugin for native navbar, tabbar, safe-area handling, and WebView snapshot transitions.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",