@capgo/capacitor-native-navigation 8.2.1 → 8.2.3

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.
@@ -25,12 +25,14 @@ import android.view.MenuItem;
25
25
  import android.view.View;
26
26
  import android.view.ViewGroup;
27
27
  import android.view.ViewOutlineProvider;
28
+ import android.view.ViewTreeObserver;
28
29
  import android.view.Window;
29
30
  import android.view.WindowInsets;
30
31
  import android.widget.FrameLayout;
31
32
  import android.widget.ImageView;
32
33
  import android.widget.LinearLayout;
33
34
  import android.widget.TextView;
35
+ import androidx.annotation.RequiresApi;
34
36
  import androidx.appcompat.content.res.AppCompatResources;
35
37
  import androidx.appcompat.widget.Toolbar;
36
38
  import androidx.core.graphics.PathParser;
@@ -67,15 +69,25 @@ public class NativeNavigationPlugin extends Plugin {
67
69
  private FrameLayout tabbarContainer;
68
70
  private Toolbar toolbar;
69
71
  private NativeTabbarLayout tabbar;
72
+ private GlassBackdropView navbarGlassBackdrop;
73
+ private View navbarGlassSurface;
74
+ private GlassBackdropView tabbarGlassBackdrop;
75
+ private View tabbarGlassSurface;
70
76
  private ImageView transitionSnapshot;
71
77
  private boolean enabled = true;
72
78
  private boolean navbarVisible = false;
73
79
  private boolean tabbarVisible = false;
74
80
  private String contentInsetMode = "css";
81
+ private GlassOptions defaultGlassOptions = GlassOptions.defaults();
82
+ private GlassOptions navbarGlassOptions = GlassOptions.defaults();
83
+ private GlassOptions tabbarGlassOptions = GlassOptions.defaults();
84
+ private JSObject navbarGlassConfig;
85
+ private JSObject tabbarGlassConfig;
75
86
  private int defaultTransitionMs = DEFAULT_TRANSITION_MS;
76
87
  private int activeTransitionMs = DEFAULT_TRANSITION_MS;
77
88
  private int tintColor = Color.rgb(0, 122, 255);
78
89
  private int inactiveTintColor = Color.rgb(120, 126, 137);
90
+ private int navbarBackgroundColor = Color.argb(225, 255, 255, 255);
79
91
  private int tabbarBackgroundColor = Color.argb(235, 255, 255, 255);
80
92
  private int badgeBackgroundColor = Color.rgb(255, 59, 48);
81
93
  private int badgeTextColor = Color.WHITE;
@@ -105,6 +117,9 @@ public class NativeNavigationPlugin extends Plugin {
105
117
  runOnUiThread(() -> {
106
118
  enabled = call.getBoolean("enabled", true);
107
119
  contentInsetMode = call.getString("contentInsetMode", contentInsetMode);
120
+ defaultGlassOptions = GlassOptions.from(call.getObject("glass", null), defaultGlassOptions);
121
+ navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
122
+ tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
108
123
  Double duration = call.getDouble("animationDuration");
109
124
  if (duration != null) {
110
125
  defaultTransitionMs = Math.max(0, duration.intValue());
@@ -120,6 +135,9 @@ public class NativeNavigationPlugin extends Plugin {
120
135
  tabbarContainer.setVisibility(View.GONE);
121
136
  }
122
137
  }
138
+ if (enabled) {
139
+ reapplyVisibleChromeBackgrounds();
140
+ }
123
141
  updateInsetsAndNotify();
124
142
  call.resolve(insetsResult());
125
143
  });
@@ -167,6 +185,8 @@ public class NativeNavigationPlugin extends Plugin {
167
185
 
168
186
  addToolbarItems(nativeToolbar, call.getArray("rightItems", new JSArray()), "right");
169
187
  JSObject colors = call.getObject("colors", new JSObject());
188
+ navbarGlassConfig = call.getObject("glass", null);
189
+ navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
170
190
  applyToolbarColors(nativeToolbar, colors);
171
191
  navbarContainer.setVisibility(View.VISIBLE);
172
192
  layoutChrome();
@@ -210,32 +230,52 @@ public class NativeNavigationPlugin extends Plugin {
210
230
  JSONArray tabs = call.getArray("tabs", new JSArray());
211
231
  String selectedId = call.getString("selectedId", null);
212
232
  JSObject colors = call.getObject("colors", new JSObject());
233
+ tabbarGlassConfig = call.getObject("glass", null);
234
+ tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
213
235
  badgeBackgroundColor = colorOption(call, colors, "badgeBackgroundColor", "badgeBackground", Color.rgb(255, 59, 48));
214
236
  badgeTextColor = colorOption(call, colors, "badgeTextColor", "badgeText", Color.WHITE);
215
237
 
216
- for (int index = 0; index < tabs.length(); index++) {
217
- JSONObject tab = tabs.optJSONObject(index);
238
+ for (int sourceIndex = 0; sourceIndex < tabs.length(); sourceIndex++) {
239
+ JSONObject tab = tabs.optJSONObject(sourceIndex);
218
240
  if (tab == null) {
219
241
  continue;
220
242
  }
221
- String id = tab.optString("id", "tab-" + index);
243
+ String id = tab.optString("id", "tab-" + sourceIndex);
244
+ boolean isHidden = tab.optBoolean("hidden", false);
245
+ if (isHidden && !id.equals(selectedId)) {
246
+ continue;
247
+ }
248
+
249
+ int visibleIndex = tabItems.size();
222
250
  String title = tab.optString("title", "");
223
251
  Drawable icon = icons ? iconFrom(tab.optJSONObject("icon")) : new ColorDrawable(Color.TRANSPARENT);
224
252
  Drawable selectedIcon = icons ? iconFrom(tab.optJSONObject("selectedIcon")) : null;
225
253
  String badge = tab.has("badge") ? String.valueOf(tab.opt("badge")) : null;
226
- tabItems.add(new NativeTabItem(id, title, icon, selectedIcon, badge, tab.optBoolean("enabled", true)));
254
+ tabItems.add(new NativeTabItem(id, title, icon, selectedIcon, badge, tab.optBoolean("enabled", true), sourceIndex));
227
255
  if (id.equals(selectedId)) {
228
- selectedTabIndex = index;
256
+ selectedTabIndex = visibleIndex;
229
257
  }
230
258
  }
231
259
 
232
- if (!tabItems.isEmpty() && (selectedTabIndex < 0 || selectedTabIndex >= tabItems.size())) {
260
+ if (tabItems.isEmpty()) {
261
+ tabbarVisible = false;
262
+ if (tabbarContainer != null) {
263
+ tabbarContainer.setVisibility(View.GONE);
264
+ }
265
+ nativeTabbar.setVisibility(View.GONE);
266
+ layoutChrome();
267
+ updateInsetsAndNotify();
268
+ call.resolve(insetsResult());
269
+ return;
270
+ }
271
+
272
+ if (selectedTabIndex < 0 || selectedTabIndex >= tabItems.size()) {
233
273
  selectedTabIndex = 0;
234
274
  }
235
275
 
236
276
  applyTabbarColors(call, colors);
237
277
  tabbarStyle = makeTabbarStyle(call.getObject("style", new JSObject()));
238
- nativeTabbar.setTabbarStyle(tabbarStyle, tabbarBackgroundColor, centerTabIndex());
278
+ applyTabbarBackground(centerTabIndex());
239
279
  renderTabbarItems(labelVisibilityMode, icons);
240
280
  if (tabbarContainer != null) {
241
281
  tabbarContainer.setVisibility(View.VISIBLE);
@@ -577,6 +617,11 @@ public class NativeNavigationPlugin extends Plugin {
577
617
  navbarContainer.setElevation(dp(8));
578
618
  toolbar = new Toolbar(getContext());
579
619
  toolbar.setPopupTheme(androidx.appcompat.R.style.ThemeOverlay_AppCompat_Light);
620
+ navbarGlassBackdrop = new GlassBackdropView(getContext());
621
+ navbarGlassSurface = new View(getContext());
622
+ navbarGlassBackdrop.setVisibility(View.GONE);
623
+ navbarGlassSurface.setVisibility(View.GONE);
624
+
580
625
  toolbar.setOnMenuItemClickListener((item) -> {
581
626
  int itemId = item.getItemId();
582
627
  JSObject event = new JSObject();
@@ -587,6 +632,8 @@ public class NativeNavigationPlugin extends Plugin {
587
632
  return true;
588
633
  });
589
634
 
635
+ navbarContainer.addView(navbarGlassBackdrop);
636
+ navbarContainer.addView(navbarGlassSurface);
590
637
  navbarContainer.addView(toolbar);
591
638
  if (root != null) {
592
639
  root.addView(navbarContainer);
@@ -609,10 +656,17 @@ public class NativeNavigationPlugin extends Plugin {
609
656
  tabbarContainer.setClipToPadding(false);
610
657
  tabbarContainer.setElevation(dp(12));
611
658
 
659
+ tabbarGlassBackdrop = new GlassBackdropView(getContext());
660
+ tabbarGlassSurface = new View(getContext());
661
+ tabbarGlassBackdrop.setVisibility(View.GONE);
662
+ tabbarGlassSurface.setVisibility(View.GONE);
663
+
612
664
  tabbar = new NativeTabbarLayout(getContext());
613
665
  tabbar.setClipChildren(false);
614
666
  tabbar.setClipToPadding(false);
615
667
  tabbar.setBackgroundColor(Color.TRANSPARENT);
668
+ tabbarContainer.addView(tabbarGlassBackdrop);
669
+ tabbarContainer.addView(tabbarGlassSurface);
616
670
  tabbarContainer.addView(
617
671
  tabbar,
618
672
  new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
@@ -634,7 +688,7 @@ public class NativeNavigationPlugin extends Plugin {
634
688
  }
635
689
  tabbar.removeAllViews();
636
690
  int centerIndex = centerTabIndex();
637
- tabbar.setTabbarStyle(tabbarStyle, tabbarBackgroundColor, centerIndex);
691
+ applyTabbarBackground(centerIndex);
638
692
  for (int index = 0; index < tabItems.size(); index++) {
639
693
  final int itemIndex = index;
640
694
  NativeTabItem item = tabItems.get(index);
@@ -652,7 +706,7 @@ public class NativeNavigationPlugin extends Plugin {
652
706
  renderTabbarItems(labelVisibilityMode, icons);
653
707
  JSObject event = new JSObject();
654
708
  event.put("id", item.id);
655
- event.put("index", itemIndex);
709
+ event.put("index", item.sourceIndex);
656
710
  event.put("title", item.title);
657
711
  notifyListeners("tabSelect", event);
658
712
  });
@@ -910,14 +964,16 @@ public class NativeNavigationPlugin extends Plugin {
910
964
  final Drawable selectedIcon;
911
965
  final String badge;
912
966
  final boolean enabled;
967
+ final int sourceIndex;
913
968
 
914
- NativeTabItem(String id, String title, Drawable icon, Drawable selectedIcon, String badge, boolean enabled) {
969
+ NativeTabItem(String id, String title, Drawable icon, Drawable selectedIcon, String badge, boolean enabled, int sourceIndex) {
915
970
  this.id = id;
916
971
  this.title = title;
917
972
  this.icon = icon;
918
973
  this.selectedIcon = selectedIcon;
919
974
  this.badge = badge;
920
975
  this.enabled = enabled;
976
+ this.sourceIndex = sourceIndex;
921
977
  }
922
978
  }
923
979
 
@@ -1309,6 +1365,7 @@ public class NativeNavigationPlugin extends Plugin {
1309
1365
  : Color.rgb(20, 24, 32);
1310
1366
  int tint = parseColor(colors.getString("tint", null), tintFallback);
1311
1367
  int background = parseColor(colors.getString("background", null), backgroundFallback);
1368
+ navbarBackgroundColor = background;
1312
1369
  int foreground = parseColor(colors.getString("foreground", null), foregroundFallback);
1313
1370
  tintColor = tint;
1314
1371
  nativeToolbar.setTitleTextColor(foreground);
@@ -1319,9 +1376,8 @@ public class NativeNavigationPlugin extends Plugin {
1319
1376
  tintedIcon.setTint(tint);
1320
1377
  nativeToolbar.setNavigationIcon(tintedIcon);
1321
1378
  }
1322
- if (navbarContainer != null) {
1323
- navbarContainer.setBackgroundColor(background);
1324
- }
1379
+ nativeToolbar.setBackgroundColor(Color.TRANSPARENT);
1380
+ applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, background, navbarGlassOptions, 0f);
1325
1381
  for (int index = 0; index < nativeToolbar.getMenu().size(); index++) {
1326
1382
  Drawable icon = nativeToolbar.getMenu().getItem(index).getIcon();
1327
1383
  if (icon != null) {
@@ -1388,9 +1444,115 @@ public class NativeNavigationPlugin extends Plugin {
1388
1444
  tintColor = parseColor(colors.getString("tint", null), tintFallback);
1389
1445
  inactiveTintColor = parseColor(colors.getString("inactiveTint", null), inactiveFallback);
1390
1446
  tabbarBackgroundColor = parseColor(colors.getString("background", null), backgroundFallback);
1391
- if (tabbar != null) {
1392
- tabbar.setTabbarStyle(tabbarStyle, tabbarBackgroundColor, centerTabIndex());
1447
+ applyTabbarBackground();
1448
+ }
1449
+
1450
+ private void applyTabbarBackground() {
1451
+ applyTabbarBackground(centerTabIndex());
1452
+ }
1453
+
1454
+ private void applyTabbarBackground(int centerIndex) {
1455
+ if (tabbar == null) {
1456
+ return;
1457
+ }
1458
+
1459
+ int drawColor = tabbarBackgroundColor;
1460
+ GlassOptions resolvedGlassOptions = tabbarGlassOptions == null ? GlassOptions.defaults() : tabbarGlassOptions;
1461
+ if (resolvedGlassOptions.isLiquidGlass()) {
1462
+ drawColor = glassSurfaceColor(tabbarBackgroundColor, resolvedGlassOptions);
1463
+ if (tabbarContainer != null) {
1464
+ tabbarContainer.setBackgroundColor(Color.TRANSPARENT);
1465
+ }
1466
+ hideGlassBackground(null, tabbarGlassSurface);
1467
+ if (tabbarGlassBackdrop != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1468
+ View webView = getBridge() == null ? null : getBridge().getWebView();
1469
+ tabbarGlassBackdrop.setClipPathProvider((width, height) -> tabbar.backgroundPath(width, height));
1470
+ tabbarGlassBackdrop.configure(webView, dp(resolvedGlassOptions.blurRadiusDp), drawColor);
1471
+ tabbarGlassBackdrop.setVisibility(View.VISIBLE);
1472
+ } else if (tabbarGlassBackdrop != null) {
1473
+ tabbarGlassBackdrop.setClipPathProvider((width, height) -> tabbar.backgroundPath(width, height));
1474
+ tabbarGlassBackdrop.clearEffect();
1475
+ tabbarGlassBackdrop.setVisibility(View.GONE);
1476
+ }
1477
+ } else {
1478
+ hideGlassBackground(tabbarGlassBackdrop, tabbarGlassSurface);
1479
+ if (tabbarGlassBackdrop != null) {
1480
+ tabbarGlassBackdrop.setClipPathProvider(null);
1481
+ }
1482
+ if (tabbarContainer != null) {
1483
+ tabbarContainer.setBackgroundColor(Color.TRANSPARENT);
1484
+ }
1393
1485
  }
1486
+
1487
+ tabbar.setTabbarStyle(tabbarStyle, drawColor, centerIndex);
1488
+ }
1489
+
1490
+ private void reapplyVisibleChromeBackgrounds() {
1491
+ if (navbarContainer != null && navbarContainer.getVisibility() == View.VISIBLE) {
1492
+ applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, navbarBackgroundColor, navbarGlassOptions, 0f);
1493
+ }
1494
+ if (tabbarContainer != null && tabbarContainer.getVisibility() == View.VISIBLE) {
1495
+ applyTabbarBackground();
1496
+ }
1497
+ }
1498
+
1499
+ private void applyChromeBackground(
1500
+ ViewGroup container,
1501
+ GlassBackdropView backdrop,
1502
+ View surface,
1503
+ int background,
1504
+ GlassOptions glassOptions,
1505
+ float cornerRadius
1506
+ ) {
1507
+ if (container == null) {
1508
+ return;
1509
+ }
1510
+ GlassOptions resolvedGlassOptions = glassOptions == null ? GlassOptions.defaults() : glassOptions;
1511
+ if (!resolvedGlassOptions.isLiquidGlass()) {
1512
+ hideGlassBackground(backdrop, surface);
1513
+ container.setBackground(chromeBackgroundDrawable(background, cornerRadius));
1514
+ return;
1515
+ }
1516
+
1517
+ container.setBackgroundColor(Color.TRANSPARENT);
1518
+ int surfaceColor = glassSurfaceColor(background, resolvedGlassOptions);
1519
+ if (surface != null) {
1520
+ surface.setBackground(chromeBackgroundDrawable(surfaceColor, cornerRadius));
1521
+ surface.setVisibility(View.VISIBLE);
1522
+ }
1523
+ if (backdrop != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1524
+ View webView = getBridge() == null ? null : getBridge().getWebView();
1525
+ backdrop.setClipPathProvider(null);
1526
+ backdrop.configure(webView, dp(resolvedGlassOptions.blurRadiusDp), surfaceColor);
1527
+ backdrop.setVisibility(View.VISIBLE);
1528
+ } else if (backdrop != null) {
1529
+ backdrop.clearEffect();
1530
+ backdrop.setVisibility(View.GONE);
1531
+ }
1532
+ }
1533
+
1534
+ private void hideGlassBackground(GlassBackdropView backdrop, View surface) {
1535
+ if (backdrop != null) {
1536
+ backdrop.clearEffect();
1537
+ backdrop.setVisibility(View.GONE);
1538
+ }
1539
+ if (surface != null) {
1540
+ surface.setBackground(null);
1541
+ surface.setVisibility(View.GONE);
1542
+ }
1543
+ }
1544
+
1545
+ private Drawable chromeBackgroundDrawable(int color, float cornerRadius) {
1546
+ GradientDrawable drawable = new GradientDrawable();
1547
+ drawable.setColor(color);
1548
+ if (cornerRadius > 0f) {
1549
+ drawable.setCornerRadius(cornerRadius);
1550
+ }
1551
+ return drawable;
1552
+ }
1553
+
1554
+ private int glassSurfaceColor(int background, GlassOptions glassOptions) {
1555
+ return withAlpha(background, Math.round(Color.alpha(background) * (float) glassOptions.surfaceAlpha));
1394
1556
  }
1395
1557
 
1396
1558
  private int parseColor(String value, int fallback) {
@@ -1462,6 +1624,12 @@ public class NativeNavigationPlugin extends Plugin {
1462
1624
  return mode == Configuration.UI_MODE_NIGHT_YES;
1463
1625
  }
1464
1626
 
1627
+ private void fillContainer(View view) {
1628
+ if (view != null) {
1629
+ view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1630
+ }
1631
+ }
1632
+
1465
1633
  private void layoutChrome() {
1466
1634
  FrameLayout root = contentRoot();
1467
1635
  if (root == null) {
@@ -1487,6 +1655,8 @@ public class NativeNavigationPlugin extends Plugin {
1487
1655
  );
1488
1656
  toolbarParams.topMargin = status;
1489
1657
  toolbar.setLayoutParams(toolbarParams);
1658
+ fillContainer(navbarGlassBackdrop);
1659
+ fillContainer(navbarGlassSurface);
1490
1660
  }
1491
1661
 
1492
1662
  if (tabbarContainer != null) {
@@ -1501,6 +1671,8 @@ public class NativeNavigationPlugin extends Plugin {
1501
1671
  );
1502
1672
  tabbarContainerParams.bottomMargin = tabbarBottomMargin;
1503
1673
  tabbarContainer.setLayoutParams(tabbarContainerParams);
1674
+ fillContainer(tabbarGlassBackdrop);
1675
+ fillContainer(tabbarGlassSurface);
1504
1676
  }
1505
1677
 
1506
1678
  if (tabbar != null) {
@@ -1510,6 +1682,7 @@ public class NativeNavigationPlugin extends Plugin {
1510
1682
  );
1511
1683
  tabbar.setLayoutParams(tabbarParams);
1512
1684
  tabbar.setPadding(0, 0, 0, 0);
1685
+ applyTabbarBackground();
1513
1686
  }
1514
1687
 
1515
1688
  bringChromeToFront();
@@ -1688,6 +1861,230 @@ public class NativeNavigationPlugin extends Plugin {
1688
1861
  return Math.round(value * getContext().getResources().getDisplayMetrics().density);
1689
1862
  }
1690
1863
 
1864
+ private float dp(double value) {
1865
+ return (float) (value * getContext().getResources().getDisplayMetrics().density);
1866
+ }
1867
+
1868
+ private static final class GlassOptions {
1869
+
1870
+ private static final String EFFECT_NONE = "none";
1871
+ private static final String EFFECT_LIQUID_GLASS = "liquidGlass";
1872
+ private static final double DEFAULT_BLUR_RADIUS_DP = 18d;
1873
+ private static final double DEFAULT_SURFACE_ALPHA = 0.62d;
1874
+
1875
+ final String effect;
1876
+ final double blurRadiusDp;
1877
+ final double surfaceAlpha;
1878
+
1879
+ GlassOptions(String effect, double blurRadiusDp, double surfaceAlpha) {
1880
+ this.effect = effect;
1881
+ this.blurRadiusDp = Math.max(0d, blurRadiusDp);
1882
+ this.surfaceAlpha = Math.max(0d, Math.min(1d, surfaceAlpha));
1883
+ }
1884
+
1885
+ static GlassOptions defaults() {
1886
+ return new GlassOptions(EFFECT_NONE, DEFAULT_BLUR_RADIUS_DP, DEFAULT_SURFACE_ALPHA);
1887
+ }
1888
+
1889
+ static GlassOptions from(JSObject raw, GlassOptions fallback) {
1890
+ GlassOptions base = fallback == null ? defaults() : fallback;
1891
+ if (raw == null) {
1892
+ return base;
1893
+ }
1894
+
1895
+ String effect = raw.optString("effect", base.effect);
1896
+ if (!EFFECT_NONE.equals(effect) && !EFFECT_LIQUID_GLASS.equals(effect)) {
1897
+ effect = base.effect;
1898
+ }
1899
+ double blurRadiusDp = raw.has("blurRadius") ? raw.optDouble("blurRadius", base.blurRadiusDp) : base.blurRadiusDp;
1900
+ double surfaceAlpha = raw.has("surfaceAlpha") ? raw.optDouble("surfaceAlpha", base.surfaceAlpha) : base.surfaceAlpha;
1901
+ return new GlassOptions(effect, blurRadiusDp, surfaceAlpha);
1902
+ }
1903
+
1904
+ boolean isLiquidGlass() {
1905
+ return EFFECT_LIQUID_GLASS.equals(effect);
1906
+ }
1907
+ }
1908
+
1909
+ private static final class GlassBackdropView extends View {
1910
+
1911
+ interface PathProvider {
1912
+ Path path(int width, int height);
1913
+ }
1914
+
1915
+ private static final long SOURCE_CONTENT_REFRESH_INTERVAL_MS = 250L;
1916
+
1917
+ private final Paint fallbackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
1918
+ private final int[] sourceLocation = new int[2];
1919
+ private final int[] viewLocation = new int[2];
1920
+ private final ViewTreeObserver.OnScrollChangedListener sourceScrollListener = this::markDirty;
1921
+ private final ViewTreeObserver.OnPreDrawListener sourcePreDrawListener = () -> {
1922
+ markDirtyFromSourcePreDraw();
1923
+ return true;
1924
+ };
1925
+ private final View.OnLayoutChangeListener sourceLayoutListener = (
1926
+ view,
1927
+ left,
1928
+ top,
1929
+ right,
1930
+ bottom,
1931
+ oldLeft,
1932
+ oldTop,
1933
+ oldRight,
1934
+ oldBottom
1935
+ ) -> markDirty();
1936
+ private View source;
1937
+ private PathProvider clipPathProvider;
1938
+ private int fallbackColor = Color.TRANSPARENT;
1939
+ private boolean dirty;
1940
+ private boolean redrawPending;
1941
+ private long lastSourcePreDrawRefreshMs;
1942
+
1943
+ GlassBackdropView(android.content.Context context) {
1944
+ super(context);
1945
+ setWillNotDraw(false);
1946
+ }
1947
+
1948
+ void configure(View source, float blurRadiusPx, int fallbackColor) {
1949
+ this.fallbackColor = fallbackColor;
1950
+ attachSource(source);
1951
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1952
+ Api31RenderEffects.setBlur(this, blurRadiusPx);
1953
+ }
1954
+ markDirty();
1955
+ }
1956
+
1957
+ void clearEffect() {
1958
+ attachSource(null);
1959
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1960
+ Api31RenderEffects.clear(this);
1961
+ }
1962
+ markDirty();
1963
+ }
1964
+
1965
+ void setClipPathProvider(PathProvider clipPathProvider) {
1966
+ this.clipPathProvider = clipPathProvider;
1967
+ markDirty();
1968
+ }
1969
+
1970
+ private void attachSource(View nextSource) {
1971
+ if (source == nextSource) {
1972
+ return;
1973
+ }
1974
+ if (source != null) {
1975
+ source.removeOnLayoutChangeListener(sourceLayoutListener);
1976
+ ViewTreeObserver observer = source.getViewTreeObserver();
1977
+ if (observer.isAlive()) {
1978
+ observer.removeOnScrollChangedListener(sourceScrollListener);
1979
+ observer.removeOnPreDrawListener(sourcePreDrawListener);
1980
+ }
1981
+ }
1982
+ source = nextSource;
1983
+ if (source != null) {
1984
+ source.addOnLayoutChangeListener(sourceLayoutListener);
1985
+ ViewTreeObserver observer = source.getViewTreeObserver();
1986
+ if (observer.isAlive()) {
1987
+ observer.addOnScrollChangedListener(sourceScrollListener);
1988
+ observer.addOnPreDrawListener(sourcePreDrawListener);
1989
+ }
1990
+ }
1991
+ markDirty();
1992
+ }
1993
+
1994
+ private void markDirty() {
1995
+ dirty = true;
1996
+ scheduleRedrawIfVisible();
1997
+ }
1998
+
1999
+ private void markDirtyFromSourcePreDraw() {
2000
+ dirty = true;
2001
+ long now = System.currentTimeMillis();
2002
+ if (now - lastSourcePreDrawRefreshMs < SOURCE_CONTENT_REFRESH_INTERVAL_MS) {
2003
+ return;
2004
+ }
2005
+ lastSourcePreDrawRefreshMs = now;
2006
+ scheduleRedrawIfVisible();
2007
+ }
2008
+
2009
+ private void scheduleRedrawIfVisible() {
2010
+ if (redrawPending || getVisibility() != View.VISIBLE || !isShown()) {
2011
+ return;
2012
+ }
2013
+ redrawPending = true;
2014
+ postInvalidateOnAnimation();
2015
+ }
2016
+
2017
+ @Override
2018
+ protected void onDraw(Canvas canvas) {
2019
+ super.onDraw(canvas);
2020
+ Path clipPath = clipPathProvider == null ? null : clipPathProvider.path(getWidth(), getHeight());
2021
+ if (clipPath == null) {
2022
+ drawSource(canvas);
2023
+ } else {
2024
+ int save = canvas.save();
2025
+ canvas.clipPath(clipPath);
2026
+ drawSource(canvas);
2027
+ canvas.restoreToCount(save);
2028
+ }
2029
+
2030
+ dirty = false;
2031
+ redrawPending = false;
2032
+ }
2033
+
2034
+ private void drawSource(Canvas canvas) {
2035
+ View currentSource = source;
2036
+ if (currentSource == null || currentSource.getWidth() <= 0 || currentSource.getHeight() <= 0) {
2037
+ fallbackPaint.setColor(fallbackColor);
2038
+ canvas.drawRect(0, 0, getWidth(), getHeight(), fallbackPaint);
2039
+ return;
2040
+ }
2041
+ currentSource.getLocationOnScreen(sourceLocation);
2042
+ getLocationOnScreen(viewLocation);
2043
+ canvas.save();
2044
+ canvas.translate(sourceLocation[0] - viewLocation[0], sourceLocation[1] - viewLocation[1]);
2045
+ currentSource.draw(canvas);
2046
+ canvas.restore();
2047
+ }
2048
+
2049
+ @Override
2050
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
2051
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
2052
+ if (width != oldWidth || height != oldHeight) {
2053
+ markDirty();
2054
+ }
2055
+ }
2056
+
2057
+ @Override
2058
+ protected void onVisibilityChanged(View changedView, int visibility) {
2059
+ super.onVisibilityChanged(changedView, visibility);
2060
+ if (visibility != View.VISIBLE) {
2061
+ redrawPending = false;
2062
+ return;
2063
+ }
2064
+ if (dirty) {
2065
+ scheduleRedrawIfVisible();
2066
+ }
2067
+ }
2068
+ }
2069
+
2070
+ @RequiresApi(Build.VERSION_CODES.S)
2071
+ private static final class Api31RenderEffects {
2072
+
2073
+ static void setBlur(View view, float blurRadiusPx) {
2074
+ if (blurRadiusPx <= 0f) {
2075
+ view.setRenderEffect(null);
2076
+ return;
2077
+ }
2078
+ view.setRenderEffect(
2079
+ android.graphics.RenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, android.graphics.Shader.TileMode.CLAMP)
2080
+ );
2081
+ }
2082
+
2083
+ static void clear(View view) {
2084
+ view.setRenderEffect(null);
2085
+ }
2086
+ }
2087
+
1691
2088
  private void runOnUiThread(Runnable runnable) {
1692
2089
  Activity activity = getActivity();
1693
2090
  if (activity == null) {
package/dist/docs.json CHANGED
@@ -412,7 +412,7 @@
412
412
  {
413
413
  "name": "inactiveTint",
414
414
  "tags": [],
415
- "docs": "Color for inactive tab items.",
415
+ "docs": "Color for inactive tab items. Ignored on iOS 26+ unless\n`experimentalBakedTintColors` is enabled.",
416
416
  "complexTypes": [],
417
417
  "type": "string | undefined"
418
418
  },
@@ -797,6 +797,13 @@
797
797
  ],
798
798
  "type": "NativeNavigationGlassOptions"
799
799
  },
800
+ {
801
+ "name": "experimentalBakedTintColors",
802
+ "tags": [],
803
+ "docs": "Opt into the iOS 26 Liquid Glass tint workaround that renders active and\ninactive tab items into baked images. This can affect badge positioning\nand icon sizing, so it is disabled by default.",
804
+ "complexTypes": [],
805
+ "type": "boolean | undefined"
806
+ },
800
807
  {
801
808
  "name": "disableTransparentOnScrollEdge",
802
809
  "tags": [],
@@ -909,6 +916,13 @@
909
916
  "docs": "Whether the tab is enabled. Defaults to `true`.",
910
917
  "complexTypes": [],
911
918
  "type": "boolean | undefined"
919
+ },
920
+ {
921
+ "name": "hidden",
922
+ "tags": [],
923
+ "docs": "Hide the tab item from the native tabbar. When the hidden tab is selected,\nnative platform constraints may keep it visible until another tab is\nselected.",
924
+ "complexTypes": [],
925
+ "type": "boolean | undefined"
912
926
  }
913
927
  ]
914
928
  },
@@ -118,6 +118,7 @@ export function defineNativeNavigationElements() {
118
118
  'ripple-color',
119
119
  'badge-background-color',
120
120
  'badge-text-color',
121
+ 'experimental-baked-tint-colors',
121
122
  'animated',
122
123
  ];
123
124
  }
@@ -144,6 +145,7 @@ export function defineNativeNavigationElements() {
144
145
  indicatorColor: normalizeAttribute(this.getAttribute('indicator-color')),
145
146
  rippleColor: normalizeAttribute(this.getAttribute('ripple-color')),
146
147
  badgeBackgroundColor: normalizeAttribute(this.getAttribute('badge-background-color')),
148
+ experimentalBakedTintColors: parseBoolean(this.getAttribute('experimental-baked-tint-colors')),
147
149
  badgeTextColor: normalizeAttribute(this.getAttribute('badge-text-color')),
148
150
  animated: parseBoolean(this.getAttribute('animated')),
149
151
  };