@capgo/capacitor-native-navigation 8.1.1 → 8.2.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.
@@ -1,7 +1,7 @@
1
1
  package app.capgo.nativenavigation;
2
2
 
3
3
  import android.app.Activity;
4
- import android.content.res.ColorStateList;
4
+ import android.content.Context;
5
5
  import android.content.res.Configuration;
6
6
  import android.content.res.Resources;
7
7
  import android.graphics.Bitmap;
@@ -12,26 +12,25 @@ import android.graphics.Paint;
12
12
  import android.graphics.Path;
13
13
  import android.graphics.Rect;
14
14
  import android.graphics.RectF;
15
+ import android.graphics.Typeface;
15
16
  import android.graphics.drawable.BitmapDrawable;
16
17
  import android.graphics.drawable.ColorDrawable;
17
18
  import android.graphics.drawable.Drawable;
18
19
  import android.graphics.drawable.GradientDrawable;
19
- import android.graphics.drawable.StateListDrawable;
20
20
  import android.os.Build;
21
- import android.view.ContextThemeWrapper;
21
+ import android.util.TypedValue;
22
22
  import android.view.Gravity;
23
23
  import android.view.Menu;
24
24
  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;
29
28
  import android.view.Window;
30
29
  import android.view.WindowInsets;
31
30
  import android.widget.FrameLayout;
32
31
  import android.widget.ImageView;
32
+ import android.widget.LinearLayout;
33
33
  import android.widget.TextView;
34
- import androidx.annotation.RequiresApi;
35
34
  import androidx.appcompat.content.res.AppCompatResources;
36
35
  import androidx.appcompat.widget.Toolbar;
37
36
  import androidx.core.graphics.PathParser;
@@ -41,9 +40,6 @@ import com.getcapacitor.Plugin;
41
40
  import com.getcapacitor.PluginCall;
42
41
  import com.getcapacitor.PluginMethod;
43
42
  import com.getcapacitor.annotation.CapacitorPlugin;
44
- import com.google.android.material.badge.BadgeDrawable;
45
- import com.google.android.material.bottomnavigation.BottomNavigationView;
46
- import com.google.android.material.navigation.NavigationBarView;
47
43
  import java.io.StringReader;
48
44
  import java.net.URLDecoder;
49
45
  import java.util.ArrayDeque;
