@capgo/capacitor-native-navigation 8.0.24 → 8.1.1

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,10 +101,19 @@ 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;
87
114
  private float activeZoomCornerRadius = 0f;
115
+ private Drawable activeTransitionRootBackground;
116
+ private boolean activeTransitionRootBackgroundCaptured = false;
88
117
  private final Map<Integer, String> menuActionIds = new HashMap<>();
89
118
  private final Map<Integer, String> menuActionTitles = new HashMap<>();
90
119
  private final Map<Integer, String> menuActionPlacements = new HashMap<>();
@@ -104,6 +133,9 @@ public class NativeNavigationPlugin extends Plugin {
104
133
  runOnUiThread(() -> {
105
134
  enabled = call.getBoolean("enabled", true);
106
135
  contentInsetMode = call.getString("contentInsetMode", contentInsetMode);
136
+ defaultGlassOptions = GlassOptions.from(call.getObject("glass", null), defaultGlassOptions);
137
+ navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
138
+ tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
107
139
  Double duration = call.getDouble("animationDuration");
108
140
  if (duration != null) {
109
141
  defaultTransitionMs = Math.max(0, duration.intValue());
@@ -119,6 +151,12 @@ public class NativeNavigationPlugin extends Plugin {
119
151
  tabbarContainer.setVisibility(View.GONE);
120
152
  }
121
153
  }
154
+ if (enabled) {
155
+ reapplyVisibleChromeBackgrounds();
156
+ if (tabbar != null) {
157
+ applyTabbarMetrics(tabbar);
158
+ }
159
+ }
122
160
  updateInsetsAndNotify();
123
161
  call.resolve(insetsResult());
124
162
  });
@@ -166,6 +204,8 @@ public class NativeNavigationPlugin extends Plugin {
166
204
 
167
205
  addToolbarItems(nativeToolbar, call.getArray("rightItems", new JSArray()), "right");
168
206
  JSObject colors = call.getObject("colors", new JSObject());
207
+ navbarGlassConfig = call.getObject("glass", null);
208
+ navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
169
209
  applyToolbarColors(nativeToolbar, colors);
170
210
  navbarContainer.setVisibility(View.VISIBLE);
171
211
  layoutChrome();
@@ -214,6 +254,8 @@ public class NativeNavigationPlugin extends Plugin {
214
254
  JSONArray tabs = call.getArray("tabs", new JSArray());
215
255
  String selectedId = call.getString("selectedId", null);
216
256
  JSObject colors = call.getObject("colors", new JSObject());
257
+ tabbarGlassConfig = call.getObject("glass", null);
258
+ tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
217
259
  Integer badgeBackground = colorOption(call, colors, "badgeBackgroundColor", "badgeBackground", null);
218
260
  Integer badgeText = colorOption(call, colors, "badgeTextColor", "badgeText", null);
219
261
  for (int index = 0; index < tabs.length(); index++) {
@@ -233,6 +275,9 @@ public class NativeNavigationPlugin extends Plugin {
233
275
  if (tab.has("badge")) {
234
276
  nativeTabbar.removeBadge(itemId);
235
277
  BadgeDrawable badge = nativeTabbar.getOrCreateBadge(itemId);
278
+ if (isLiquidGlassTabbar()) {
279
+ badge.setVerticalOffset(dp(LIQUID_GLASS_TABBAR_BADGE_VERTICAL_OFFSET_DP));
280
+ }
236
281
  if (badgeBackground != null) {
237
282
  badge.setBackgroundColor(badgeBackground);
238
283
  }
@@ -295,8 +340,11 @@ public class NativeNavigationPlugin extends Plugin {
295
340
 
296
341
  if (transitionSnapshot != null) {
297
342
  root.removeView(transitionSnapshot);
343
+ restoreTransitionRootBackground();
298
344
  }
299
345
 
346
+ int transitionSurface = transitionSurfaceColor(webView);
347
+ prepareTransitionRootBackground(root, transitionSurface);
300
348
  Bitmap bitmap = Bitmap.createBitmap(webView.getWidth(), webView.getHeight(), Bitmap.Config.ARGB_8888);
301
349
  webView.draw(new Canvas(bitmap));
302
350
  if (zoomSourceRect != null) {
@@ -305,6 +353,7 @@ public class NativeNavigationPlugin extends Plugin {
305
353
  }
306
354
  transitionSnapshot = new ImageView(getContext());
307
355
  transitionSnapshot.setImageBitmap(bitmap);
356
+ transitionSnapshot.setBackgroundColor(transitionSurface);
308
357
  transitionSnapshot.setScaleType(ImageView.ScaleType.FIT_XY);
309
358
  FrameLayout.LayoutParams params =
310
359
  activeZoomSourceFrame == null
@@ -372,8 +421,9 @@ public class NativeNavigationPlugin extends Plugin {
372
421
  snapshotEndTranslation = -width * 0.3f;
373
422
  }
374
423
 
424
+ boolean stationaryTransition = isStationaryTransition(direction);
375
425
  webView.setTranslationX(startTranslation);
376
- webView.setAlpha("none".equals(direction) ? 1f : 0.01f);
426
+ webView.setAlpha(stationaryTransition ? 1f : 0.01f);
377
427
  View snapshot = transitionSnapshot;
378
428
  JSObject event = transitionEvent(transitionId, direction, durationMs);
379
429
  Runnable finish = () -> {
@@ -381,6 +431,7 @@ public class NativeNavigationPlugin extends Plugin {
381
431
  if (root != null && transitionSnapshot != null) {
382
432
  root.removeView(transitionSnapshot);
383
433
  }
434
+ restoreTransitionRootBackground();
384
435
  transitionSnapshot = null;
385
436
  activeTransitionId = null;
386
437
  activeZoomSourceFrame = null;
@@ -400,7 +451,7 @@ public class NativeNavigationPlugin extends Plugin {
400
451
  snapshot
401
452
  .animate()
402
453
  .translationX(snapshotEndTranslation)
403
- .alpha("none".equals(direction) ? 0f : 0.75f)
454
+ .alpha(stationaryTransition ? 0f : 0.75f)
404
455
  .setDuration(durationMs)
405
456
  .withEndAction(finish)
406
457
  .start();
@@ -437,6 +488,7 @@ public class NativeNavigationPlugin extends Plugin {
437
488
  if (root != null && transitionSnapshot != null) {
438
489
  root.removeView(transitionSnapshot);
439
490
  }
491
+ restoreTransitionRootBackground();
440
492
  transitionSnapshot = null;
441
493
  activeTransitionId = null;
442
494
  activeZoomSourceFrame = null;
@@ -595,6 +647,10 @@ public class NativeNavigationPlugin extends Plugin {
595
647
  FrameLayout root = contentRoot();
596
648
  navbarContainer = new FrameLayout(getContext());
597
649
  navbarContainer.setElevation(dp(8));
650
+ navbarGlassBackdrop = new GlassBackdropView(getContext());
651
+ navbarGlassSurface = new View(getContext());
652
+ navbarGlassBackdrop.setVisibility(View.GONE);
653
+ navbarGlassSurface.setVisibility(View.GONE);
598
654
  toolbar = new Toolbar(getContext());
599
655
  toolbar.setPopupTheme(androidx.appcompat.R.style.ThemeOverlay_AppCompat_Light);
600
656
  toolbar.setOnMenuItemClickListener((item) -> {
@@ -607,6 +663,8 @@ public class NativeNavigationPlugin extends Plugin {
607
663
  return true;
608
664
  });
609
665
 
666
+ navbarContainer.addView(navbarGlassBackdrop);
667
+ navbarContainer.addView(navbarGlassSurface);
610
668
  navbarContainer.addView(toolbar);
611
669
  if (root != null) {
612
670
  root.addView(navbarContainer);
@@ -626,26 +684,34 @@ public class NativeNavigationPlugin extends Plugin {
626
684
  FrameLayout root = contentRoot();
627
685
  tabbarContainer = new FrameLayout(getContext());
628
686
  tabbarContainer.setElevation(dp(12));
687
+ tabbarContainer.setClipChildren(false);
688
+ tabbarContainer.setClipToPadding(false);
689
+ tabbarGlassBackdrop = new GlassBackdropView(getContext());
690
+ tabbarGlassSurface = new View(getContext());
691
+ tabbarGlassBackdrop.setVisibility(View.GONE);
692
+ tabbarGlassSurface.setVisibility(View.GONE);
629
693
  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
- }
694
+ ViewOutlineProvider tabbarPillOutlineProvider = new ViewOutlineProvider() {
695
+ @Override
696
+ public void getOutline(View view, Outline outline) {
697
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), view.getHeight() / 2f);
637
698
  }
638
- );
639
- }
640
-
641
- tabbar = new BottomNavigationView(getContext());
699
+ };
700
+ tabbarContainer.setClipToOutline(false);
701
+ tabbarContainer.setOutlineProvider(tabbarPillOutlineProvider);
702
+ tabbarGlassBackdrop.setClipToOutline(true);
703
+ tabbarGlassBackdrop.setOutlineProvider(tabbarPillOutlineProvider);
704
+ tabbarGlassSurface.setClipToOutline(true);
705
+ tabbarGlassSurface.setOutlineProvider(tabbarPillOutlineProvider);
706
+ }
707
+ tabbar = new BottomNavigationView(
708
+ new ContextThemeWrapper(getContext(), com.google.android.material.R.style.Theme_MaterialComponents_DayNight)
709
+ );
642
710
  tabbar.setElevation(0);
643
- tabbar.setMinimumHeight(dp(DEFAULT_TABBAR_DP));
711
+ tabbar.setClipChildren(false);
712
+ tabbar.setClipToPadding(false);
644
713
  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));
714
+ applyTabbarMetrics(tabbar);
649
715
  tabbar.setBackgroundColor(Color.TRANSPARENT);
650
716
  tabbar.setOnItemSelectedListener((item) -> {
651
717
  int itemId = item.getItemId();
@@ -659,14 +725,13 @@ public class NativeNavigationPlugin extends Plugin {
659
725
  notifyListeners("tabSelect", event);
660
726
  return true;
661
727
  });
728
+ tabbarContainer.addView(tabbarGlassBackdrop);
729
+ tabbarContainer.addView(tabbarGlassSurface);
662
730
  tabbarContainer.addView(tabbar);
663
731
  if (root != null) {
664
732
  root.addView(tabbarContainer);
665
733
  } else {
666
- getActivity().addContentView(
667
- tabbarContainer,
668
- new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(DEFAULT_TABBAR_DP))
669
- );
734
+ getActivity().addContentView(tabbarContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, tabbarHeight()));
670
735
  }
671
736
  return tabbar;
672
737
  }
@@ -1026,6 +1091,7 @@ public class NativeNavigationPlugin extends Plugin {
1026
1091
  : Color.rgb(20, 24, 32);
1027
1092
  int tint = parseColor(colors.getString("tint", null), tintFallback);
1028
1093
  int background = parseColor(colors.getString("background", null), backgroundFallback);
1094
+ navbarBackgroundColor = background;
1029
1095
  int foreground = parseColor(colors.getString("foreground", null), foregroundFallback);
1030
1096
  tintColor = tint;
1031
1097
  nativeToolbar.setTitleTextColor(foreground);
@@ -1036,9 +1102,8 @@ public class NativeNavigationPlugin extends Plugin {
1036
1102
  tintedIcon.setTint(tint);
1037
1103
  nativeToolbar.setNavigationIcon(tintedIcon);
1038
1104
  }
1039
- if (navbarContainer != null) {
1040
- navbarContainer.setBackgroundColor(background);
1041
- }
1105
+ nativeToolbar.setBackgroundColor(Color.TRANSPARENT);
1106
+ applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, background, navbarGlassOptions, 0f);
1042
1107
  for (int index = 0; index < nativeToolbar.getMenu().size(); index++) {
1043
1108
  Drawable icon = nativeToolbar.getMenu().getItem(index).getIcon();
1044
1109
  if (icon != null) {
@@ -1057,6 +1122,7 @@ public class NativeNavigationPlugin extends Plugin {
1057
1122
  int tint = parseColor(colors.getString("tint", null), tintFallback);
1058
1123
  int inactiveTint = parseColor(colors.getString("inactiveTint", null), inactiveFallback);
1059
1124
  int background = parseColor(colors.getString("background", null), backgroundFallback);
1125
+ tabbarBackgroundColor = background;
1060
1126
  tintColor = tint;
1061
1127
  inactiveTintColor = inactiveTint;
1062
1128
  int[][] states = new int[][] { new int[] { android.R.attr.state_checked }, new int[] {} };
@@ -1070,12 +1136,89 @@ public class NativeNavigationPlugin extends Plugin {
1070
1136
  Integer ripple = colorOption(call, colors, "rippleColor", "ripple", null);
1071
1137
  nativeTabbar.setItemRippleColor(ripple == null ? null : ColorStateList.valueOf(ripple));
1072
1138
  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);
1139
+ applyTabbarMetrics(nativeTabbar);
1140
+ applyChromeBackground(
1141
+ tabbarContainer,
1142
+ tabbarGlassBackdrop,
1143
+ tabbarGlassSurface,
1144
+ background,
1145
+ tabbarGlassOptions,
1146
+ tabbarCornerRadius()
1147
+ );
1148
+ }
1149
+
1150
+ private void reapplyVisibleChromeBackgrounds() {
1151
+ if (navbarContainer != null && navbarContainer.getVisibility() == View.VISIBLE) {
1152
+ applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, navbarBackgroundColor, navbarGlassOptions, 0f);
1078
1153
  }
1154
+ if (tabbarContainer != null && tabbarContainer.getVisibility() == View.VISIBLE) {
1155
+ applyChromeBackground(
1156
+ tabbarContainer,
1157
+ tabbarGlassBackdrop,
1158
+ tabbarGlassSurface,
1159
+ tabbarBackgroundColor,
1160
+ tabbarGlassOptions,
1161
+ tabbarCornerRadius()
1162
+ );
1163
+ }
1164
+ }
1165
+
1166
+ private void applyChromeBackground(
1167
+ ViewGroup container,
1168
+ GlassBackdropView backdrop,
1169
+ View surface,
1170
+ int background,
1171
+ GlassOptions glassOptions,
1172
+ float cornerRadius
1173
+ ) {
1174
+ if (container == null) {
1175
+ return;
1176
+ }
1177
+ GlassOptions resolvedGlassOptions = glassOptions == null ? GlassOptions.defaults() : glassOptions;
1178
+ if (!resolvedGlassOptions.isLiquidGlass()) {
1179
+ hideGlassBackground(backdrop, surface);
1180
+ container.setBackground(chromeBackgroundDrawable(background, cornerRadius));
1181
+ return;
1182
+ }
1183
+
1184
+ container.setBackgroundColor(Color.TRANSPARENT);
1185
+ int surfaceColor = glassSurfaceColor(background, resolvedGlassOptions);
1186
+ if (surface != null) {
1187
+ surface.setBackground(chromeBackgroundDrawable(surfaceColor, cornerRadius));
1188
+ surface.setVisibility(View.VISIBLE);
1189
+ }
1190
+ if (backdrop != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1191
+ View webView = getBridge() == null ? null : getBridge().getWebView();
1192
+ backdrop.configure(webView, dp(resolvedGlassOptions.blurRadiusDp), surfaceColor);
1193
+ backdrop.setVisibility(View.VISIBLE);
1194
+ } else if (backdrop != null) {
1195
+ backdrop.clearEffect();
1196
+ backdrop.setVisibility(View.GONE);
1197
+ }
1198
+ }
1199
+
1200
+ private void hideGlassBackground(GlassBackdropView backdrop, View surface) {
1201
+ if (backdrop != null) {
1202
+ backdrop.clearEffect();
1203
+ backdrop.setVisibility(View.GONE);
1204
+ }
1205
+ if (surface != null) {
1206
+ surface.setBackground(null);
1207
+ surface.setVisibility(View.GONE);
1208
+ }
1209
+ }
1210
+
1211
+ private Drawable chromeBackgroundDrawable(int color, float cornerRadius) {
1212
+ GradientDrawable drawable = new GradientDrawable();
1213
+ drawable.setColor(color);
1214
+ if (cornerRadius > 0f) {
1215
+ drawable.setCornerRadius(cornerRadius);
1216
+ }
1217
+ return drawable;
1218
+ }
1219
+
1220
+ private int glassSurfaceColor(int background, GlassOptions glassOptions) {
1221
+ return withAlpha(background, Math.round(Color.alpha(background) * (float) glassOptions.surfaceAlpha));
1079
1222
  }
1080
1223
 
1081
1224
  private int parseColor(String value, int fallback) {
@@ -1141,6 +1284,91 @@ public class NativeNavigationPlugin extends Plugin {
1141
1284
  return mode == Configuration.UI_MODE_NIGHT_YES;
1142
1285
  }
1143
1286
 
1287
+ private void fillContainer(View view) {
1288
+ if (view != null) {
1289
+ view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
1290
+ }
1291
+ }
1292
+
1293
+ private boolean isLiquidGlassTabbar() {
1294
+ return tabbarGlassOptions != null && tabbarGlassOptions.isLiquidGlass();
1295
+ }
1296
+
1297
+ private int tabbarHeight() {
1298
+ return dp(isLiquidGlassTabbar() ? LIQUID_GLASS_TABBAR_DP : DEFAULT_TABBAR_DP);
1299
+ }
1300
+
1301
+ private float tabbarCornerRadius() {
1302
+ return tabbarHeight() / 2f;
1303
+ }
1304
+
1305
+ private int tabbarWidth(int rootWidth) {
1306
+ if (isLiquidGlassTabbar()) {
1307
+ return Math.min(Math.max(0, rootWidth - dp(LIQUID_GLASS_TABBAR_SIDE_MARGIN_DP * 2)), dp(LIQUID_GLASS_TABBAR_MAX_WIDTH_DP));
1308
+ }
1309
+ return Math.min(Math.max(0, rootWidth - dp(48)), dp(420));
1310
+ }
1311
+
1312
+ private int floatingTabbarBottomMargin(int navigationInset) {
1313
+ if (isLiquidGlassTabbar()) {
1314
+ return Math.max(dp(LIQUID_GLASS_TABBAR_FLOATING_BOTTOM_GAP_DP), navigationInset);
1315
+ }
1316
+ return navigationInset + dp(TABBAR_FLOATING_BOTTOM_GAP_DP);
1317
+ }
1318
+
1319
+ private void applyTabbarMetrics(BottomNavigationView nativeTabbar) {
1320
+ boolean liquidGlass = isLiquidGlassTabbar();
1321
+ nativeTabbar.setMinimumHeight(tabbarHeight());
1322
+ nativeTabbar.setPadding(0, liquidGlass ? dp(LIQUID_GLASS_TABBAR_CONTENT_TOP_PADDING_DP) : 0, 0, 0);
1323
+ nativeTabbar.setItemPaddingTop(dp(liquidGlass ? LIQUID_GLASS_TABBAR_ITEM_VERTICAL_PADDING_DP : TABBAR_ITEM_VERTICAL_PADDING_DP));
1324
+ nativeTabbar.setItemPaddingBottom(dp(liquidGlass ? LIQUID_GLASS_TABBAR_ITEM_VERTICAL_PADDING_DP : TABBAR_ITEM_VERTICAL_PADDING_DP));
1325
+ nativeTabbar.setItemActiveIndicatorHeight(dp(liquidGlass ? LIQUID_GLASS_TABBAR_INDICATOR_DP : TABBAR_INDICATOR_DP));
1326
+ nativeTabbar.setActiveIndicatorLabelPadding(
1327
+ dp(liquidGlass ? LIQUID_GLASS_TABBAR_INDICATOR_LABEL_PADDING_DP : TABBAR_INDICATOR_LABEL_PADDING_DP)
1328
+ );
1329
+ alignTabbarMenu(nativeTabbar, liquidGlass);
1330
+ nativeTabbar.post(() -> alignTabbarMenu(nativeTabbar, isLiquidGlassTabbar()));
1331
+ }
1332
+
1333
+ private void alignTabbarMenu(BottomNavigationView nativeTabbar, boolean liquidGlass) {
1334
+ nativeTabbar.setClipChildren(false);
1335
+ nativeTabbar.setClipToPadding(false);
1336
+ if (nativeTabbar.getChildCount() == 0) {
1337
+ return;
1338
+ }
1339
+ View menuView = nativeTabbar.getChildAt(0);
1340
+ ViewGroup.LayoutParams currentParams = menuView.getLayoutParams();
1341
+ FrameLayout.LayoutParams menuParams =
1342
+ currentParams instanceof FrameLayout.LayoutParams
1343
+ ? (FrameLayout.LayoutParams) currentParams
1344
+ : new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
1345
+ menuParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
1346
+ menuParams.height = liquidGlass ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT;
1347
+ menuParams.gravity = liquidGlass ? Gravity.CENTER : Gravity.TOP;
1348
+ menuView.setLayoutParams(menuParams);
1349
+ menuView.setTranslationY(liquidGlass ? dp(LIQUID_GLASS_TABBAR_CONTENT_TRANSLATION_Y_DP) : 0f);
1350
+ if (menuView instanceof ViewGroup) {
1351
+ ViewGroup menuGroup = (ViewGroup) menuView;
1352
+ menuGroup.setClipChildren(false);
1353
+ menuGroup.setClipToPadding(false);
1354
+ translateTabbarLabels(menuGroup, liquidGlass ? dp(LIQUID_GLASS_TABBAR_LABEL_TRANSLATION_Y_DP) : 0f);
1355
+ }
1356
+ }
1357
+
1358
+ private void translateTabbarLabels(View view, float translationY) {
1359
+ if (view instanceof TextView) {
1360
+ view.setTranslationY(translationY);
1361
+ return;
1362
+ }
1363
+ if (!(view instanceof ViewGroup)) {
1364
+ return;
1365
+ }
1366
+ ViewGroup group = (ViewGroup) view;
1367
+ for (int index = 0; index < group.getChildCount(); index++) {
1368
+ translateTabbarLabels(group.getChildAt(index), translationY);
1369
+ }
1370
+ }
1371
+
1144
1372
  private void layoutChrome() {
1145
1373
  FrameLayout root = contentRoot();
1146
1374
  if (root == null) {
@@ -1149,8 +1377,8 @@ public class NativeNavigationPlugin extends Plugin {
1149
1377
  int status = statusBarInset();
1150
1378
  int bottom = navigationBarInset();
1151
1379
  int navbarHeight = navbarVisible ? status + dp(DEFAULT_NAVBAR_DP) : 0;
1152
- int tabbarHeight = dp(DEFAULT_TABBAR_DP);
1153
- int tabbarBottomMargin = tabbarVisible ? bottom + dp(10) : bottom;
1380
+ int tabbarHeight = tabbarHeight();
1381
+ int tabbarBottomMargin = tabbarVisible ? floatingTabbarBottomMargin(bottom) : bottom;
1154
1382
 
1155
1383
  if (navbarContainer != null) {
1156
1384
  FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(
@@ -1159,6 +1387,8 @@ public class NativeNavigationPlugin extends Plugin {
1159
1387
  Gravity.TOP
1160
1388
  );
1161
1389
  navbarContainer.setLayoutParams(containerParams);
1390
+ fillContainer(navbarGlassBackdrop);
1391
+ fillContainer(navbarGlassSurface);
1162
1392
  FrameLayout.LayoutParams toolbarParams = new FrameLayout.LayoutParams(
1163
1393
  ViewGroup.LayoutParams.MATCH_PARENT,
1164
1394
  dp(DEFAULT_NAVBAR_DP),
@@ -1170,7 +1400,7 @@ public class NativeNavigationPlugin extends Plugin {
1170
1400
 
1171
1401
  if (tabbarContainer != null) {
1172
1402
  int rootWidth = root.getWidth() > 0 ? root.getWidth() : Resources.getSystem().getDisplayMetrics().widthPixels;
1173
- int tabbarWidth = Math.min(Math.max(0, rootWidth - dp(48)), dp(420));
1403
+ int tabbarWidth = tabbarWidth(rootWidth);
1174
1404
  FrameLayout.LayoutParams tabbarContainerParams = new FrameLayout.LayoutParams(
1175
1405
  tabbarWidth,
1176
1406
  tabbarHeight,
@@ -1178,18 +1408,23 @@ public class NativeNavigationPlugin extends Plugin {
1178
1408
  );
1179
1409
  tabbarContainerParams.bottomMargin = tabbarBottomMargin;
1180
1410
  tabbarContainer.setLayoutParams(tabbarContainerParams);
1411
+ fillContainer(tabbarGlassBackdrop);
1412
+ fillContainer(tabbarGlassSurface);
1181
1413
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1182
1414
  tabbarContainer.invalidateOutline();
1415
+ tabbarGlassBackdrop.invalidateOutline();
1416
+ tabbarGlassSurface.invalidateOutline();
1183
1417
  }
1184
1418
  }
1185
1419
 
1186
1420
  if (tabbar != null) {
1421
+ applyTabbarMetrics(tabbar);
1187
1422
  FrameLayout.LayoutParams tabbarParams = new FrameLayout.LayoutParams(
1188
1423
  ViewGroup.LayoutParams.MATCH_PARENT,
1189
1424
  ViewGroup.LayoutParams.MATCH_PARENT
1190
1425
  );
1191
1426
  tabbar.setLayoutParams(tabbarParams);
1192
- tabbar.setPadding(0, 0, 0, 0);
1427
+ tabbar.setPadding(0, isLiquidGlassTabbar() ? dp(LIQUID_GLASS_TABBAR_CONTENT_TOP_PADDING_DP) : 0, 0, 0);
1193
1428
  }
1194
1429
 
1195
1430
  bringChromeToFront();
@@ -1245,14 +1480,15 @@ public class NativeNavigationPlugin extends Plugin {
1245
1480
  }
1246
1481
 
1247
1482
  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;
1483
+ int navbarHeight = navbarVisible ? statusBarInset() + dp(DEFAULT_NAVBAR_DP) : 0;
1484
+ int top = navbarVisible ? navbarHeight : 0;
1485
+ int bottom = tabbarVisible ? floatingTabbarBottomMargin(navigationBarInset()) + tabbarHeight() : 0;
1250
1486
  JSObject insets = new JSObject();
1251
1487
  insets.put("top", top);
1252
1488
  insets.put("right", 0);
1253
1489
  insets.put("bottom", bottom);
1254
1490
  insets.put("left", 0);
1255
- insets.put("navbarHeight", top);
1491
+ insets.put("navbarHeight", navbarHeight);
1256
1492
  insets.put("tabbarHeight", bottom);
1257
1493
  return insets;
1258
1494
  }
@@ -1271,6 +1507,52 @@ public class NativeNavigationPlugin extends Plugin {
1271
1507
  return event;
1272
1508
  }
1273
1509
 
1510
+ private boolean isStationaryTransition(String direction) {
1511
+ return "tab".equals(direction) || "root".equals(direction) || "none".equals(direction);
1512
+ }
1513
+
1514
+ private void prepareTransitionRootBackground(FrameLayout root, int surfaceColor) {
1515
+ activeTransitionRootBackground = root.getBackground();
1516
+ activeTransitionRootBackgroundCaptured = true;
1517
+ if (needsTransitionSurface(activeTransitionRootBackground)) {
1518
+ root.setBackgroundColor(surfaceColor);
1519
+ }
1520
+ }
1521
+
1522
+ private void restoreTransitionRootBackground() {
1523
+ if (!activeTransitionRootBackgroundCaptured) {
1524
+ return;
1525
+ }
1526
+ FrameLayout root = contentRoot();
1527
+ if (root != null) {
1528
+ root.setBackground(activeTransitionRootBackground);
1529
+ }
1530
+ activeTransitionRootBackground = null;
1531
+ activeTransitionRootBackgroundCaptured = false;
1532
+ }
1533
+
1534
+ private boolean needsTransitionSurface(Drawable background) {
1535
+ if (background == null) {
1536
+ return true;
1537
+ }
1538
+ if (background instanceof ColorDrawable) {
1539
+ return Color.alpha(((ColorDrawable) background).getColor()) < 255;
1540
+ }
1541
+ return false;
1542
+ }
1543
+
1544
+ private int transitionSurfaceColor(View webView) {
1545
+ Drawable background = webView.getBackground();
1546
+ if (background instanceof ColorDrawable) {
1547
+ int color = ((ColorDrawable) background).getColor();
1548
+ if (Color.alpha(color) > 0) {
1549
+ return withAlpha(color, 255);
1550
+ }
1551
+ }
1552
+ int fallback = isNightMode() ? Color.rgb(18, 18, 18) : Color.WHITE;
1553
+ return dynamicColor(isNightMode() ? "system_neutral1_900" : "system_neutral1_50", fallback);
1554
+ }
1555
+
1274
1556
  private FrameLayout contentRoot() {
1275
1557
  Activity activity = getActivity();
1276
1558
  return activity == null ? null : activity.findViewById(android.R.id.content);
@@ -1307,7 +1589,10 @@ public class NativeNavigationPlugin extends Plugin {
1307
1589
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1308
1590
  WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
1309
1591
  if (insets != null) {
1310
- return insets.getStableInsetBottom();
1592
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1593
+ return insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
1594
+ }
1595
+ return insets.getSystemWindowInsetBottom();
1311
1596
  }
1312
1597
  }
1313
1598
  return systemDimension("navigation_bar_height");
@@ -1322,6 +1607,208 @@ public class NativeNavigationPlugin extends Plugin {
1322
1607
  return Math.round(value * getContext().getResources().getDisplayMetrics().density);
1323
1608
  }
1324
1609
 
1610
+ private float dp(double value) {
1611
+ return (float) (value * getContext().getResources().getDisplayMetrics().density);
1612
+ }
1613
+
1614
+ private static final class GlassOptions {
1615
+
1616
+ private static final String EFFECT_NONE = "none";
1617
+ private static final String EFFECT_LIQUID_GLASS = "liquidGlass";
1618
+ private static final double DEFAULT_BLUR_RADIUS_DP = 18d;
1619
+ private static final double DEFAULT_SURFACE_ALPHA = 0.62d;
1620
+
1621
+ final String effect;
1622
+ final double blurRadiusDp;
1623
+ final double surfaceAlpha;
1624
+
1625
+ GlassOptions(String effect, double blurRadiusDp, double surfaceAlpha) {
1626
+ this.effect = effect;
1627
+ this.blurRadiusDp = Math.max(0d, blurRadiusDp);
1628
+ this.surfaceAlpha = Math.max(0d, Math.min(1d, surfaceAlpha));
1629
+ }
1630
+
1631
+ static GlassOptions defaults() {
1632
+ return new GlassOptions(EFFECT_NONE, DEFAULT_BLUR_RADIUS_DP, DEFAULT_SURFACE_ALPHA);
1633
+ }
1634
+
1635
+ static GlassOptions from(JSObject raw, GlassOptions fallback) {
1636
+ GlassOptions base = fallback == null ? defaults() : fallback;
1637
+ if (raw == null) {
1638
+ return base;
1639
+ }
1640
+
1641
+ String effect = raw.optString("effect", base.effect);
1642
+ if (!EFFECT_NONE.equals(effect) && !EFFECT_LIQUID_GLASS.equals(effect)) {
1643
+ effect = base.effect;
1644
+ }
1645
+ double blurRadiusDp = raw.has("blurRadius") ? raw.optDouble("blurRadius", base.blurRadiusDp) : base.blurRadiusDp;
1646
+ double surfaceAlpha = raw.has("surfaceAlpha") ? raw.optDouble("surfaceAlpha", base.surfaceAlpha) : base.surfaceAlpha;
1647
+ return new GlassOptions(effect, blurRadiusDp, surfaceAlpha);
1648
+ }
1649
+
1650
+ boolean isLiquidGlass() {
1651
+ return EFFECT_LIQUID_GLASS.equals(effect);
1652
+ }
1653
+ }
1654
+
1655
+ private static final class GlassBackdropView extends View {
1656
+
1657
+ private static final long SOURCE_CONTENT_REFRESH_INTERVAL_MS = 250L;
1658
+
1659
+ private final Paint fallbackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
1660
+ private final int[] sourceLocation = new int[2];
1661
+ private final int[] viewLocation = new int[2];
1662
+ private final ViewTreeObserver.OnScrollChangedListener sourceScrollListener = this::markDirty;
1663
+ private final ViewTreeObserver.OnPreDrawListener sourcePreDrawListener = () -> {
1664
+ markDirtyFromSourcePreDraw();
1665
+ return true;
1666
+ };
1667
+ private final View.OnLayoutChangeListener sourceLayoutListener = (
1668
+ view,
1669
+ left,
1670
+ top,
1671
+ right,
1672
+ bottom,
1673
+ oldLeft,
1674
+ oldTop,
1675
+ oldRight,
1676
+ oldBottom
1677
+ ) -> markDirty();
1678
+ private View source;
1679
+ private int fallbackColor = Color.TRANSPARENT;
1680
+ private boolean dirty;
1681
+ private boolean redrawPending;
1682
+ private long lastSourcePreDrawRefreshMs;
1683
+
1684
+ GlassBackdropView(android.content.Context context) {
1685
+ super(context);
1686
+ setWillNotDraw(false);
1687
+ }
1688
+
1689
+ void configure(View source, float blurRadiusPx, int fallbackColor) {
1690
+ this.fallbackColor = fallbackColor;
1691
+ attachSource(source);
1692
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1693
+ Api31RenderEffects.setBlur(this, blurRadiusPx);
1694
+ }
1695
+ markDirty();
1696
+ }
1697
+
1698
+ void clearEffect() {
1699
+ attachSource(null);
1700
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1701
+ Api31RenderEffects.clear(this);
1702
+ }
1703
+ markDirty();
1704
+ }
1705
+
1706
+ private void attachSource(View nextSource) {
1707
+ if (source == nextSource) {
1708
+ return;
1709
+ }
1710
+ if (source != null) {
1711
+ source.removeOnLayoutChangeListener(sourceLayoutListener);
1712
+ ViewTreeObserver observer = source.getViewTreeObserver();
1713
+ if (observer.isAlive()) {
1714
+ observer.removeOnScrollChangedListener(sourceScrollListener);
1715
+ observer.removeOnPreDrawListener(sourcePreDrawListener);
1716
+ }
1717
+ }
1718
+ source = nextSource;
1719
+ if (source != null) {
1720
+ source.addOnLayoutChangeListener(sourceLayoutListener);
1721
+ ViewTreeObserver observer = source.getViewTreeObserver();
1722
+ if (observer.isAlive()) {
1723
+ observer.addOnScrollChangedListener(sourceScrollListener);
1724
+ observer.addOnPreDrawListener(sourcePreDrawListener);
1725
+ }
1726
+ }
1727
+ markDirty();
1728
+ }
1729
+
1730
+ private void markDirty() {
1731
+ dirty = true;
1732
+ scheduleRedrawIfVisible();
1733
+ }
1734
+
1735
+ private void markDirtyFromSourcePreDraw() {
1736
+ dirty = true;
1737
+ long now = System.currentTimeMillis();
1738
+ if (now - lastSourcePreDrawRefreshMs < SOURCE_CONTENT_REFRESH_INTERVAL_MS) {
1739
+ return;
1740
+ }
1741
+ lastSourcePreDrawRefreshMs = now;
1742
+ scheduleRedrawIfVisible();
1743
+ }
1744
+
1745
+ private void scheduleRedrawIfVisible() {
1746
+ if (redrawPending || getVisibility() != View.VISIBLE || !isShown()) {
1747
+ return;
1748
+ }
1749
+ redrawPending = true;
1750
+ postInvalidateOnAnimation();
1751
+ }
1752
+
1753
+ @Override
1754
+ protected void onDraw(Canvas canvas) {
1755
+ super.onDraw(canvas);
1756
+ View currentSource = source;
1757
+ if (currentSource == null || currentSource.getWidth() <= 0 || currentSource.getHeight() <= 0) {
1758
+ fallbackPaint.setColor(fallbackColor);
1759
+ canvas.drawRect(0, 0, getWidth(), getHeight(), fallbackPaint);
1760
+ } else {
1761
+ currentSource.getLocationOnScreen(sourceLocation);
1762
+ getLocationOnScreen(viewLocation);
1763
+ canvas.save();
1764
+ canvas.translate(sourceLocation[0] - viewLocation[0], sourceLocation[1] - viewLocation[1]);
1765
+ currentSource.draw(canvas);
1766
+ canvas.restore();
1767
+ }
1768
+
1769
+ dirty = false;
1770
+ redrawPending = false;
1771
+ }
1772
+
1773
+ @Override
1774
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
1775
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
1776
+ if (width != oldWidth || height != oldHeight) {
1777
+ markDirty();
1778
+ }
1779
+ }
1780
+
1781
+ @Override
1782
+ protected void onVisibilityChanged(View changedView, int visibility) {
1783
+ super.onVisibilityChanged(changedView, visibility);
1784
+ if (visibility != View.VISIBLE) {
1785
+ redrawPending = false;
1786
+ return;
1787
+ }
1788
+ if (dirty) {
1789
+ scheduleRedrawIfVisible();
1790
+ }
1791
+ }
1792
+ }
1793
+
1794
+ @RequiresApi(Build.VERSION_CODES.S)
1795
+ private static final class Api31RenderEffects {
1796
+
1797
+ static void setBlur(View view, float blurRadiusPx) {
1798
+ if (blurRadiusPx <= 0f) {
1799
+ view.setRenderEffect(null);
1800
+ return;
1801
+ }
1802
+ view.setRenderEffect(
1803
+ android.graphics.RenderEffect.createBlurEffect(blurRadiusPx, blurRadiusPx, android.graphics.Shader.TileMode.CLAMP)
1804
+ );
1805
+ }
1806
+
1807
+ static void clear(View view) {
1808
+ view.setRenderEffect(null);
1809
+ }
1810
+ }
1811
+
1325
1812
  private void runOnUiThread(Runnable runnable) {
1326
1813
  Activity activity = getActivity();
1327
1814
  if (activity == null) {