@capgo/capacitor-native-navigation 8.0.23 → 8.1.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.
@@ -18,16 +18,20 @@ import android.graphics.drawable.Drawable;
18
18
  import android.graphics.drawable.GradientDrawable;
19
19
  import android.graphics.drawable.StateListDrawable;
20
20
  import android.os.Build;
21
+ import android.view.ContextThemeWrapper;
21
22
  import android.view.Gravity;
22
23
  import android.view.Menu;
23
24
  import android.view.MenuItem;
24
25
  import android.view.View;
25
26
  import android.view.ViewGroup;
26
27
  import android.view.ViewOutlineProvider;
28
+ import android.view.ViewTreeObserver;
27
29
  import android.view.Window;
28
30
  import android.view.WindowInsets;
29
31
  import android.widget.FrameLayout;
30
32
  import android.widget.ImageView;
33
+ import android.widget.TextView;
34
+ import androidx.annotation.RequiresApi;
31
35
  import androidx.appcompat.content.res.AppCompatResources;
32
36
  import androidx.appcompat.widget.Toolbar;
33
37
  import androidx.core.graphics.PathParser;
@@ -60,16 +64,32 @@ public class NativeNavigationPlugin extends Plugin {
60
64
  private static final int DEFAULT_NAVBAR_DP = 56;
61
65
  // Android uses Material bottom navigation metrics; web/iOS keep their own platform-native tab bar heights.
62
66
  private static final int DEFAULT_TABBAR_DP = 80;
67
+ private static final int LIQUID_GLASS_TABBAR_DP = 64;
68
+ private static final int LIQUID_GLASS_TABBAR_MAX_WIDTH_DP = 300;
69
+ private static final int LIQUID_GLASS_TABBAR_SIDE_MARGIN_DP = 30;
70
+ private static final int LIQUID_GLASS_TABBAR_CONTENT_TOP_PADDING_DP = 8;
71
+ private static final int LIQUID_GLASS_TABBAR_CONTENT_TRANSLATION_Y_DP = 6;
72
+ private static final int LIQUID_GLASS_TABBAR_LABEL_TRANSLATION_Y_DP = 3;
73
+ private static final int LIQUID_GLASS_TABBAR_BADGE_VERTICAL_OFFSET_DP = 2;
63
74
  private static final int TABBAR_ICON_DP = 24;
64
75
  private static final int TABBAR_ITEM_VERTICAL_PADDING_DP = 8;
76
+ private static final int LIQUID_GLASS_TABBAR_ITEM_VERTICAL_PADDING_DP = 4;
65
77
  private static final int TABBAR_INDICATOR_DP = 32;
78
+ private static final int LIQUID_GLASS_TABBAR_INDICATOR_DP = 28;
66
79
  private static final int TABBAR_INDICATOR_LABEL_PADDING_DP = 4;
80
+ private static final int LIQUID_GLASS_TABBAR_INDICATOR_LABEL_PADDING_DP = 6;
81
+ private static final int TABBAR_FLOATING_BOTTOM_GAP_DP = 10;
82
+ private static final int LIQUID_GLASS_TABBAR_FLOATING_BOTTOM_GAP_DP = 24;
67
83
  private static final int DEFAULT_TRANSITION_MS = 350;
68
84
  private static final int MENU_ITEM_BASE = 10_000;
69
85
 
70
86
  private final NativeNavigation implementation = new NativeNavigation();
71
87
  private FrameLayout navbarContainer;
72
88
  private FrameLayout tabbarContainer;
89
+ private GlassBackdropView navbarGlassBackdrop;
90
+ private View navbarGlassSurface;
91
+ private GlassBackdropView tabbarGlassBackdrop;
92
+ private View tabbarGlassSurface;
73
93
  private Toolbar toolbar;
74
94
  private BottomNavigationView tabbar;
75
95
  private ImageView transitionSnapshot;
@@ -81,6 +101,13 @@ public class NativeNavigationPlugin extends Plugin {
81
101
  private int activeTransitionMs = DEFAULT_TRANSITION_MS;
82
102
  private int tintColor = Color.rgb(0, 122, 255);
83
103
  private int inactiveTintColor = Color.rgb(120, 126, 137);
104
+ private GlassOptions defaultGlassOptions = GlassOptions.defaults();
105
+ private GlassOptions navbarGlassOptions = GlassOptions.defaults();
106
+ private GlassOptions tabbarGlassOptions = GlassOptions.defaults();
107
+ private JSObject navbarGlassConfig;
108
+ private JSObject tabbarGlassConfig;
109
+ private int navbarBackgroundColor = Color.argb(225, 255, 255, 255);
110
+ private int tabbarBackgroundColor = Color.argb(235, 255, 255, 255);
84
111
  private String activeTransitionId;
85
112
  private String activeTransitionDirection = "forward";
86
113
  private RectF activeZoomSourceFrame;
@@ -104,6 +131,9 @@ public class NativeNavigationPlugin extends Plugin {
104
131
  runOnUiThread(() -> {
105
132
  enabled = call.getBoolean("enabled", true);
106
133
  contentInsetMode = call.getString("contentInsetMode", contentInsetMode);
134
+ defaultGlassOptions = GlassOptions.from(call.getObject("glass", null), defaultGlassOptions);
135
+ navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
136
+ tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
107
137
  Double duration = call.getDouble("animationDuration");
108
138
  if (duration != null) {
109
139
  defaultTransitionMs = Math.max(0, duration.intValue());
@@ -119,6 +149,12 @@ public class NativeNavigationPlugin extends Plugin {
119
149
  tabbarContainer.setVisibility(View.GONE);
120
150
  }
121
151
  }
152
+ if (enabled) {
153
+ reapplyVisibleChromeBackgrounds();
154
+ if (tabbar != null) {
155
+ applyTabbarMetrics(tabbar);
156
+ }
157
+ }
122
158
  updateInsetsAndNotify();
123
159
  call.resolve(insetsResult());
124
160
  });
@@ -166,6 +202,8 @@ public class NativeNavigationPlugin extends Plugin {
166
202
 
167
203
  addToolbarItems(nativeToolbar, call.getArray("rightItems", new JSArray()), "right");
168
204
  JSObject colors = call.getObject("colors", new JSObject());
205
+ navbarGlassConfig = call.getObject("glass", null);
206
+ navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
169
207
  applyToolbarColors(nativeToolbar, colors);
170
208
  navbarContainer.setVisibility(View.VISIBLE);
171
209
  layoutChrome();
@@ -214,6 +252,8 @@ public class NativeNavigationPlugin extends Plugin {
214
252
  JSONArray tabs = call.getArray("tabs", new JSArray());
215
253
  String selectedId = call.getString("selectedId", null);
216
254
  JSObject colors = call.getObject("colors", new JSObject());
255
+ tabbarGlassConfig = call.getObject("glass", null);
256
+ tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
217
257
  Integer badgeBackground = colorOption(call, colors, "badgeBackgroundColor", "badgeBackground", null);
218
258
  Integer badgeText = colorOption(call, colors, "badgeTextColor", "badgeText", null);
219
259
  for (int index = 0; index < tabs.length(); index++) {
@@ -233,6 +273,9 @@ public class NativeNavigationPlugin extends Plugin {
233
273
  if (tab.has("badge")) {
234
274
  nativeTabbar.removeBadge(itemId);
235
275
  BadgeDrawable badge = nativeTabbar.getOrCreateBadge(itemId);
276
+ if (isLiquidGlassTabbar()) {
277
+ badge.setVerticalOffset(dp(LIQUID_GLASS_TABBAR_BADGE_VERTICAL_OFFSET_DP));
278
+ }
236
279
  if (badgeBackground != null) {
237
280
  badge.setBackgroundColor(badgeBackground);
238
281
  }
@@ -595,6 +638,10 @@ public class NativeNavigationPlugin extends Plugin {
595
638
  FrameLayout root = contentRoot();
596
639
  navbarContainer = new FrameLayout(getContext());
597
640
  navbarContainer.setElevation(dp(8));
641
+ navbarGlassBackdrop = new GlassBackdropView(getContext());
642
+ navbarGlassSurface = new View(getContext());
643
+ navbarGlassBackdrop.setVisibility(View.GONE);
644
+ navbarGlassSurface.setVisibility(View.GONE);
598
645
  toolbar = new Toolbar(getContext());
599
646
  toolbar.setPopupTheme(androidx.appcompat.R.style.ThemeOverlay_AppCompat_Light);
600
647
  toolbar.setOnMenuItemClickListener((item) -> {
@@ -607,6 +654,8 @@ public class NativeNavigationPlugin extends Plugin {
607
654
  return true;
608
655
  });
609
656
 
657
+ navbarContainer.addView(navbarGlassBackdrop);
658
+ navbarContainer.addView(navbarGlassSurface);
610
659
  navbarContainer.addView(toolbar);
611
660
  if (root != null) {
612
661
  root.addView(navbarContainer);
@@ -626,26 +675,34 @@ public class NativeNavigationPlugin extends Plugin {
626
675
  FrameLayout root = contentRoot();
627
676
  tabbarContainer = new FrameLayout(getContext());
628
677
  tabbarContainer.setElevation(dp(12));
678
+ tabbarContainer.setClipChildren(false);
679
+ tabbarContainer.setClipToPadding(false);
680
+ tabbarGlassBackdrop = new GlassBackdropView(getContext());
681
+ tabbarGlassSurface = new View(getContext());
682
+ tabbarGlassBackdrop.setVisibility(View.GONE);
683
+ tabbarGlassSurface.setVisibility(View.GONE);
629
684
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
630
- tabbarContainer.setClipToOutline(true);
631
- tabbarContainer.setOutlineProvider(
632
- new ViewOutlineProvider() {
633
- @Override
634
- public void getOutline(View view, Outline outline) {
635
- outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), view.getHeight() / 2f);
636
- }
685
+ ViewOutlineProvider tabbarPillOutlineProvider = new ViewOutlineProvider() {
686
+ @Override
687
+ public void getOutline(View view, Outline outline) {
688
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), view.getHeight() / 2f);
637
689
  }
638
- );
639
- }
640
-
641
- tabbar = new BottomNavigationView(getContext());
690
+ };
691
+ tabbarContainer.setClipToOutline(false);
692
+ tabbarContainer.setOutlineProvider(tabbarPillOutlineProvider);
693
+ tabbarGlassBackdrop.setClipToOutline(true);
694
+ tabbarGlassBackdrop.setOutlineProvider(tabbarPillOutlineProvider);
695
+ tabbarGlassSurface.setClipToOutline(true);
696
+ tabbarGlassSurface.setOutlineProvider(tabbarPillOutlineProvider);
697
+ }
698
+ tabbar = new BottomNavigationView(
699
+ new ContextThemeWrapper(getContext(), com.google.android.material.R.style.Theme_MaterialComponents_DayNight)
700
+ );
642
701
  tabbar.setElevation(0);
643
- tabbar.setMinimumHeight(dp(DEFAULT_TABBAR_DP));
702
+ tabbar.setClipChildren(false);
703
+ tabbar.setClipToPadding(false);
644
704
  tabbar.setItemIconSize(dp(TABBAR_ICON_DP));
645
- tabbar.setItemPaddingTop(dp(TABBAR_ITEM_VERTICAL_PADDING_DP));
646
- tabbar.setItemPaddingBottom(dp(TABBAR_ITEM_VERTICAL_PADDING_DP));
647
- tabbar.setItemActiveIndicatorHeight(dp(TABBAR_INDICATOR_DP));
648
- tabbar.setActiveIndicatorLabelPadding(dp(TABBAR_INDICATOR_LABEL_PADDING_DP));
705
+ applyTabbarMetrics(tabbar);
649
706
  tabbar.setBackgroundColor(Color.TRANSPARENT);
650
707
  tabbar.setOnItemSelectedListener((item) -> {
651
708
  int itemId = item.getItemId();
@@ -659,14 +716,13 @@ public class NativeNavigationPlugin extends Plugin {
659
716
  notifyListeners("tabSelect", event);
660
717
  return true;
661
718
  });
719
+ tabbarContainer.addView(tabbarGlassBackdrop);
720
+ tabbarContainer.addView(tabbarGlassSurface);
662
721
  tabbarContainer.addView(tabbar);
663
722
  if (root != null) {
664
723
  root.addView(tabbarContainer);
665
724
  } else {
666
- getActivity().addContentView(
667
- tabbarContainer,
668
- new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(DEFAULT_TABBAR_DP))
669
- );
725
+ getActivity().addContentView(tabbarContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, tabbarHeight()));
670
726
  }
671
727
  return tabbar;
672
728
  }
@@ -1026,6 +1082,7 @@ public class NativeNavigationPlugin extends Plugin {
1026
1082
  : Color.rgb(20, 24, 32);
1027
1083
  int tint = parseColor(colors.getString("tint", null), tintFallback);
1028
1084
  int background = parseColor(colors.getString("background", null), backgroundFallback);
1085
+ navbarBackgroundColor = background;
1029
1086
  int foreground = parseColor(colors.getString("foreground", null), foregroundFallback);
1030
1087
  tintColor = tint;
1031
1088
  nativeToolbar.setTitleTextColor(foreground);
@@ -1036,9 +1093,8 @@ public class NativeNavigationPlugin extends Plugin {
1036
1093
  tintedIcon.setTint(tint);
1037
1094
  nativeToolbar.setNavigationIcon(tintedIcon);
1038
1095
  }
1039
- if (navbarContainer != null) {
1040
- navbarContainer.setBackgroundColor(background);
1041
- }
1096
+ nativeToolbar.setBackgroundColor(Color.TRANSPARENT);
1097
+ applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, background, navbarGlassOptions, 0f);
1042
1098
  for (int index = 0; index < nativeToolbar.getMenu().size(); index++) {
1043
1099
  Drawable icon = nativeToolbar.getMenu().getItem(index).getIcon();
1044
1100
  if (icon != null) {
@@ -1057,6 +1113,7 @@ public class NativeNavigationPlugin extends Plugin {
1057
1113
  int tint = parseColor(colors.getString("tint", null), tintFallback);
1058
1114
  int inactiveTint = parseColor(colors.getString("inactiveTint", null), inactiveFallback);
1059
1115
  int background = parseColor(colors.getString("background", null), backgroundFallback);
1116
+ tabbarBackgroundColor = background;
1060
1117
  tintColor = tint;
1061
1118
  inactiveTintColor = inactiveTint;
1062
1119
  int[][] states = new int[][] { new int[] { android.R.attr.state_checked }, new int[] {} };
@@ -1070,14 +1127,91 @@ public class NativeNavigationPlugin extends Plugin {
1070
1127
  Integer ripple = colorOption(call, colors, "rippleColor", "ripple", null);
1071
1128
  nativeTabbar.setItemRippleColor(ripple == null ? null : ColorStateList.valueOf(ripple));
1072
1129
  nativeTabbar.setBackgroundColor(Color.TRANSPARENT);
1073
- if (tabbarContainer != null) {
1074
- GradientDrawable capsule = new GradientDrawable();
1075
- capsule.setColor(background);
1076
- capsule.setCornerRadius(dp(DEFAULT_TABBAR_DP) / 2f);
1077
- tabbarContainer.setBackground(capsule);
1130
+ applyTabbarMetrics(nativeTabbar);
1131
+ applyChromeBackground(
1132
+ tabbarContainer,
1133
+ tabbarGlassBackdrop,
1134
+ tabbarGlassSurface,
1135
+ background,
1136
+ tabbarGlassOptions,
1137
+ tabbarCornerRadius()
1138
+ );
1139
+ }
1140
+
1141
+ private void reapplyVisibleChromeBackgrounds() {
1142
+ if (navbarContainer != null && navbarContainer.getVisibility() == View.VISIBLE) {
1143
+ applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, navbarBackgroundColor, navbarGlassOptions, 0f);
1144
+ }
1145
+ if (tabbarContainer != null && tabbarContainer.getVisibility() == View.VISIBLE) {
1146
+ applyChromeBackground(
1147
+ tabbarContainer,
1148
+ tabbarGlassBackdrop,
1149
+ tabbarGlassSurface,
1150
+ tabbarBackgroundColor,
1151
+ tabbarGlassOptions,
1152
+ tabbarCornerRadius()
1153
+ );
1154
+ }
1155
+ }
1156
+
1157
+ private void applyChromeBackground(
1158
+ ViewGroup container,
1159
+ GlassBackdropView backdrop,
1160
+ View surface,
1161
+ int background,
1162
+ GlassOptions glassOptions,
1163
+ float cornerRadius
1164
+ ) {
1165
+ if (container == null) {
1166
+ return;
1167
+ }
1168
+ GlassOptions resolvedGlassOptions = glassOptions == null ? GlassOptions.defaults() : glassOptions;
1169
+ if (!resolvedGlassOptions.isLiquidGlass()) {
1170
+ hideGlassBackground(backdrop, surface);
1171
+ container.setBackground(chromeBackgroundDrawable(background, cornerRadius));
1172
+ return;
1173
+ }
1174
+
1175
+ container.setBackgroundColor(Color.TRANSPARENT);
1176
+ int surfaceColor = glassSurfaceColor(background, resolvedGlassOptions);
1177
+ if (surface != null) {
1178
+ surface.setBackground(chromeBackgroundDrawable(surfaceColor, cornerRadius));
1179
+ surface.setVisibility(View.VISIBLE);
1180
+ }
1181
+ if (backdrop != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1182
+ View webView = getBridge() == null ? null : getBridge().getWebView();
1183
+ backdrop.configure(webView, dp(resolvedGlassOptions.blurRadiusDp), surfaceColor);
1184
+ backdrop.setVisibility(View.VISIBLE);
1185
+ } else if (backdrop != null) {
1186
+ backdrop.clearEffect();
1187
+ backdrop.setVisibility(View.GONE);
1188
+ }
1189
+ }
1190
+
1191
+ private void hideGlassBackground(GlassBackdropView backdrop, View surface) {
1192
+ if (backdrop != null) {
1193
+ backdrop.clearEffect();
1194
+ backdrop.setVisibility(View.GONE);
1195
+ }
1196
+ if (surface != null) {
1197
+ surface.setBackground(null);
1198
+ surface.setVisibility(View.GONE);
1078
1199
  }
1079
1200
  }
1080
1201
 
1202
+ private Drawable chromeBackgroundDrawable(int color, float cornerRadius) {
1203
+ GradientDrawable drawable = new GradientDrawable();
1204
+ drawable.setColor(color);
1205
+ if (cornerRadius > 0f) {
1206
+ drawable.setCornerRadius(cornerRadius);
1207
+ }
1208
+ return drawable;
1209
+ }
1210
+
1211
+ private int glassSurfaceColor(int background, GlassOptions glassOptions) {
1212
+ return withAlpha(background, Math.round(Color.alpha(background) * (float) glassOptions.surfaceAlpha));
1213
+ }
1214
+
1081
1215
  private int parseColor(String value, int fallback) {
1082
1216
  if (value == null || value.isEmpty()) {
1083
1217
  return fallback;
@@ -1141,6 +1275,91 @@ public class NativeNavigationPlugin extends Plugin {
1141
1275
  return mode == Configuration.UI_MODE_NIGHT_YES;
1142
1276
  }
1143
1277
 
1278
+ private void fillContainer(View view) {
1279
+ if (view != null) {
1280
+ view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1281
+ }
1282
+ }
1283
+
1284
+ private boolean isLiquidGlassTabbar() {
1285
+ return tabbarGlassOptions != null && tabbarGlassOptions.isLiquidGlass();
1286
+ }
1287
+
1288
+ private int tabbarHeight() {
1289
+ return dp(isLiquidGlassTabbar() ? LIQUID_GLASS_TABBAR_DP : DEFAULT_TABBAR_DP);
1290
+ }
1291
+
1292
+ private float tabbarCornerRadius() {
1293
+ return tabbarHeight() / 2f;
1294
+ }
1295
+
1296
+ private int tabbarWidth(int rootWidth) {
1297
+ if (isLiquidGlassTabbar()) {
1298
+ return Math.min(Math.max(0, rootWidth - dp(LIQUID_GLASS_TABBAR_SIDE_MARGIN_DP * 2)), dp(LIQUID_GLASS_TABBAR_MAX_WIDTH_DP));
1299
+ }
1300
+ return Math.min(Math.max(0, rootWidth - dp(48)), dp(420));
1301
+ }
1302
+
1303
+ private int floatingTabbarBottomMargin(int navigationInset) {
1304
+ if (isLiquidGlassTabbar()) {
1305
+ return Math.max(dp(LIQUID_GLASS_TABBAR_FLOATING_BOTTOM_GAP_DP), navigationInset);
1306
+ }
1307
+ return navigationInset + dp(TABBAR_FLOATING_BOTTOM_GAP_DP);
1308
+ }
1309
+
1310
+ private void applyTabbarMetrics(BottomNavigationView nativeTabbar) {
1311
+ boolean liquidGlass = isLiquidGlassTabbar();
1312
+ nativeTabbar.setMinimumHeight(tabbarHeight());
1313
+ nativeTabbar.setPadding(0, liquidGlass ? dp(LIQUID_GLASS_TABBAR_CONTENT_TOP_PADDING_DP) : 0, 0, 0);
1314
+ nativeTabbar.setItemPaddingTop(dp(liquidGlass ? LIQUID_GLASS_TABBAR_ITEM_VERTICAL_PADDING_DP : TABBAR_ITEM_VERTICAL_PADDING_DP));
1315
+ nativeTabbar.setItemPaddingBottom(dp(liquidGlass ? LIQUID_GLASS_TABBAR_ITEM_VERTICAL_PADDING_DP : TABBAR_ITEM_VERTICAL_PADDING_DP));
1316
+ nativeTabbar.setItemActiveIndicatorHeight(dp(liquidGlass ? LIQUID_GLASS_TABBAR_INDICATOR_DP : TABBAR_INDICATOR_DP));
1317
+ nativeTabbar.setActiveIndicatorLabelPadding(
1318
+ dp(liquidGlass ? LIQUID_GLASS_TABBAR_INDICATOR_LABEL_PADDING_DP : TABBAR_INDICATOR_LABEL_PADDING_DP)
1319
+ );
1320
+ alignTabbarMenu(nativeTabbar, liquidGlass);
1321
+ nativeTabbar.post(() -> alignTabbarMenu(nativeTabbar, isLiquidGlassTabbar()));
1322
+ }
1323
+
1324
+ private void alignTabbarMenu(BottomNavigationView nativeTabbar, boolean liquidGlass) {
1325
+ nativeTabbar.setClipChildren(false);
1326
+ nativeTabbar.setClipToPadding(false);
1327
+ if (nativeTabbar.getChildCount() == 0) {
1328
+ return;
1329
+ }
1330
+ View menuView = nativeTabbar.getChildAt(0);
1331
+ ViewGroup.LayoutParams currentParams = menuView.getLayoutParams();
1332
+ FrameLayout.LayoutParams menuParams =
1333
+ currentParams instanceof FrameLayout.LayoutParams
1334
+ ? (FrameLayout.LayoutParams) currentParams
1335
+ : new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
1336
+ menuParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
1337
+ menuParams.height = liquidGlass ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;
1338
+ menuParams.gravity = liquidGlass ? Gravity.CENTER : Gravity.TOP;
1339
+ menuView.setLayoutParams(menuParams);
1340
+ menuView.setTranslationY(liquidGlass ? dp(LIQUID_GLASS_TABBAR_CONTENT_TRANSLATION_Y_DP) : 0f);
1341
+ if (menuView instanceof ViewGroup) {
1342
+ ViewGroup menuGroup = (ViewGroup) menuView;
1343
+ menuGroup.setClipChildren(false);
1344
+ menuGroup.setClipToPadding(false);
1345
+ translateTabbarLabels(menuGroup, liquidGlass ? dp(LIQUID_GLASS_TABBAR_LABEL_TRANSLATION_Y_DP) : 0f);
1346
+ }
1347
+ }
1348
+
1349
+ private void translateTabbarLabels(View view, float translationY) {
1350
+ if (view instanceof TextView) {
1351
+ view.setTranslationY(translationY);
1352
+ return;
1353
+ }
1354
+ if (!(view instanceof ViewGroup)) {
1355
+ return;
1356
+ }
1357
+ ViewGroup group = (ViewGroup) view;
1358
+ for (int index = 0; index < group.getChildCount(); index++) {
1359
+ translateTabbarLabels(group.getChildAt(index), translationY);
1360
+ }
1361
+ }
1362
+
1144
1363
  private void layoutChrome() {
1145
1364
  FrameLayout root = contentRoot();
1146
1365
  if (root == null) {
@@ -1149,8 +1368,8 @@ public class NativeNavigationPlugin extends Plugin {
1149
1368
  int status = statusBarInset();
1150
1369
  int bottom = navigationBarInset();
1151
1370
  int navbarHeight = navbarVisible ? status + dp(DEFAULT_NAVBAR_DP) : 0;
1152
- int tabbarHeight = dp(DEFAULT_TABBAR_DP);
1153
- int tabbarBottomMargin = tabbarVisible ? bottom + dp(10) : bottom;
1371
+ int tabbarHeight = tabbarHeight();
1372
+ int tabbarBottomMargin = tabbarVisible ? floatingTabbarBottomMargin(bottom) : bottom;
1154
1373
 
1155
1374
  if (navbarContainer != null) {
1156
1375
  FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(
@@ -1159,6 +1378,8 @@ public class NativeNavigationPlugin extends Plugin {
1159
1378
  Gravity.TOP
1160
1379
  );
1161
1380
  navbarContainer.setLayoutParams(containerParams);
1381
+ fillContainer(navbarGlassBackdrop);
1382
+ fillContainer(navbarGlassSurface);
1162
1383
  FrameLayout.LayoutParams toolbarParams = new FrameLayout.LayoutParams(
1163
1384
  ViewGroup.LayoutParams.MATCH_PARENT,
1164
1385
  dp(DEFAULT_NAVBAR_DP),
@@ -1170,7 +1391,7 @@ public class NativeNavigationPlugin extends Plugin {
1170
1391
 
1171
1392
  if (tabbarContainer != null) {
1172
1393
  int rootWidth = root.getWidth() > 0 ? root.getWidth() : Resources.getSystem().getDisplayMetrics().widthPixels;
1173
- int tabbarWidth = Math.min(Math.max(0, rootWidth - dp(48)), dp(420));
1394
+ int tabbarWidth = tabbarWidth(rootWidth);
1174
1395
  FrameLayout.LayoutParams tabbarContainerParams = new FrameLayout.LayoutParams(
1175
1396
  tabbarWidth,
1176
1397
  tabbarHeight,
@@ -1178,18 +1399,23 @@ public class NativeNavigationPlugin extends Plugin {
1178
1399
  );
1179
1400
  tabbarContainerParams.bottomMargin = tabbarBottomMargin;
1180
1401
  tabbarContainer.setLayoutParams(tabbarContainerParams);
1402
+ fillContainer(tabbarGlassBackdrop);
1403
+ fillContainer(tabbarGlassSurface);
1181
1404
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1182
1405
  tabbarContainer.invalidateOutline();
1406
+ tabbarGlassBackdrop.invalidateOutline();
1407
+ tabbarGlassSurface.invalidateOutline();
1183
1408
  }
1184
1409
  }
1185
1410
 
1186
1411
  if (tabbar != null) {
1412
+ applyTabbarMetrics(tabbar);
1187
1413
  FrameLayout.LayoutParams tabbarParams = new FrameLayout.LayoutParams(
1188
1414
  ViewGroup.LayoutParams.MATCH_PARENT,
1189
1415
  ViewGroup.LayoutParams.MATCH_PARENT
1190
1416
  );
1191
1417
  tabbar.setLayoutParams(tabbarParams);
1192
- tabbar.setPadding(0, 0, 0, 0);
1418
+ tabbar.setPadding(0, isLiquidGlassTabbar() ? dp(LIQUID_GLASS_TABBAR_CONTENT_TOP_PADDING_DP) : 0, 0, 0);
1193
1419
  }
1194
1420
 
1195
1421
  bringChromeToFront();
@@ -1245,14 +1471,15 @@ public class NativeNavigationPlugin extends Plugin {
1245
1471
  }
1246
1472
 
1247
1473
  private JSObject currentInsets() {
1248
- int top = navbarVisible ? statusBarInset() + dp(DEFAULT_NAVBAR_DP) : 0;
1249
- int bottom = tabbarVisible ? navigationBarInset() + dp(DEFAULT_TABBAR_DP) + dp(10) : 0;
1474
+ int navbarHeight = navbarVisible ? statusBarInset() + dp(DEFAULT_NAVBAR_DP) : 0;
1475
+ int top = navbarVisible ? navbarHeight : 0;
1476
+ int bottom = tabbarVisible ? floatingTabbarBottomMargin(navigationBarInset()) + tabbarHeight() : 0;
1250
1477
  JSObject insets = new JSObject();
1251
1478
  insets.put("top", top);
1252
1479
  insets.put("right", 0);
1253
1480
  insets.put("bottom", bottom);
1254
1481
  insets.put("left", 0);
1255
- insets.put("navbarHeight", top);
1482
+ insets.put("navbarHeight", navbarHeight);
1256
1483
  insets.put("tabbarHeight", bottom);
1257
1484
  return insets;
1258
1485
  }
@@ -1307,7 +1534,10 @@ public class NativeNavigationPlugin extends Plugin {
1307
1534
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1308
1535
  WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
1309
1536
  if (insets != null) {
1310
- return insets.getStableInsetBottom();
1537
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1538
+ return insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
1539
+ }
1540
+ return insets.getSystemWindowInsetBottom();
1311
1541
  }
1312
1542
  }
1313
1543
  return systemDimension("navigation_bar_height");
@@ -1322,6 +1552,208 @@ public class NativeNavigationPlugin extends Plugin {
1322
1552
  return Math.round(value * getContext().getResources().getDisplayMetrics().density);
1323
1553
  }
1324
1554
 
1555
+ private float dp(double value) {
1556
+ return (float) (value * getContext().getResources().getDisplayMetrics().density);
1557
+ }
1558
+
1559
+ private static final class GlassOptions {
1560
+
1561
+ private static final String EFFECT_NONE = "none";
1562
+ private static final String EFFECT_LIQUID_GLASS = "liquidGlass";
1563
+ private static final double DEFAULT_BLUR_RADIUS_DP = 18d;
1564
+ private static final double DEFAULT_SURFACE_ALPHA = 0.62d;
1565
+
1566
+ final String effect;
1567
+ final double blurRadiusDp;
1568
+ final double surfaceAlpha;
1569
+
1570
+ GlassOptions(String effect, double blurRadiusDp, double surfaceAlpha) {
1571
+ this.effect = effect;
1572
+ this.blurRadiusDp = Math.max(0d, blurRadiusDp);
1573
+ this.surfaceAlpha = Math.max(0d, Math.min(1d, surfaceAlpha));
1574
+ }
1575
+
1576
+ static GlassOptions defaults() {
1577
+ return new GlassOptions(EFFECT_NONE, DEFAULT_BLUR_RADIUS_DP, DEFAULT_SURFACE_ALPHA);
1578
+ }
1579
+
1580
+ static GlassOptions from(JSObject raw, GlassOptions fallback) {
1581
+ GlassOptions base = fallback == null ? defaults() : fallback;
1582
+ if (raw == null) {
1583
+ return base;
1584
+ }
1585
+
1586
+ String effect = raw.optString("effect", base.effect);
1587
+ if (!EFFECT_NONE.equals(effect) && !EFFECT_LIQUID_GLASS.equals(effect)) {
1588
+ effect = base.effect;
1589
+ }
1590
+ double blurRadiusDp = raw.has("blurRadius") ? raw.optDouble("blurRadius", base.blurRadiusDp) : base.blurRadiusDp;
1591
+ double surfaceAlpha = raw.has("surfaceAlpha") ? raw.optDouble("surfaceAlpha", base.surfaceAlpha) : base.surfaceAlpha;
1592
+ return new GlassOptions(effect, blurRadiusDp, surfaceAlpha);
1593
+ }
1594
+
1595
+ boolean isLiquidGlass() {
1596
+ return EFFECT_LIQUID_GLASS.equals(effect);
1597
+ }
1598
+ }
1599
+
1600
+ private static final class GlassBackdropView extends View {
1601
+
1602
+ private static final long SOURCE_CONTENT_REFRESH_INTERVAL_MS = 250L;
1603
+
1604
+ private final Paint fallbackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
1605
+ private final int[] sourceLocation = new int[2];
1606
+ private final int[] viewLocation = new int[2];
1607
+ private final ViewTreeObserver.OnScrollChangedListener sourceScrollListener = this::markDirty;
1608
+ private final ViewTreeObserver.OnPreDrawListener sourcePreDrawListener = () -> {
1609
+ markDirtyFromSourcePreDraw();
1610
+ return true;
1611
+ };
1612
+ private final View.OnLayoutChangeListener sourceLayoutListener = (
1613
+ view,
1614
+ left,
1615
+ top,
1616
+ right,
1617
+ bottom,
1618
+ oldLeft,
1619
+ oldTop,
1620
+ oldRight,
1621
+ oldBottom
1622
+ ) -> markDirty();
1623
+ private View source;
1624
+ private int fallbackColor = Color.TRANSPARENT;
1625
+ private boolean dirty;
1626
+ private boolean redrawPending;
1627
+ private long lastSourcePreDrawRefreshMs;
1628
+
1629
+ GlassBackdropView(android.content.Context context) {
1630
+ super(context);
1631
+ setWillNotDraw(false);
1632
+ }
1633
+
1634
+ void configure(View source, float blurRadiusPx, int fallbackColor) {
1635
+ this.fallbackColor = fallbackColor;
1636
+ attachSource(source);
1637
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1638
+ Api31RenderEffects.setBlur(this, blurRadiusPx);
1639
+ }
1640
+ markDirty();
1641
+ }
1642
+
1643
+ void clearEffect() {
1644
+ attachSource(null);
1645
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1646
+ Api31RenderEffects.clear(this);
1647
+ }
1648
+ markDirty();
1649
+ }
1650
+
1651
+ private void attachSource(View nextSource) {
1652
+ if (source == nextSource) {
1653
+ return;
1654
+ }
1655
+ if (source != null) {
1656
+ source.removeOnLayoutChangeListener(sourceLayoutListener);
1657
+ ViewTreeObserver observer = source.getViewTreeObserver();
1658
+ if (observer.isAlive()) {
1659
+ observer.removeOnScrollChangedListener(sourceScrollListener);
1660
+ observer.removeOnPreDrawListener(sourcePreDrawListener);
1661
+ }
1662
+ }
1663
+ source = nextSource;
1664
+ if (source != null) {
1665
+ source.addOnLayoutChangeListener(sourceLayoutListener);
1666
+ ViewTreeObserver observer = source.getViewTreeObserver();
1667
+ if (observer.isAlive()) {
1668
+ observer.addOnScrollChangedListener(sourceScrollListener);
1669
+ observer.addOnPreDrawListener(sourcePreDrawListener);
1670
+ }
1671
+ }
1672
+ markDirty();
1673
+ }
1674
+
1675
+ private void markDirty() {
1676
+ dirty = true;
1677
+ scheduleRedrawIfVisible();
1678
+ }
1679
+
1680
+ private void markDirtyFromSourcePreDraw() {
1681
+ dirty = true;
1682
+ long now = System.currentTimeMillis();
1683
+ if (now - lastSourcePreDrawRefreshMs < SOURCE_CONTENT_REFRESH_INTERVAL_MS) {
1684
+ return;
1685
+ }
1686
+ lastSourcePreDrawRefreshMs = now;
1687
+ scheduleRedrawIfVisible();
1688
+ }
1689
+
1690
+ private void scheduleRedrawIfVisible() {
1691
+ if (redrawPending || getVisibility() != View.VISIBLE || !isShown()) {
1692
+ return;
1693
+ }
1694
+ redrawPending = true;
1695
+ postInvalidateOnAnimation();
1696
+ }
1697
+
1698
+ @Override
1699
+ protected void onDraw(Canvas canvas) {
1700
+ super.onDraw(canvas);
1701
+ View currentSource = source;
1702
+ if (currentSource == null || currentSource.getWidth() <= 0 || currentSource.getHeight() <= 0) {
1703
+ fallbackPaint.setColor(fallbackColor);
1704
+ canvas.drawRect(0, 0, getWidth(), getHeight(), fallbackPaint);
1705
+ } else {
1706
+ currentSource.getLocationOnScreen(sourceLocation);
1707
+ getLocationOnScreen(viewLocation);
1708
+ canvas.save();
1709
+ canvas.translate(sourceLocation[0] - viewLocation[0], sourceLocation[1] - viewLocation[1]);
1710
+ currentSource.draw(canvas);
1711
+ canvas.restore();
1712
+ }
1713
+
1714
+ dirty = false;
1715
+ redrawPending = false;
1716
+ }
1717
+
1718
+ @Override
1719
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
1720
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
1721
+ if (width != oldWidth || height != oldHeight) {
1722
+ markDirty();
1723
+ }
1724
+ }
1725
+
1726
+ @Override
1727
+ protected void onVisibilityChanged(View changedView, int visibility) {
1728
+ super.onVisibilityChanged(changedView, visibility);
1729
+ if (visibility != View.VISIBLE) {
1730
+ redrawPending = false;
1731
+ return;
1732
+ }
1733
+ if (dirty) {
1734
+ scheduleRedrawIfVisible();
1735
+ }
1736
+ }
1737
+ }
1738
+
1739
+ @RequiresApi(Build.VERSION_CODES.S)
1740
+ private static final class Api31RenderEffects {
1741
+
1742
+ static void setBlur(View view, float blurRadiusPx) {
1743
+ if (blurRadiusPx <= 0f) {
1744
+ view.setRenderEffect(null);
1745
+ return;
1746
+ }
1747
+ view.setRenderEffect(
1748
+ android.graphics.RenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, android.graphics.Shader.TileMode.CLAMP)
1749
+ );
1750
+ }
1751
+
1752
+ static void clear(View view) {
1753
+ view.setRenderEffect(null);
1754
+ }
1755
+ }
1756
+
1325
1757
  private void runOnUiThread(Runnable runnable) {
1326
1758
  Activity activity = getActivity();
1327
1759
  if (activity == null) {