@@ -62,36 +58,15 @@ import org.xmlpull.v1.XmlPullParserFactory;
62
58
  public class NativeNavigationPlugin extends Plugin {
63
59
 
64
60
  private static final int DEFAULT_NAVBAR_DP = 56;
65
- // Android uses Material bottom navigation metrics; web/iOS keep their own platform-native tab bar heights.
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;
74
- private static final int TABBAR_ICON_DP = 24;
75
- private static final int TABBAR_ITEM_VERTICAL_PADDING_DP = 8;
76
- private static final int LIQUID_GLASS_TABBAR_ITEM_VERTICAL_PADDING_DP = 4;
77
- private static final int TABBAR_INDICATOR_DP = 32;
78
- private static final int LIQUID_GLASS_TABBAR_INDICATOR_DP = 28;
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;
61
+ private static final int DEFAULT_TABBAR_DP = 64;
83
62
  private static final int DEFAULT_TRANSITION_MS = 350;
84
63
  private static final int MENU_ITEM_BASE = 10_000;
85
64
 
86
65
  private final NativeNavigation implementation = new NativeNavigation();
87
66
  private FrameLayout navbarContainer;
88
67
  private FrameLayout tabbarContainer;
89
- private GlassBackdropView navbarGlassBackdrop;
90
- private View navbarGlassSurface;
91
- private GlassBackdropView tabbarGlassBackdrop;
92
- private View tabbarGlassSurface;
93
68
  private Toolbar toolbar;
94
- private BottomNavigationView tabbar;
69
+ private NativeTabbarLayout tabbar;
95
70
  private ImageView transitionSnapshot;
96
71
  private boolean enabled = true;
97
72
  private boolean navbarVisible = false;
@@ -101,13 +76,10 @@ public class NativeNavigationPlugin extends Plugin {
101
76
  private int activeTransitionMs = DEFAULT_TRANSITION_MS;
102
77
  private int tintColor = Color.rgb(0, 122, 255);
103
78
  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
79
  private int tabbarBackgroundColor = Color.argb(235, 255, 255, 255);
80
+ private int badgeBackgroundColor = Color.rgb(255, 59, 48);
81
+ private int badgeTextColor = Color.WHITE;
82
+ private TabbarStyle tabbarStyle = TabbarStyle.defaults(Color.rgb(0, 122, 255));
111
83
  private String activeTransitionId;
112
84
  private String activeTransitionDirection = "forward";
113
85
  private RectF activeZoomSourceFrame;
@@ -117,8 +89,8 @@ public class NativeNavigationPlugin extends Plugin {
117
89
  private final Map<Integer, String> menuActionIds = new HashMap<>();
118
90
  private final Map<Integer, String> menuActionTitles = new HashMap<>();
119
91
  private final Map<Integer, String> menuActionPlacements = new HashMap<>();
120
- private final Map<Integer, String> tabIds = new HashMap<>();
121
- private final Map<Integer, String> tabTitles = new HashMap<>();
92
+ private final List<NativeTabItem> tabItems = new ArrayList<>();
93
+ private int selectedTabIndex = 0;
122
94
 
123
95
  @Override
124
96
  public void load() {
@@ -133,9 +105,6 @@ public class NativeNavigationPlugin extends Plugin {
133
105
  runOnUiThread(() -> {
134
106
  enabled = call.getBoolean("enabled", true);
135
107
  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);
139
108
  Double duration = call.getDouble("animationDuration");
140
109
  if (duration != null) {
141
110
  defaultTransitionMs = Math.max(0, duration.intValue());
@@ -151,12 +120,6 @@ public class NativeNavigationPlugin extends Plugin {
151
120
  tabbarContainer.setVisibility(View.GONE);
152
121
  }
153
122
  }
154
- if (enabled) {
155
- reapplyVisibleChromeBackgrounds();
156
- if (tabbar != null) {
157
- applyTabbarMetrics(tabbar);
158
- }
159
- }
160
123
  updateInsetsAndNotify();
161
124
  call.resolve(insetsResult());
162
125
  });
@@ -204,8 +167,6 @@ public class NativeNavigationPlugin extends Plugin {
204
167
 
205
168
  addToolbarItems(nativeToolbar, call.getArray("rightItems", new JSArray()), "right");
206
169
  JSObject colors = call.getObject("colors", new JSObject());
207
- navbarGlassConfig = call.getObject("glass", null);
208
- navbarGlassOptions = GlassOptions.from(navbarGlassConfig, defaultGlassOptions);
209
170
  applyToolbarColors(nativeToolbar, colors);
210
171
  navbarContainer.setVisibility(View.VISIBLE);
211
172
  layoutChrome();
@@ -238,77 +199,44 @@ public class NativeNavigationPlugin extends Plugin {
238
199
  return;
239
200
  }
240
201
 
241
- BottomNavigationView nativeTabbar = ensureTabbar();
242
- for (Integer existingItemId : new ArrayList<>(tabIds.keySet())) {
243
- nativeTabbar.removeBadge(existingItemId);
244
- }
245
- nativeTabbar.getMenu().clear();
246
- tabIds.clear();
247
- tabTitles.clear();
202
+ NativeTabbarLayout nativeTabbar = ensureTabbar();
203
+ nativeTabbar.removeAllViews();
204
+ tabItems.clear();
205
+ selectedTabIndex = -1;
248
206
 
249
207
  boolean labels = call.getBoolean("labels", true);
250
208
  boolean icons = call.getBoolean("icons", true);
251
209
  String labelVisibilityMode = call.getString("labelVisibilityMode", labels ? "labeled" : "unlabeled");
252
- nativeTabbar.setLabelVisibilityMode(labelVisibilityMode(labelVisibilityMode));
253
-
254
210
  JSONArray tabs = call.getArray("tabs", new JSArray());
255
211
  String selectedId = call.getString("selectedId", null);
256
212
  JSObject colors = call.getObject("colors", new JSObject());
257
- tabbarGlassConfig = call.getObject("glass", null);
258
- tabbarGlassOptions = GlassOptions.from(tabbarGlassConfig, defaultGlassOptions);
259
- Integer badgeBackground = colorOption(call, colors, "badgeBackgroundColor", "badgeBackground", null);
260
- Integer badgeText = colorOption(call, colors, "badgeTextColor", "badgeText", null);
213
+ badgeBackgroundColor = colorOption(call, colors, "badgeBackgroundColor", "badgeBackground", Color.rgb(255, 59, 48));
214
+ badgeTextColor = colorOption(call, colors, "badgeTextColor", "badgeText", Color.WHITE);
215
+
261
216
  for (int index = 0; index < tabs.length(); index++) {
262
217
  JSONObject tab = tabs.optJSONObject(index);
263
218
  if (tab == null) {
264
219
  continue;
265
220
  }
266
- int itemId = MENU_ITEM_BASE + index;
267
221
  String id = tab.optString("id", "tab-" + index);
268
222
  String title = tab.optString("title", "");
269
- MenuItem item = nativeTabbar.getMenu().add(Menu.NONE, itemId, index, labelVisibilityMode.equals("unlabeled") ? "" : title);
270
- item.setEnabled(tab.optBoolean("enabled", true));
271
- Drawable icon = icons ? tabIconFrom(tab) : new ColorDrawable(Color.TRANSPARENT);
272
- if (icon != null) {
273
- item.setIcon(icon);
274
- }
275
- if (tab.has("badge")) {
276
- nativeTabbar.removeBadge(itemId);
277
- BadgeDrawable badge = nativeTabbar.getOrCreateBadge(itemId);
278
- if (isLiquidGlassTabbar()) {
279
- badge.setVerticalOffset(dp(LIQUID_GLASS_TABBAR_BADGE_VERTICAL_OFFSET_DP));
280
- }
281
- if (badgeBackground != null) {
282
- badge.setBackgroundColor(badgeBackground);
283
- }
284
- if (badgeText != null) {
285
- badge.setBadgeTextColor(badgeText);
286
- }
287
- Object badgeValue = tab.opt("badge");
288
- if (badgeValue instanceof Number) {
289
- badge.setNumber(((Number) badgeValue).intValue());
290
- } else {
291
- try {
292
- badge.setNumber(Integer.parseInt(String.valueOf(badgeValue)));
293
- } catch (NumberFormatException ignored) {
294
- badge.setVisible(true);
295
- }
296
- }
297
- } else {
298
- nativeTabbar.removeBadge(itemId);
299
- }
300
- tabIds.put(itemId, id);
301
- tabTitles.put(itemId, title);
223
+ Drawable icon = icons ? iconFrom(tab.optJSONObject("icon")) : new ColorDrawable(Color.TRANSPARENT);
224
+ Drawable selectedIcon = icons ? iconFrom(tab.optJSONObject("selectedIcon")) : null;
225
+ 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)));
302
227
  if (id.equals(selectedId)) {
303
- nativeTabbar.setSelectedItemId(itemId);
228
+ selectedTabIndex = index;
304
229
  }
305
230
  }
306
231
 
307
- if (nativeTabbar.getSelectedItemId() == 0 && nativeTabbar.getMenu().size() > 0) {
308
- nativeTabbar.setSelectedItemId(nativeTabbar.getMenu().getItem(0).getItemId());
232
+ if (!tabItems.isEmpty() && (selectedTabIndex < 0 || selectedTabIndex >= tabItems.size())) {
233
+ selectedTabIndex = 0;
309
234
  }
310
235
 
311
- applyTabbarColors(nativeTabbar, call, colors);
236
+ applyTabbarColors(call, colors);
237
+ tabbarStyle = makeTabbarStyle(call.getObject("style", new JSObject()));
238
+ nativeTabbar.setTabbarStyle(tabbarStyle, tabbarBackgroundColor, centerTabIndex());
239
+ renderTabbarItems(labelVisibilityMode, icons);
312
240
  if (tabbarContainer != null) {
313
241
  tabbarContainer.setVisibility(View.VISIBLE);
314
242
  }
@@ -647,10 +575,6 @@ public class NativeNavigationPlugin extends Plugin {
647
575
  FrameLayout root = contentRoot();
648
576
  navbarContainer = new FrameLayout(getContext());
649
577
  navbarContainer.setElevation(dp(8));
650
- navbarGlassBackdrop = new GlassBackdropView(getContext());
651
- navbarGlassSurface = new View(getContext());
652
- navbarGlassBackdrop.setVisibility(View.GONE);
653
- navbarGlassSurface.setVisibility(View.GONE);
654
578
  toolbar = new Toolbar(getContext());
655
579
  toolbar.setPopupTheme(androidx.appcompat.R.style.ThemeOverlay_AppCompat_Light);
656
580
  toolbar.setOnMenuItemClickListener((item) -> {
@@ -663,8 +587,6 @@ public class NativeNavigationPlugin extends Plugin {
663
587
  return true;
664
588
  });
665
589
 
666
- navbarContainer.addView(navbarGlassBackdrop);
667
- navbarContainer.addView(navbarGlassSurface);
668
590
  navbarContainer.addView(toolbar);
669
591
  if (root != null) {
670
592
  root.addView(navbarContainer);
@@ -677,90 +599,157 @@ public class NativeNavigationPlugin extends Plugin {
677
599
  return toolbar;
678
600
  }
679
601
 
680
- private BottomNavigationView ensureTabbar() {
602
+ private NativeTabbarLayout ensureTabbar() {
681
603
  if (tabbar != null) {
682
604
  return tabbar;
683
605
  }
684
606
  FrameLayout root = contentRoot();
685
607
  tabbarContainer = new FrameLayout(getContext());
686
- tabbarContainer.setElevation(dp(12));
687
608
  tabbarContainer.setClipChildren(false);
688
609
  tabbarContainer.setClipToPadding(false);
689
- tabbarGlassBackdrop = new GlassBackdropView(getContext());
690
- tabbarGlassSurface = new View(getContext());
691
- tabbarGlassBackdrop.setVisibility(View.GONE);
692
- tabbarGlassSurface.setVisibility(View.GONE);
693
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
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);
698
- }
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
- );
710
- tabbar.setElevation(0);
610
+ tabbarContainer.setElevation(dp(12));
611
+
612
+ tabbar = new NativeTabbarLayout(getContext());
711
613
  tabbar.setClipChildren(false);
712
614
  tabbar.setClipToPadding(false);
713
- tabbar.setItemIconSize(dp(TABBAR_ICON_DP));
714
- applyTabbarMetrics(tabbar);
715
615
  tabbar.setBackgroundColor(Color.TRANSPARENT);
716
- tabbar.setOnItemSelectedListener((item) -> {
717
- int itemId = item.getItemId();
718
- if (!tabIds.containsKey(itemId)) {
719
- return false;
720
- }
721
- JSObject event = new JSObject();
722
- event.put("id", tabIds.get(itemId));
723
- event.put("index", itemId - MENU_ITEM_BASE);
724
- event.put("title", tabTitles.get(itemId));
725
- notifyListeners("tabSelect", event);
726
- return true;
727
- });
728
- tabbarContainer.addView(tabbarGlassBackdrop);
729
- tabbarContainer.addView(tabbarGlassSurface);
730
- tabbarContainer.addView(tabbar);
616
+ tabbarContainer.addView(
617
+ tabbar,
618
+ new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
619
+ );
731
620
  if (root != null) {
732
621
  root.addView(tabbarContainer);
733
622
  } else {
734
- getActivity().addContentView(tabbarContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, tabbarHeight()));
623
+ getActivity().addContentView(
624
+ tabbarContainer,
625
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(DEFAULT_TABBAR_DP))
626
+ );
735
627
  }
736
628
  return tabbar;
737
629
  }
738
630
 
739
- private int labelVisibilityMode(String mode) {
740
- if ("auto".equals(mode)) {
741
- return NavigationBarView.LABEL_VISIBILITY_AUTO;
631
+ private void renderTabbarItems(String labelVisibilityMode, boolean icons) {
632
+ if (tabbar == null) {
633
+ return;
742
634
  }
743
- if ("selected".equals(mode)) {
744
- return NavigationBarView.LABEL_VISIBILITY_SELECTED;
635
+ tabbar.removeAllViews();
636
+ int centerIndex = centerTabIndex();
637
+ tabbar.setTabbarStyle(tabbarStyle, tabbarBackgroundColor, centerIndex);
638
+ for (int index = 0; index < tabItems.size(); index++) {
639
+ final int itemIndex = index;
640
+ NativeTabItem item = tabItems.get(index);
641
+ boolean selected = itemIndex == selectedTabIndex;
642
+ boolean showLabel = shouldShowTabLabel(labelVisibilityMode, selected);
643
+ FrameLayout button = makeTabButton(item, selected, showLabel, icons, itemIndex == centerIndex);
644
+ button.setSelected(selected);
645
+ button.setEnabled(item.enabled);
646
+ button.setAlpha(item.enabled ? 1f : 0.38f);
647
+ button.setOnClickListener((view) -> {
648
+ if (!item.enabled) {
649
+ return;
650
+ }
651
+ selectedTabIndex = itemIndex;
652
+ renderTabbarItems(labelVisibilityMode, icons);
653
+ JSObject event = new JSObject();
654
+ event.put("id", item.id);
655
+ event.put("index", itemIndex);
656
+ event.put("title", item.title);
657
+ notifyListeners("tabSelect", event);
658
+ });
659
+ tabbar.addView(button, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
745
660
  }
661
+ }
662
+
663
+ private boolean shouldShowTabLabel(String mode, boolean selected) {
746
664
  if ("unlabeled".equals(mode)) {
747
- return NavigationBarView.LABEL_VISIBILITY_UNLABELED;
665
+ return false;
666
+ }
667
+ if ("selected".equals(mode)) {
668
+ return selected;
748
669
  }
749
- return NavigationBarView.LABEL_VISIBILITY_LABELED;
670
+ return true;
750
671
  }
751
672
 
752
- private Drawable tabIconFrom(JSONObject tab) {
753
- Drawable icon = iconFrom(tab.optJSONObject("icon"));
754
- Drawable selectedIcon = iconFrom(tab.optJSONObject("selectedIcon"));
755
- if (selectedIcon == null) {
756
- return icon;
757
- }
758
- StateListDrawable stateList = new StateListDrawable();
759
- stateList.addState(new int[] { android.R.attr.state_checked }, selectedIcon);
760
- if (icon != null) {
761
- stateList.addState(new int[] {}, icon);
673
+ private FrameLayout makeTabButton(NativeTabItem item, boolean selected, boolean showLabel, boolean icons, boolean center) {
674
+ FrameLayout button = new FrameLayout(getContext());
675
+ button.setClipChildren(false);
676
+ button.setClipToPadding(false);
677
+ button.setForeground(selectableItemBackground());
678
+ if (center) {
679
+ GradientDrawable centerBackground = new GradientDrawable();
680
+ centerBackground.setShape(GradientDrawable.OVAL);
681
+ centerBackground.setColor(tabbarStyle.centerButtonColor);
682
+ button.setBackground(centerBackground);
683
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
684
+ button.setElevation(dp(10));
685
+ }
686
+ }
687
+
688
+ if (!center && selected) {
689
+ GradientDrawable selectedBackground = new GradientDrawable();
690
+ selectedBackground.setShape(GradientDrawable.OVAL);
691
+ selectedBackground.setColor(withAlpha(tintColor, 34));
692
+ View selectedCircle = new View(getContext());
693
+ selectedCircle.setBackground(selectedBackground);
694
+ button.addView(selectedCircle, new FrameLayout.LayoutParams(dp(58), dp(58), Gravity.CENTER));
695
+ }
696
+
697
+ LinearLayout content = new LinearLayout(getContext());
698
+ content.setOrientation(LinearLayout.VERTICAL);
699
+ content.setGravity(Gravity.CENTER);
700
+ content.setPadding(dp(4), dp(4), dp(4), dp(4));
701
+ button.addView(content, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
702
+
703
+ Drawable currentIcon = selected && item.selectedIcon != null ? item.selectedIcon : item.icon;
704
+ boolean labelVisible = center ? showLabel && (currentIcon == null || !icons) : showLabel;
705
+ int itemColor = center ? tabbarStyle.centerButtonIconColor : (selected ? tintColor : inactiveTintColor);
706
+ if (icons && currentIcon != null) {
707
+ Drawable icon = currentIcon.mutate();
708
+ icon.setTint(itemColor);
709
+ ImageView image = new ImageView(getContext());
710
+ image.setImageDrawable(icon);
711
+ image.setColorFilter(itemColor);
712
+ int imageSize = center ? dp(32) : dp(24);
713
+ LinearLayout.LayoutParams imageParams = new LinearLayout.LayoutParams(imageSize, imageSize);
714
+ imageParams.bottomMargin = labelVisible ? dp(2) : 0;
715
+ content.addView(image, imageParams);
716
+ }
717
+
718
+ if (labelVisible) {
719
+ TextView label = new TextView(getContext());
720
+ label.setText(item.title);
721
+ label.setTextColor(itemColor);
722
+ label.setTextSize(12);
723
+ label.setTypeface(Typeface.DEFAULT, selected ? Typeface.BOLD : Typeface.NORMAL);
724
+ label.setGravity(Gravity.CENTER);
725
+ label.setSingleLine(true);
726
+ content.addView(label, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(18)));
727
+ }
728
+
729
+ if (item.badge != null && !item.badge.isEmpty() && !"0".equals(item.badge)) {
730
+ TextView badge = new TextView(getContext());
731
+ badge.setText(item.badge);
732
+ badge.setTextColor(badgeTextColor);
733
+ badge.setTextSize(11);
734
+ badge.setTypeface(Typeface.DEFAULT_BOLD);
735
+ badge.setGravity(Gravity.CENTER);
736
+ GradientDrawable badgeBackground = new GradientDrawable();
737
+ badgeBackground.setColor(badgeBackgroundColor);
738
+ badgeBackground.setCornerRadius(dp(9));
739
+ badge.setBackground(badgeBackground);
740
+ int badgeWidth = Math.max(dp(18), item.badge.length() * dp(7) + dp(10));
741
+ FrameLayout.LayoutParams badgeParams = new FrameLayout.LayoutParams(
742
+ badgeWidth,
743
+ dp(18),
744
+ Gravity.TOP | Gravity.CENTER_HORIZONTAL
745
+ );
746
+ badgeParams.topMargin = center ? 0 : dp(4);
747
+ badge.setTranslationX(center ? dp(18) : dp(16));
748
+ button.addView(badge, badgeParams);
762
749
  }
763
- return stateList;
750
+
751
+ button.setContentDescription(item.title);
752
+ return button;
764
753
  }
765
754
 
766
755
  private Drawable iconFrom(JSONObject descriptor) {
@@ -913,6 +902,235 @@ public class NativeNavigationPlugin extends Plugin {
913
902
  }
914
903
  }
915
904
 
905
+ private static final class NativeTabItem {
906
+
907
+ final String id;
908
+ final String title;
909
+ final Drawable icon;
910
+ final Drawable selectedIcon;
911
+ final String badge;
912
+ final boolean enabled;
913
+
914
+ NativeTabItem(String id, String title, Drawable icon, Drawable selectedIcon, String badge, boolean enabled) {
915
+ this.id = id;
916
+ this.title = title;
917
+ this.icon = icon;
918
+ this.selectedIcon = selectedIcon;
919
+ this.badge = badge;
920
+ this.enabled = enabled;
921
+ }
922
+ }
923
+
924
+ private static final class TabbarStyle {
925
+
926
+ final String shape;
927
+ final int height;
928
+ final int horizontalMargin;
929
+ final int maxWidth;
930
+ final int bottomGap;
931
+ final int cornerRadius;
932
+ final String centerItemId;
933
+ final int centerButtonDiameter;
934
+ final int centerButtonLift;
935
+ final int centerButtonColor;
936
+ final int centerButtonIconColor;
937
+
938
+ TabbarStyle(
939
+ String shape,
940
+ int height,
941
+ int horizontalMargin,
942
+ int maxWidth,
943
+ int bottomGap,
944
+ int cornerRadius,
945
+ String centerItemId,
946
+ int centerButtonDiameter,
947
+ int centerButtonLift,
948
+ int centerButtonColor,
949
+ int centerButtonIconColor
950
+ ) {
951
+ this.shape = shape;
952
+ this.height = height;
953
+ this.horizontalMargin = horizontalMargin;
954
+ this.maxWidth = maxWidth;
955
+ this.bottomGap = bottomGap;
956
+ this.cornerRadius = cornerRadius;
957
+ this.centerItemId = centerItemId;
958
+ this.centerButtonDiameter = centerButtonDiameter;
959
+ this.centerButtonLift = centerButtonLift;
960
+ this.centerButtonColor = centerButtonColor;
961
+ this.centerButtonIconColor = centerButtonIconColor;
962
+ }
963
+
964
+ static TabbarStyle defaults(int tintColor) {
965
+ return new TabbarStyle("floating", 64, 24, 430, 10, 32, null, 76, 38, tintColor, Color.WHITE);
966
+ }
967
+
968
+ boolean isCurve() {
969
+ return "curve".equals(shape);
970
+ }
971
+
972
+ int barTop() {
973
+ return isCurve() ? centerButtonLift : 0;
974
+ }
975
+
976
+ int totalHeight() {
977
+ return height + barTop();
978
+ }
979
+ }
980
+
981
+ private static final class NativeTabbarLayout extends FrameLayout {
982
+
983
+ private final Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
984
+ private TabbarStyle style = TabbarStyle.defaults(Color.rgb(0, 122, 255));
985
+ private int backgroundColor = Color.argb(235, 255, 255, 255);
986
+ private int centerIndex = -1;
987
+
988
+ NativeTabbarLayout(Context context) {
989
+ super(context);
990
+ setWillNotDraw(false);
991
+ }
992
+
993
+ void setTabbarStyle(TabbarStyle style, int backgroundColor, int centerIndex) {
994
+ this.style = style;
995
+ this.backgroundColor = backgroundColor;
996
+ this.centerIndex = centerIndex;
997
+ requestLayout();
998
+ invalidate();
999
+ }
1000
+
1001
+ @Override
1002
+ protected void onDraw(Canvas canvas) {
1003
+ super.onDraw(canvas);
1004
+ backgroundPaint.setColor(backgroundColor);
1005
+ canvas.drawPath(backgroundPath(getWidth(), getHeight()), backgroundPaint);
1006
+ }
1007
+
1008
+ @Override
1009
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1010
+ int width = MeasureSpec.getSize(widthMeasureSpec);
1011
+ int height = MeasureSpec.getSize(heightMeasureSpec);
1012
+ if (hasCenterButton()) {
1013
+ int barHeight = dp(style.height);
1014
+ int barTop = dp(style.barTop());
1015
+ int centerGap = Math.min(dp(style.centerButtonDiameter + 22), Math.round(width * 0.46f));
1016
+ int leftWidth = Math.max(0, Math.round(width / 2f - centerGap / 2f));
1017
+ int rightX = Math.min(width, Math.round(width / 2f + centerGap / 2f));
1018
+ measureRange(0, centerIndex, leftWidth, barHeight);
1019
+ measureChildExact(getChildAt(centerIndex), dp(style.centerButtonDiameter), dp(style.centerButtonDiameter));
1020
+ measureRange(centerIndex + 1, getChildCount(), width - rightX, barHeight);
1021
+ setMeasuredDimension(width, Math.max(height, barTop + barHeight));
1022
+ return;
1023
+ }
1024
+
1025
+ int count = Math.max(getChildCount(), 1);
1026
+ int childWidth = width / count;
1027
+ for (int index = 0; index < getChildCount(); index++) {
1028
+ measureChildExact(getChildAt(index), childWidth, height);
1029
+ }
1030
+ setMeasuredDimension(width, height);
1031
+ }
1032
+
1033
+ @Override
1034
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1035
+ int width = right - left;
1036
+ int height = bottom - top;
1037
+ if (hasCenterButton()) {
1038
+ int barTop = dp(style.barTop());
1039
+ int barHeight = dp(style.height);
1040
+ int buttonDiameter = dp(style.centerButtonDiameter);
1041
+ int centerGap = Math.min(dp(style.centerButtonDiameter + 22), Math.round(width * 0.46f));
1042
+ int leftWidth = Math.max(0, Math.round(width / 2f - centerGap / 2f));
1043
+ int rightX = Math.min(width, Math.round(width / 2f + centerGap / 2f));
1044
+ View center = getChildAt(centerIndex);
1045
+ center.layout((width - buttonDiameter) / 2, 0, (width + buttonDiameter) / 2, buttonDiameter);
1046
+ layoutRange(0, centerIndex, 0, barTop, leftWidth, barHeight);
1047
+ layoutRange(centerIndex + 1, getChildCount(), rightX, barTop, width - rightX, barHeight);
1048
+ return;
1049
+ }
1050
+
1051
+ layoutRange(0, getChildCount(), 0, 0, width, height);
1052
+ }
1053
+
1054
+ private boolean hasCenterButton() {
1055
+ return style.isCurve() && centerIndex >= 0 && centerIndex < getChildCount();
1056
+ }
1057
+
1058
+ private void measureRange(int start, int end, int width, int height) {
1059
+ int count = Math.max(0, end - start);
1060
+ if (count == 0) {
1061
+ return;
1062
+ }
1063
+ int childWidth = Math.max(0, width / count);
1064
+ for (int index = start; index < end; index++) {
1065
+ measureChildExact(getChildAt(index), childWidth, height);
1066
+ }
1067
+ }
1068
+
1069
+ private void layoutRange(int start, int end, int left, int top, int width, int height) {
1070
+ int count = Math.max(0, end - start);
1071
+ if (count == 0) {
1072
+ return;
1073
+ }
1074
+ int childWidth = Math.max(0, width / count);
1075
+ for (int index = start; index < end; index++) {
1076
+ int childLeft = left + (index - start) * childWidth;
1077
+ getChildAt(index).layout(childLeft, top, childLeft + childWidth, top + height);
1078
+ }
1079
+ }
1080
+
1081
+ private void measureChildExact(View child, int width, int height) {
1082
+ child.measure(
1083
+ MeasureSpec.makeMeasureSpec(Math.max(0, width), MeasureSpec.EXACTLY),
1084
+ MeasureSpec.makeMeasureSpec(Math.max(0, height), MeasureSpec.EXACTLY)
1085
+ );
1086
+ }
1087
+
1088
+ private Path backgroundPath(int width, int height) {
1089
+ Path path = new Path();
1090
+ if (!style.isCurve()) {
1091
+ float radius = dp(style.cornerRadius);
1092
+ path.addRoundRect(new RectF(0, 0, width, height), radius, radius, Path.Direction.CW);
1093
+ return path;
1094
+ }
1095
+ float barTop = dp(style.barTop());
1096
+ float barHeight = dp(style.height);
1097
+ float cornerRadius = Math.min(dp(style.cornerRadius), barHeight / 2f);
1098
+ float centerX = width / 2f;
1099
+ float notchRadius = dp(style.centerButtonDiameter) / 2f + dp(7);
1100
+ float notchDepth = Math.min(barHeight * 0.78f, notchRadius);
1101
+ float leftShoulder = Math.max(cornerRadius, centerX - notchRadius);
1102
+ float rightShoulder = Math.min(width - cornerRadius, centerX + notchRadius);
1103
+ float control = notchDepth * 0.55228475f;
1104
+ RectF barRect = new RectF(0, barTop, width, barTop + barHeight);
1105
+
1106
+ path.moveTo(barRect.left + cornerRadius, barRect.top);
1107
+ path.lineTo(leftShoulder, barRect.top);
1108
+ path.cubicTo(
1109
+ leftShoulder,
1110
+ barRect.top + control,
1111
+ centerX - control,
1112
+ barRect.top + notchDepth,
1113
+ centerX,
1114
+ barRect.top + notchDepth
1115
+ );
1116
+ path.cubicTo(centerX + control, barRect.top + notchDepth, rightShoulder, barRect.top + control, rightShoulder, barRect.top);
1117
+ path.lineTo(barRect.right - cornerRadius, barRect.top);
1118
+ path.quadTo(barRect.right, barRect.top, barRect.right, barRect.top + cornerRadius);
1119
+ path.lineTo(barRect.right, barRect.bottom - cornerRadius);
1120
+ path.quadTo(barRect.right, barRect.bottom, barRect.right - cornerRadius, barRect.bottom);
1121
+ path.lineTo(barRect.left + cornerRadius, barRect.bottom);
1122
+ path.quadTo(barRect.left, barRect.bottom, barRect.left, barRect.bottom - cornerRadius);
1123
+ path.lineTo(barRect.left, barRect.top + cornerRadius);
1124
+ path.quadTo(barRect.left, barRect.top, barRect.left + cornerRadius, barRect.top);
1125
+ path.close();
1126
+ return path;
1127
+ }
1128
+
1129
+ private int dp(int value) {
1130
+ return Math.round(value * getResources().getDisplayMetrics().density);
1131
+ }
1132
+ }
1133
+
916
1134
  private static final class SvgIconRenderer {
917
1135
 
918
1136
  private static final Pattern NUMBER_PATTERN = Pattern.compile("[-+]?(?:\\d*\\.\\d+|\\d+\\.?)(?:[eE][-+]?\\d+)?");
@@ -1091,7 +1309,6 @@ public class NativeNavigationPlugin extends Plugin {
1091
1309
  : Color.rgb(20, 24, 32);
1092
1310
  int tint = parseColor(colors.getString("tint", null), tintFallback);
1093
1311
  int background = parseColor(colors.getString("background", null), backgroundFallback);
1094
- navbarBackgroundColor = background;
1095
1312
  int foreground = parseColor(colors.getString("foreground", null), foregroundFallback);
1096
1313
  tintColor = tint;
1097
1314
  nativeToolbar.setTitleTextColor(foreground);
@@ -1102,8 +1319,9 @@ public class NativeNavigationPlugin extends Plugin {
1102
1319
  tintedIcon.setTint(tint);
1103
1320
  nativeToolbar.setNavigationIcon(tintedIcon);
1104
1321
  }
1105
- nativeToolbar.setBackgroundColor(Color.TRANSPARENT);
1106
- applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, background, navbarGlassOptions, 0f);
1322
+ if (navbarContainer != null) {
1323
+ navbarContainer.setBackgroundColor(background);
1324
+ }
1107
1325
  for (int index = 0; index < nativeToolbar.getMenu().size(); index++) {
1108
1326
  Drawable icon = nativeToolbar.getMenu().getItem(index).getIcon();
1109
1327
  if (icon != null) {
@@ -1112,113 +1330,67 @@ public class NativeNavigationPlugin extends Plugin {
1112
1330
  }
1113
1331
  }
1114
1332
 
1115
- private void applyTabbarColors(BottomNavigationView nativeTabbar, PluginCall call, JSObject colors) {
1116
- boolean dynamic = Boolean.TRUE.equals(colors.getBool("dynamic"));
1117
- int tintFallback = dynamic ? dynamicColor("system_accent1_600", tintColor) : tintColor;
1118
- int inactiveFallback = dynamic ? dynamicColor("system_neutral2_600", inactiveTintColor) : inactiveTintColor;
1119
- int backgroundFallback = dynamic
1120
- ? withAlpha(dynamicColor(isNightMode() ? "system_neutral1_900" : "system_neutral1_50", Color.WHITE), 245)
1121
- : Color.argb(235, 255, 255, 255);
1122
- int tint = parseColor(colors.getString("tint", null), tintFallback);
1123
- int inactiveTint = parseColor(colors.getString("inactiveTint", null), inactiveFallback);
1124
- int background = parseColor(colors.getString("background", null), backgroundFallback);
1125
- tabbarBackgroundColor = background;
1126
- tintColor = tint;
1127
- inactiveTintColor = inactiveTint;
1128
- int[][] states = new int[][] { new int[] { android.R.attr.state_checked }, new int[] {} };
1129
- int[] colorState = new int[] { tint, inactiveTint };
1130
- ColorStateList colorStateList = new ColorStateList(states, colorState);
1131
- nativeTabbar.setItemIconTintList(colorStateList);
1132
- nativeTabbar.setItemTextColor(colorStateList);
1133
- nativeTabbar.setItemActiveIndicatorEnabled(!call.getBoolean("disableIndicator", false));
1134
- Integer indicator = colorOption(call, colors, "indicatorColor", "indicator", null);
1135
- nativeTabbar.setItemActiveIndicatorColor(indicator == null ? null : ColorStateList.valueOf(indicator));
1136
- Integer ripple = colorOption(call, colors, "rippleColor", "ripple", null);
1137
- nativeTabbar.setItemRippleColor(ripple == null ? null : ColorStateList.valueOf(ripple));
1138
- nativeTabbar.setBackgroundColor(Color.TRANSPARENT);
1139
- applyTabbarMetrics(nativeTabbar);
1140
- applyChromeBackground(
1141
- tabbarContainer,
1142
- tabbarGlassBackdrop,
1143
- tabbarGlassSurface,
1144
- background,
1145
- tabbarGlassOptions,
1146
- tabbarCornerRadius()
1333
+ private TabbarStyle makeTabbarStyle(JSObject rawStyle) {
1334
+ String requestedShape = rawStyle.getString("shape", "floating");
1335
+ boolean curve = "curve".equalsIgnoreCase(requestedShape);
1336
+ int centerButtonDiameter = Math.max(styleDimension(rawStyle, "centerButtonDiameter", 76), 44);
1337
+ int height = Math.max(styleDimension(rawStyle, "height", curve ? 76 : DEFAULT_TABBAR_DP), 44);
1338
+ int centerButtonLift = Math.max(styleDimension(rawStyle, "centerButtonLift", centerButtonDiameter / 2), 0);
1339
+ int bottomGap = Math.max(styleDimension(rawStyle, "bottomGap", curve ? 0 : 10), 0);
1340
+ int horizontalMargin = Math.max(styleDimension(rawStyle, "horizontalMargin", curve ? 0 : 24), 0);
1341
+ int maxWidth = Math.max(styleDimension(rawStyle, "maxWidth", curve ? 0 : 430), 0);
1342
+ int cornerRadius = Math.max(styleDimension(rawStyle, "cornerRadius", curve ? 24 : height / 2), 0);
1343
+ int centerButtonColor = parseColor(rawStyle.getString("centerButtonColor", null), tintColor);
1344
+ int centerButtonIconColor = parseColor(rawStyle.getString("centerButtonIconColor", null), Color.WHITE);
1345
+ return new TabbarStyle(
1346
+ curve ? "curve" : "floating",
1347
+ height,
1348
+ horizontalMargin,
1349
+ maxWidth,
1350
+ bottomGap,
1351
+ cornerRadius,
1352
+ rawStyle.getString("centerItemId", null),
1353
+ centerButtonDiameter,
1354
+ centerButtonLift,
1355
+ centerButtonColor,
1356
+ centerButtonIconColor
1147
1357
  );
1148
1358
  }
1149
1359
 
1150
- private void reapplyVisibleChromeBackgrounds() {
1151
- if (navbarContainer != null && navbarContainer.getVisibility() == View.VISIBLE) {
1152
- applyChromeBackground(navbarContainer, navbarGlassBackdrop, navbarGlassSurface, navbarBackgroundColor, navbarGlassOptions, 0f);
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);
1360
+ private int styleDimension(JSObject rawStyle, String key, int fallback) {
1361
+ if (rawStyle == null || !rawStyle.has(key)) {
1362
+ return fallback;
1197
1363
  }
1364
+ return (int) Math.round(rawStyle.optDouble(key, fallback));
1198
1365
  }
1199
1366
 
1200
- private void hideGlassBackground(GlassBackdropView backdrop, View surface) {
1201
- if (backdrop != null) {
1202
- backdrop.clearEffect();
1203
- backdrop.setVisibility(View.GONE);
1367
+ private int centerTabIndex() {
1368
+ if (!tabbarStyle.isCurve() || tabItems.isEmpty()) {
1369
+ return -1;
1204
1370
  }
1205
- if (surface != null) {
1206
- surface.setBackground(null);
1207
- surface.setVisibility(View.GONE);
1371
+ if (tabbarStyle.centerItemId != null) {
1372
+ for (int index = 0; index < tabItems.size(); index++) {
1373
+ if (tabbarStyle.centerItemId.equals(tabItems.get(index).id)) {
1374
+ return index;
1375
+ }
1376
+ }
1208
1377
  }
1378
+ return tabItems.size() / 2;
1209
1379
  }
1210
1380
 
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);
1381
+ private void applyTabbarColors(PluginCall call, JSObject colors) {
1382
+ boolean dynamic = Boolean.TRUE.equals(colors.getBool("dynamic"));
1383
+ int tintFallback = dynamic ? dynamicColor("system_accent1_600", tintColor) : tintColor;
1384
+ int inactiveFallback = dynamic ? dynamicColor("system_neutral2_600", inactiveTintColor) : inactiveTintColor;
1385
+ int backgroundFallback = dynamic
1386
+ ? withAlpha(dynamicColor(isNightMode() ? "system_neutral1_900" : "system_neutral1_50", Color.WHITE), 245)
1387
+ : Color.argb(235, 255, 255, 255);
1388
+ tintColor = parseColor(colors.getString("tint", null), tintFallback);
1389
+ inactiveTintColor = parseColor(colors.getString("inactiveTint", null), inactiveFallback);
1390
+ tabbarBackgroundColor = parseColor(colors.getString("background", null), backgroundFallback);
1391
+ if (tabbar != null) {
1392
+ tabbar.setTabbarStyle(tabbarStyle, tabbarBackgroundColor, centerTabIndex());
1216
1393
  }
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));
1222
1394
  }
1223
1395
 
1224
1396
  private int parseColor(String value, int fallback) {
@@ -1279,96 +1451,17 @@ public class NativeNavigationPlugin extends Plugin {
1279
1451
  return Color.argb(Math.max(0, Math.min(255, alpha)), Color.red(color), Color.green(color), Color.blue(color));
1280
1452
  }
1281
1453
 
1454
+ private Drawable selectableItemBackground() {
1455
+ TypedValue outValue = new TypedValue();
1456
+ getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true);
1457
+ return AppCompatResources.getDrawable(getContext(), outValue.resourceId);
1458
+ }
1459
+
1282
1460
  private boolean isNightMode() {
1283
1461
  int mode = getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1284
1462
  return mode == Configuration.UI_MODE_NIGHT_YES;
1285
1463
  }
1286
1464
 
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
-
1372
1465
  private void layoutChrome() {
1373
1466
  FrameLayout root = contentRoot();
1374
1467
  if (root == null) {
@@ -1377,8 +1470,8 @@ public class NativeNavigationPlugin extends Plugin {
1377
1470
  int status = statusBarInset();
1378
1471
  int bottom = navigationBarInset();
1379
1472
  int navbarHeight = navbarVisible ? status + dp(DEFAULT_NAVBAR_DP) : 0;
1380
- int tabbarHeight = tabbarHeight();
1381
- int tabbarBottomMargin = tabbarVisible ? floatingTabbarBottomMargin(bottom) : bottom;
1473
+ int tabbarHeight = dp(tabbarStyle.totalHeight());
1474
+ int tabbarBottomMargin = tabbarVisible ? bottom + dp(tabbarStyle.bottomGap) : bottom;
1382
1475
 
1383
1476
  if (navbarContainer != null) {
1384
1477
  FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(
@@ -1387,8 +1480,6 @@ public class NativeNavigationPlugin extends Plugin {
1387
1480
  Gravity.TOP
1388
1481
  );
1389
1482
  navbarContainer.setLayoutParams(containerParams);
1390
- fillContainer(navbarGlassBackdrop);
1391
- fillContainer(navbarGlassSurface);
1392
1483
  FrameLayout.LayoutParams toolbarParams = new FrameLayout.LayoutParams(
1393
1484
  ViewGroup.LayoutParams.MATCH_PARENT,
1394
1485
  dp(DEFAULT_NAVBAR_DP),
@@ -1400,7 +1491,9 @@ public class NativeNavigationPlugin extends Plugin {
1400
1491
 
1401
1492
  if (tabbarContainer != null) {
1402
1493
  int rootWidth = root.getWidth() > 0 ? root.getWidth() : Resources.getSystem().getDisplayMetrics().widthPixels;
1403
- int tabbarWidth = tabbarWidth(rootWidth);
1494
+ int availableWidth = Math.max(0, rootWidth - dp(tabbarStyle.horizontalMargin) * 2);
1495
+ int maxWidth = tabbarStyle.maxWidth > 0 ? dp(tabbarStyle.maxWidth) : availableWidth;
1496
+ int tabbarWidth = Math.min(availableWidth, maxWidth);
1404
1497
  FrameLayout.LayoutParams tabbarContainerParams = new FrameLayout.LayoutParams(
1405
1498
  tabbarWidth,
1406
1499
  tabbarHeight,
@@ -1408,23 +1501,15 @@ public class NativeNavigationPlugin extends Plugin {
1408
1501
  );
1409
1502
  tabbarContainerParams.bottomMargin = tabbarBottomMargin;
1410
1503
  tabbarContainer.setLayoutParams(tabbarContainerParams);
1411
- fillContainer(tabbarGlassBackdrop);
1412
- fillContainer(tabbarGlassSurface);
1413
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1414
- tabbarContainer.invalidateOutline();
1415
- tabbarGlassBackdrop.invalidateOutline();
1416
- tabbarGlassSurface.invalidateOutline();
1417
- }
1418
1504
  }
1419
1505
 
1420
1506
  if (tabbar != null) {
1421
- applyTabbarMetrics(tabbar);
1422
1507
  FrameLayout.LayoutParams tabbarParams = new FrameLayout.LayoutParams(
1423
1508
  ViewGroup.LayoutParams.MATCH_PARENT,
1424
1509
  ViewGroup.LayoutParams.MATCH_PARENT
1425
1510
  );
1426
1511
  tabbar.setLayoutParams(tabbarParams);
1427
- tabbar.setPadding(0, isLiquidGlassTabbar() ? dp(LIQUID_GLASS_TABBAR_CONTENT_TOP_PADDING_DP) : 0, 0, 0);
1512
+ tabbar.setPadding(0, 0, 0, 0);
1428
1513
  }
1429
1514
 
1430
1515
  bringChromeToFront();
@@ -1480,15 +1565,14 @@ public class NativeNavigationPlugin extends Plugin {
1480
1565
  }
1481
1566
 
1482
1567
  private JSObject currentInsets() {
1483
- int navbarHeight = navbarVisible ? statusBarInset() + dp(DEFAULT_NAVBAR_DP) : 0;
1484
- int top = navbarVisible ? navbarHeight : 0;
1485
- int bottom = tabbarVisible ? floatingTabbarBottomMargin(navigationBarInset()) + tabbarHeight() : 0;
1568
+ int top = navbarVisible ? statusBarInset() + dp(DEFAULT_NAVBAR_DP) : 0;
1569
+ int bottom = tabbarVisible ? navigationBarInset() + dp(tabbarStyle.totalHeight()) + dp(tabbarStyle.bottomGap) : 0;
1486
1570
  JSObject insets = new JSObject();
1487
1571
  insets.put("top", top);
1488
1572
  insets.put("right", 0);
1489
1573
  insets.put("bottom", bottom);
1490
1574
  insets.put("left", 0);
1491
- insets.put("navbarHeight", navbarHeight);
1575
+ insets.put("navbarHeight", top);
1492
1576
  insets.put("tabbarHeight", bottom);
1493
1577
  return insets;
1494
1578
  }
@@ -1589,10 +1673,7 @@ public class NativeNavigationPlugin extends Plugin {
1589
1673
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1590
1674
  WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
1591
1675
  if (insets != null) {
1592
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1593
- return insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
1594
- }
1595
- return insets.getSystemWindowInsetBottom();
1676
+ return insets.getStableInsetBottom();
1596
1677
  }
1597
1678
  }
1598
1679
  return systemDimension("navigation_bar_height");
@@ -1607,208 +1688,6 @@ public class NativeNavigationPlugin extends Plugin {
1607
1688
  return Math.round(value * getContext().getResources().getDisplayMetrics().density);
1608
1689
  }
1609
1690
 
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
-
1812
1691
  private void runOnUiThread(Runnable runnable) {
1813
1692
  Activity activity = getActivity();
1814
1693
  if (activity == null) {