@capgo/capacitor-native-navigation 8.0.9

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.
Files changed (36) hide show
  1. package/CapgoNativeNavigation.podspec +17 -0
  2. package/LICENSE +373 -0
  3. package/Package.swift +28 -0
  4. package/README.md +858 -0
  5. package/android/build.gradle +61 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/app/capgo/nativenavigation/NativeNavigation.java +8 -0
  8. package/android/src/main/java/app/capgo/nativenavigation/NativeNavigationPlugin.java +1322 -0
  9. package/android/src/main/res/.gitkeep +0 -0
  10. package/dist/docs.json +1369 -0
  11. package/dist/esm/components.d.ts +1 -0
  12. package/dist/esm/components.js +159 -0
  13. package/dist/esm/components.js.map +1 -0
  14. package/dist/esm/definitions.d.ts +470 -0
  15. package/dist/esm/definitions.js +2 -0
  16. package/dist/esm/definitions.js.map +1 -0
  17. package/dist/esm/index.d.ts +19 -0
  18. package/dist/esm/index.js +40 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/plugin.d.ts +2 -0
  21. package/dist/esm/plugin.js +2 -0
  22. package/dist/esm/plugin.js.map +1 -0
  23. package/dist/esm/web.d.ts +17 -0
  24. package/dist/esm/web.js +90 -0
  25. package/dist/esm/web.js.map +1 -0
  26. package/dist/plugin.cjs.js +310 -0
  27. package/dist/plugin.cjs.js.map +1 -0
  28. package/dist/plugin.js +313 -0
  29. package/dist/plugin.js.map +1 -0
  30. package/docs/demo-navigation.webp +0 -0
  31. package/docs/demo-options.webp +0 -0
  32. package/docs/demo-svg-icons.webp +0 -0
  33. package/ios/Sources/NativeNavigationPlugin/NativeNavigation.swift +7 -0
  34. package/ios/Sources/NativeNavigationPlugin/NativeNavigationPlugin.swift +1580 -0
  35. package/ios/Tests/NativeNavigationPluginTests/NativeNavigationTests.swift +19 -0
  36. package/package.json +91 -0
@@ -0,0 +1,1322 @@
1
+ package app.capgo.nativenavigation;
2
+
3
+ import android.app.Activity;
4
+ import android.content.res.ColorStateList;
5
+ import android.content.res.Configuration;
6
+ import android.content.res.Resources;
7
+ import android.graphics.Bitmap;
8
+ import android.graphics.Canvas;
9
+ import android.graphics.Color;
10
+ import android.graphics.Outline;
11
+ import android.graphics.Paint;
12
+ import android.graphics.Path;
13
+ import android.graphics.Rect;
14
+ import android.graphics.RectF;
15
+ import android.graphics.drawable.BitmapDrawable;
16
+ import android.graphics.drawable.ColorDrawable;
17
+ import android.graphics.drawable.Drawable;
18
+ import android.graphics.drawable.GradientDrawable;
19
+ import android.graphics.drawable.StateListDrawable;
20
+ import android.os.Build;
21
+ import android.view.Gravity;
22
+ import android.view.Menu;
23
+ import android.view.MenuItem;
24
+ import android.view.View;
25
+ import android.view.ViewGroup;
26
+ import android.view.ViewOutlineProvider;
27
+ import android.view.Window;
28
+ import android.view.WindowInsets;
29
+ import android.widget.FrameLayout;
30
+ import android.widget.ImageView;
31
+ import androidx.appcompat.content.res.AppCompatResources;
32
+ import androidx.appcompat.widget.Toolbar;
33
+ import androidx.core.graphics.PathParser;
34
+ import com.getcapacitor.JSArray;
35
+ import com.getcapacitor.JSObject;
36
+ import com.getcapacitor.Plugin;
37
+ import com.getcapacitor.PluginCall;
38
+ import com.getcapacitor.PluginMethod;
39
+ import com.getcapacitor.annotation.CapacitorPlugin;
40
+ import com.google.android.material.badge.BadgeDrawable;
41
+ import com.google.android.material.bottomnavigation.BottomNavigationView;
42
+ import com.google.android.material.navigation.NavigationBarView;
43
+ import java.io.StringReader;
44
+ import java.net.URLDecoder;
45
+ import java.util.ArrayDeque;
46
+ import java.util.ArrayList;
47
+ import java.util.HashMap;
48
+ import java.util.List;
49
+ import java.util.Map;
50
+ import java.util.regex.Matcher;
51
+ import java.util.regex.Pattern;
52
+ import org.json.JSONArray;
53
+ import org.json.JSONObject;
54
+ import org.xmlpull.v1.XmlPullParser;
55
+ import org.xmlpull.v1.XmlPullParserFactory;
56
+
57
+ @CapacitorPlugin(name = "NativeNavigation")
58
+ public class NativeNavigationPlugin extends Plugin {
59
+
60
+ private static final int DEFAULT_NAVBAR_DP = 56;
61
+ private static final int DEFAULT_TABBAR_DP = 64;
62
+ private static final int DEFAULT_TRANSITION_MS = 350;
63
+ private static final int MENU_ITEM_BASE = 10_000;
64
+
65
+ private final NativeNavigation implementation = new NativeNavigation();
66
+ private FrameLayout navbarContainer;
67
+ private FrameLayout tabbarContainer;
68
+ private Toolbar toolbar;
69
+ private BottomNavigationView tabbar;
70
+ private ImageView transitionSnapshot;
71
+ private boolean enabled = true;
72
+ private boolean navbarVisible = false;
73
+ private boolean tabbarVisible = false;
74
+ private String contentInsetMode = "css";
75
+ private int defaultTransitionMs = DEFAULT_TRANSITION_MS;
76
+ private int activeTransitionMs = DEFAULT_TRANSITION_MS;
77
+ private int tintColor = Color.rgb(0, 122, 255);
78
+ private int inactiveTintColor = Color.rgb(120, 126, 137);
79
+ private String activeTransitionId;
80
+ private String activeTransitionDirection = "forward";
81
+ private RectF activeZoomSourceFrame;
82
+ private float activeZoomCornerRadius = 0f;
83
+ private final Map<Integer, String> menuActionIds = new HashMap<>();
84
+ private final Map<Integer, String> menuActionTitles = new HashMap<>();
85
+ private final Map<Integer, String> menuActionPlacements = new HashMap<>();
86
+ private final Map<Integer, String> tabIds = new HashMap<>();
87
+ private final Map<Integer, String> tabTitles = new HashMap<>();
88
+
89
+ @Override
90
+ public void load() {
91
+ Activity activity = getActivity();
92
+ if (activity != null) {
93
+ activity.runOnUiThread(this::enableEdgeToEdge);
94
+ }
95
+ }
96
+
97
+ @PluginMethod
98
+ public void configure(PluginCall call) {
99
+ runOnUiThread(() -> {
100
+ enabled = call.getBoolean("enabled", true);
101
+ contentInsetMode = call.getString("contentInsetMode", contentInsetMode);
102
+ Double duration = call.getDouble("animationDuration");
103
+ if (duration != null) {
104
+ defaultTransitionMs = Math.max(0, duration.intValue());
105
+ }
106
+ if (!enabled) {
107
+ if (navbarContainer != null) {
108
+ navbarContainer.setVisibility(View.GONE);
109
+ }
110
+ if (tabbar != null) {
111
+ tabbar.setVisibility(View.GONE);
112
+ }
113
+ if (tabbarContainer != null) {
114
+ tabbarContainer.setVisibility(View.GONE);
115
+ }
116
+ }
117
+ updateInsetsAndNotify();
118
+ call.resolve(insetsResult());
119
+ });
120
+ }
121
+
122
+ @PluginMethod
123
+ public void setNavbar(PluginCall call) {
124
+ runOnUiThread(() -> {
125
+ if (!enabled) {
126
+ navbarVisible = false;
127
+ updateInsetsAndNotify();
128
+ call.resolve(insetsResult());
129
+ return;
130
+ }
131
+
132
+ boolean hidden = call.getBoolean("hidden", false);
133
+ navbarVisible = !hidden;
134
+ if (hidden) {
135
+ if (navbarContainer != null) {
136
+ navbarContainer.setVisibility(View.GONE);
137
+ }
138
+ updateInsetsAndNotify();
139
+ call.resolve(insetsResult());
140
+ return;
141
+ }
142
+
143
+ Toolbar nativeToolbar = ensureToolbar();
144
+ nativeToolbar.setTitle(call.getString("title", ""));
145
+ nativeToolbar.setSubtitle(call.getString("subtitle", null));
146
+ nativeToolbar.getMenu().clear();
147
+ menuActionIds.clear();
148
+ menuActionTitles.clear();
149
+ menuActionPlacements.clear();
150
+
151
+ JSObject backButton = call.getObject("backButton", null);
152
+ if (backButton != null && Boolean.TRUE.equals(backButton.getBool("visible"))) {
153
+ nativeToolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material);
154
+ nativeToolbar.setNavigationContentDescription(backButton.getString("title", "Back"));
155
+ nativeToolbar.setNavigationOnClickListener((v) -> notifyListeners("navbarBack", new JSObject().put("source", "navbar")));
156
+ } else {
157
+ nativeToolbar.setNavigationIcon(null);
158
+ nativeToolbar.setNavigationOnClickListener(null);
159
+ addToolbarItems(nativeToolbar, call.getArray("leftItems", new JSArray()), "left");
160
+ }
161
+
162
+ addToolbarItems(nativeToolbar, call.getArray("rightItems", new JSArray()), "right");
163
+ JSObject colors = call.getObject("colors", new JSObject());
164
+ applyToolbarColors(nativeToolbar, colors);
165
+ navbarContainer.setVisibility(View.VISIBLE);
166
+ layoutChrome();
167
+ updateInsetsAndNotify();
168
+ call.resolve(insetsResult());
169
+ });
170
+ }
171
+
172
+ @PluginMethod
173
+ public void setTabbar(PluginCall call) {
174
+ runOnUiThread(() -> {
175
+ if (!enabled) {
176
+ tabbarVisible = false;
177
+ updateInsetsAndNotify();
178
+ call.resolve(insetsResult());
179
+ return;
180
+ }
181
+
182
+ boolean hidden = call.getBoolean("hidden", false);
183
+ tabbarVisible = !hidden;
184
+ if (hidden) {
185
+ if (tabbar != null) {
186
+ tabbar.setVisibility(View.GONE);
187
+ }
188
+ if (tabbarContainer != null) {
189
+ tabbarContainer.setVisibility(View.GONE);
190
+ }
191
+ updateInsetsAndNotify();
192
+ call.resolve(insetsResult());
193
+ return;
194
+ }
195
+
196
+ BottomNavigationView nativeTabbar = ensureTabbar();
197
+ for (Integer existingItemId : new ArrayList<>(tabIds.keySet())) {
198
+ nativeTabbar.removeBadge(existingItemId);
199
+ }
200
+ nativeTabbar.getMenu().clear();
201
+ tabIds.clear();
202
+ tabTitles.clear();
203
+
204
+ boolean labels = call.getBoolean("labels", true);
205
+ boolean icons = call.getBoolean("icons", true);
206
+ String labelVisibilityMode = call.getString("labelVisibilityMode", labels ? "labeled" : "unlabeled");
207
+ nativeTabbar.setLabelVisibilityMode(labelVisibilityMode(labelVisibilityMode));
208
+
209
+ JSONArray tabs = call.getArray("tabs", new JSArray());
210
+ String selectedId = call.getString("selectedId", null);
211
+ JSObject colors = call.getObject("colors", new JSObject());
212
+ Integer badgeBackground = colorOption(call, colors, "badgeBackgroundColor", "badgeBackground", null);
213
+ Integer badgeText = colorOption(call, colors, "badgeTextColor", "badgeText", null);
214
+ for (int index = 0; index < tabs.length(); index++) {
215
+ JSONObject tab = tabs.optJSONObject(index);
216
+ if (tab == null) {
217
+ continue;
218
+ }
219
+ int itemId = MENU_ITEM_BASE + index;
220
+ String id = tab.optString("id", "tab-" + index);
221
+ String title = tab.optString("title", "");
222
+ MenuItem item = nativeTabbar.getMenu().add(Menu.NONE, itemId, index, labelVisibilityMode.equals("unlabeled") ? "" : title);
223
+ item.setEnabled(tab.optBoolean("enabled", true));
224
+ Drawable icon = icons ? tabIconFrom(tab) : new ColorDrawable(Color.TRANSPARENT);
225
+ if (icon != null) {
226
+ item.setIcon(icon);
227
+ }
228
+ if (tab.has("badge")) {
229
+ nativeTabbar.removeBadge(itemId);
230
+ BadgeDrawable badge = nativeTabbar.getOrCreateBadge(itemId);
231
+ if (badgeBackground != null) {
232
+ badge.setBackgroundColor(badgeBackground);
233
+ }
234
+ if (badgeText != null) {
235
+ badge.setBadgeTextColor(badgeText);
236
+ }
237
+ Object badgeValue = tab.opt("badge");
238
+ if (badgeValue instanceof Number) {
239
+ badge.setNumber(((Number) badgeValue).intValue());
240
+ } else {
241
+ try {
242
+ badge.setNumber(Integer.parseInt(String.valueOf(badgeValue)));
243
+ } catch (NumberFormatException ignored) {
244
+ badge.setVisible(true);
245
+ }
246
+ }
247
+ } else {
248
+ nativeTabbar.removeBadge(itemId);
249
+ }
250
+ tabIds.put(itemId, id);
251
+ tabTitles.put(itemId, title);
252
+ if (id.equals(selectedId)) {
253
+ nativeTabbar.setSelectedItemId(itemId);
254
+ }
255
+ }
256
+
257
+ if (nativeTabbar.getSelectedItemId() == 0 && nativeTabbar.getMenu().size() > 0) {
258
+ nativeTabbar.setSelectedItemId(nativeTabbar.getMenu().getItem(0).getItemId());
259
+ }
260
+
261
+ applyTabbarColors(nativeTabbar, call, colors);
262
+ if (tabbarContainer != null) {
263
+ tabbarContainer.setVisibility(View.VISIBLE);
264
+ }
265
+ nativeTabbar.setVisibility(View.VISIBLE);
266
+ layoutChrome();
267
+ updateInsetsAndNotify();
268
+ call.resolve(insetsResult());
269
+ });
270
+ }
271
+
272
+ @PluginMethod
273
+ public void beginTransition(PluginCall call) {
274
+ runOnUiThread(() -> {
275
+ View webView = getBridge().getWebView();
276
+ FrameLayout root = contentRoot();
277
+ if (webView == null || root == null || webView.getWidth() <= 0 || webView.getHeight() <= 0) {
278
+ call.reject("WebView unavailable");
279
+ return;
280
+ }
281
+
282
+ activeTransitionId = call.getString("id", "transition-" + System.currentTimeMillis());
283
+ activeTransitionDirection = call.getString("direction", "forward");
284
+ Double duration = call.getDouble("duration");
285
+ activeTransitionMs = duration == null ? defaultTransitionMs : Math.max(0, duration.intValue());
286
+ RectF zoomSourceRect = "zoom".equals(activeTransitionDirection) ? transitionRect(call.getObject("sourceRect", null)) : null;
287
+ activeZoomSourceFrame = zoomSourceRect == null ? null : rootFrame(zoomSourceRect, webView);
288
+ Double cornerRadius = call.getDouble("cornerRadius");
289
+ activeZoomCornerRadius = cornerRadius == null ? 0f : cornerRadius.floatValue();
290
+
291
+ if (transitionSnapshot != null) {
292
+ root.removeView(transitionSnapshot);
293
+ }
294
+
295
+ Bitmap bitmap = Bitmap.createBitmap(webView.getWidth(), webView.getHeight(), Bitmap.Config.ARGB_8888);
296
+ webView.draw(new Canvas(bitmap));
297
+ if (zoomSourceRect != null) {
298
+ Rect crop = bitmapCropRect(zoomSourceRect, bitmap);
299
+ bitmap = Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height());
300
+ }
301
+ transitionSnapshot = new ImageView(getContext());
302
+ transitionSnapshot.setImageBitmap(bitmap);
303
+ transitionSnapshot.setScaleType(ImageView.ScaleType.FIT_XY);
304
+ FrameLayout.LayoutParams params =
305
+ activeZoomSourceFrame == null
306
+ ? new FrameLayout.LayoutParams(webView.getWidth(), webView.getHeight())
307
+ : new FrameLayout.LayoutParams(Math.round(activeZoomSourceFrame.width()), Math.round(activeZoomSourceFrame.height()));
308
+ params.leftMargin = activeZoomSourceFrame == null ? webView.getLeft() : Math.round(activeZoomSourceFrame.left);
309
+ params.topMargin = activeZoomSourceFrame == null ? webView.getTop() : Math.round(activeZoomSourceFrame.top);
310
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && activeZoomCornerRadius > 0) {
311
+ transitionSnapshot.setClipToOutline(true);
312
+ transitionSnapshot.setOutlineProvider(roundRectOutlineProvider(activeZoomCornerRadius));
313
+ }
314
+ root.addView(transitionSnapshot, params);
315
+ webView.setAlpha(0.01f);
316
+ bringChromeToFront();
317
+
318
+ JSObject event = transitionEvent(activeTransitionId, activeTransitionDirection, activeTransitionMs);
319
+ notifyListeners("transitionStart", event);
320
+ call.resolve(event);
321
+ });
322
+ }
323
+
324
+ @PluginMethod
325
+ public void finishTransition(PluginCall call) {
326
+ runOnUiThread(() -> {
327
+ View webView = getBridge().getWebView();
328
+ if (webView == null) {
329
+ call.reject("WebView unavailable");
330
+ return;
331
+ }
332
+
333
+ String transitionId = call.getString(
334
+ "id",
335
+ activeTransitionId == null ? "transition-" + System.currentTimeMillis() : activeTransitionId
336
+ );
337
+ String direction = call.getString("direction", activeTransitionDirection);
338
+ Double duration = call.getDouble("duration");
339
+ int durationMs = duration == null ? activeTransitionMs : Math.max(0, duration.intValue());
340
+ float width = webView.getWidth();
341
+ if ("zoom".equals(direction)) {
342
+ RectF sourceRect = transitionRect(call.getObject("sourceRect", null));
343
+ RectF targetRect = transitionRect(call.getObject("targetRect", null));
344
+ Double cornerRadius = call.getDouble("cornerRadius");
345
+ finishZoomTransition(
346
+ webView,
347
+ transitionSnapshot,
348
+ transitionId,
349
+ durationMs,
350
+ sourceRect == null ? null : rootFrame(sourceRect, webView),
351
+ targetRect == null ? null : rootFrame(targetRect, webView),
352
+ cornerRadius == null ? activeZoomCornerRadius : cornerRadius.floatValue(),
353
+ call
354
+ );
355
+ return;
356
+ }
357
+ float startTranslation;
358
+ float snapshotEndTranslation;
359
+ if ("back".equals(direction)) {
360
+ startTranslation = -width * 0.3f;
361
+ snapshotEndTranslation = width;
362
+ } else if ("tab".equals(direction) || "root".equals(direction) || "none".equals(direction)) {
363
+ startTranslation = 0;
364
+ snapshotEndTranslation = 0;
365
+ } else {
366
+ startTranslation = width;
367
+ snapshotEndTranslation = -width * 0.3f;
368
+ }
369
+
370
+ webView.setTranslationX(startTranslation);
371
+ webView.setAlpha("none".equals(direction) ? 1f : 0.01f);
372
+ View snapshot = transitionSnapshot;
373
+ JSObject event = transitionEvent(transitionId, direction, durationMs);
374
+ Runnable finish = () -> {
375
+ FrameLayout root = contentRoot();
376
+ if (root != null && transitionSnapshot != null) {
377
+ root.removeView(transitionSnapshot);
378
+ }
379
+ transitionSnapshot = null;
380
+ activeTransitionId = null;
381
+ activeZoomSourceFrame = null;
382
+ webView.setTranslationX(0);
383
+ webView.setAlpha(1f);
384
+ notifyListeners("transitionEnd", event);
385
+ call.resolve(event);
386
+ };
387
+
388
+ if (durationMs == 0) {
389
+ finish.run();
390
+ return;
391
+ }
392
+
393
+ webView.animate().translationX(0).alpha(1f).setDuration(durationMs).start();
394
+ if (snapshot != null) {
395
+ snapshot
396
+ .animate()
397
+ .translationX(snapshotEndTranslation)
398
+ .alpha("none".equals(direction) ? 0f : 0.75f)
399
+ .setDuration(durationMs)
400
+ .withEndAction(finish)
401
+ .start();
402
+ } else {
403
+ webView.postDelayed(finish, durationMs);
404
+ }
405
+ });
406
+ }
407
+
408
+ @PluginMethod
409
+ public void getPluginVersion(PluginCall call) {
410
+ JSObject ret = new JSObject();
411
+ ret.put("version", implementation.getPluginVersion());
412
+ call.resolve(ret);
413
+ }
414
+
415
+ private void finishZoomTransition(
416
+ View webView,
417
+ View snapshot,
418
+ String transitionId,
419
+ int durationMs,
420
+ RectF sourceFrame,
421
+ RectF targetFrame,
422
+ float cornerRadius,
423
+ PluginCall call
424
+ ) {
425
+ RectF startFrame = sourceFrame == null ? activeZoomSourceFrame : sourceFrame;
426
+ if (startFrame == null) {
427
+ startFrame = new RectF(webView.getLeft(), webView.getTop(), webView.getRight(), webView.getBottom());
428
+ }
429
+ JSObject event = transitionEvent(transitionId, "zoom", durationMs);
430
+ Runnable finish = () -> {
431
+ FrameLayout root = contentRoot();
432
+ if (root != null && transitionSnapshot != null) {
433
+ root.removeView(transitionSnapshot);
434
+ }
435
+ transitionSnapshot = null;
436
+ activeTransitionId = null;
437
+ activeZoomSourceFrame = null;
438
+ webView.setTranslationX(0);
439
+ webView.setTranslationY(0);
440
+ webView.setScaleX(1f);
441
+ webView.setScaleY(1f);
442
+ webView.setAlpha(1f);
443
+ notifyListeners("transitionEnd", event);
444
+ call.resolve(event);
445
+ };
446
+
447
+ if (durationMs == 0) {
448
+ finish.run();
449
+ return;
450
+ }
451
+
452
+ if (targetFrame != null && snapshot != null) {
453
+ webView.setAlpha(0.01f);
454
+ snapshot.setX(startFrame.left);
455
+ snapshot.setY(startFrame.top);
456
+ snapshot.setPivotX(0f);
457
+ snapshot.setPivotY(0f);
458
+ float scaleX = targetFrame.width() / Math.max(startFrame.width(), 1f);
459
+ float scaleY = targetFrame.height() / Math.max(startFrame.height(), 1f);
460
+ webView.animate().alpha(1f).setDuration(durationMs).start();
461
+ snapshot
462
+ .animate()
463
+ .x(targetFrame.left)
464
+ .y(targetFrame.top)
465
+ .scaleX(scaleX)
466
+ .scaleY(scaleY)
467
+ .alpha(0f)
468
+ .setDuration(durationMs)
469
+ .withEndAction(finish)
470
+ .start();
471
+ return;
472
+ }
473
+
474
+ float fullWidth = Math.max(webView.getWidth(), 1f);
475
+ float fullHeight = Math.max(webView.getHeight(), 1f);
476
+ float fullCenterX = webView.getLeft() + fullWidth / 2f;
477
+ float fullCenterY = webView.getTop() + fullHeight / 2f;
478
+ webView.setPivotX(fullWidth / 2f);
479
+ webView.setPivotY(fullHeight / 2f);
480
+ webView.setTranslationX(startFrame.centerX() - fullCenterX);
481
+ webView.setTranslationY(startFrame.centerY() - fullCenterY);
482
+ webView.setScaleX(Math.max(startFrame.width() / fullWidth, 0.01f));
483
+ webView.setScaleY(Math.max(startFrame.height() / fullHeight, 0.01f));
484
+ webView.setAlpha(1f);
485
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && cornerRadius > 0) {
486
+ webView.setClipToOutline(true);
487
+ webView.setOutlineProvider(roundRectOutlineProvider(cornerRadius));
488
+ }
489
+
490
+ if (snapshot != null) {
491
+ snapshot.setX(startFrame.left);
492
+ snapshot.setY(startFrame.top);
493
+ snapshot.setPivotX(0f);
494
+ snapshot.setPivotY(0f);
495
+ snapshot
496
+ .animate()
497
+ .x(webView.getLeft())
498
+ .y(webView.getTop())
499
+ .scaleX(fullWidth / Math.max(startFrame.width(), 1f))
500
+ .scaleY(fullHeight / Math.max(startFrame.height(), 1f))
501
+ .alpha(0f)
502
+ .setDuration(durationMs)
503
+ .start();
504
+ }
505
+
506
+ webView
507
+ .animate()
508
+ .translationX(0)
509
+ .translationY(0)
510
+ .scaleX(1f)
511
+ .scaleY(1f)
512
+ .alpha(1f)
513
+ .setDuration(durationMs)
514
+ .withEndAction(() -> {
515
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
516
+ webView.setClipToOutline(false);
517
+ webView.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
518
+ }
519
+ finish.run();
520
+ })
521
+ .start();
522
+ }
523
+
524
+ private void addToolbarItems(Toolbar nativeToolbar, JSONArray rawItems, String placement) {
525
+ for (int index = 0; index < rawItems.length(); index++) {
526
+ JSONObject rawItem = rawItems.optJSONObject(index);
527
+ if (rawItem == null) {
528
+ continue;
529
+ }
530
+ int itemId = MENU_ITEM_BASE + menuActionIds.size();
531
+ String id = rawItem.optString("id", "item-" + itemId);
532
+ String title = rawItem.optString("title", "");
533
+ MenuItem item = nativeToolbar.getMenu().add(Menu.NONE, itemId, index, title);
534
+ item.setEnabled(rawItem.optBoolean("enabled", true));
535
+ Drawable icon = iconFrom(rawItem.optJSONObject("icon"));
536
+ if (icon != null) {
537
+ item.setIcon(icon);
538
+ }
539
+ item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
540
+ menuActionIds.put(itemId, id);
541
+ menuActionTitles.put(itemId, title);
542
+ menuActionPlacements.put(itemId, placement);
543
+ }
544
+ }
545
+
546
+ private RectF transitionRect(JSObject object) {
547
+ if (object == null) {
548
+ return null;
549
+ }
550
+ double width = object.optDouble("width", 0);
551
+ double height = object.optDouble("height", 0);
552
+ if (width <= 0 || height <= 0) {
553
+ return null;
554
+ }
555
+ float x = (float) object.optDouble("x", 0);
556
+ float y = (float) object.optDouble("y", 0);
557
+ return new RectF(x, y, x + (float) width, y + (float) height);
558
+ }
559
+
560
+ private RectF rootFrame(RectF viewportRect, View webView) {
561
+ return new RectF(
562
+ webView.getLeft() + viewportRect.left,
563
+ webView.getTop() + viewportRect.top,
564
+ webView.getLeft() + viewportRect.right,
565
+ webView.getTop() + viewportRect.bottom
566
+ );
567
+ }
568
+
569
+ private Rect bitmapCropRect(RectF viewportRect, Bitmap bitmap) {
570
+ int left = Math.max(0, Math.min(bitmap.getWidth() - 1, Math.round(viewportRect.left)));
571
+ int top = Math.max(0, Math.min(bitmap.getHeight() - 1, Math.round(viewportRect.top)));
572
+ int right = Math.max(left + 1, Math.min(bitmap.getWidth(), Math.round(viewportRect.right)));
573
+ int bottom = Math.max(top + 1, Math.min(bitmap.getHeight(), Math.round(viewportRect.bottom)));
574
+ return new Rect(left, top, right, bottom);
575
+ }
576
+
577
+ private ViewOutlineProvider roundRectOutlineProvider(float radius) {
578
+ return new ViewOutlineProvider() {
579
+ @Override
580
+ public void getOutline(View view, Outline outline) {
581
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
582
+ }
583
+ };
584
+ }
585
+
586
+ private Toolbar ensureToolbar() {
587
+ if (toolbar != null) {
588
+ return toolbar;
589
+ }
590
+ FrameLayout root = contentRoot();
591
+ navbarContainer = new FrameLayout(getContext());
592
+ navbarContainer.setElevation(dp(8));
593
+ toolbar = new Toolbar(getContext());
594
+ toolbar.setPopupTheme(androidx.appcompat.R.style.ThemeOverlay_AppCompat_Light);
595
+ toolbar.setOnMenuItemClickListener((item) -> {
596
+ int itemId = item.getItemId();
597
+ JSObject event = new JSObject();
598
+ event.put("id", menuActionIds.get(itemId));
599
+ event.put("title", menuActionTitles.get(itemId));
600
+ event.put("placement", menuActionPlacements.get(itemId));
601
+ notifyListeners("navbarItemTap", event);
602
+ return true;
603
+ });
604
+
605
+ navbarContainer.addView(toolbar);
606
+ if (root != null) {
607
+ root.addView(navbarContainer);
608
+ } else {
609
+ getActivity().addContentView(
610
+ navbarContainer,
611
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(DEFAULT_NAVBAR_DP))
612
+ );
613
+ }
614
+ return toolbar;
615
+ }
616
+
617
+ private BottomNavigationView ensureTabbar() {
618
+ if (tabbar != null) {
619
+ return tabbar;
620
+ }
621
+ FrameLayout root = contentRoot();
622
+ tabbarContainer = new FrameLayout(getContext());
623
+ tabbarContainer.setElevation(dp(12));
624
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
625
+ tabbarContainer.setClipToOutline(true);
626
+ tabbarContainer.setOutlineProvider(
627
+ new ViewOutlineProvider() {
628
+ @Override
629
+ public void getOutline(View view, Outline outline) {
630
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), view.getHeight() / 2f);
631
+ }
632
+ }
633
+ );
634
+ }
635
+
636
+ tabbar = new BottomNavigationView(getContext());
637
+ tabbar.setElevation(0);
638
+ tabbar.setBackgroundColor(Color.TRANSPARENT);
639
+ tabbar.setOnItemSelectedListener((item) -> {
640
+ int itemId = item.getItemId();
641
+ if (!tabIds.containsKey(itemId)) {
642
+ return false;
643
+ }
644
+ JSObject event = new JSObject();
645
+ event.put("id", tabIds.get(itemId));
646
+ event.put("index", itemId - MENU_ITEM_BASE);
647
+ event.put("title", tabTitles.get(itemId));
648
+ notifyListeners("tabSelect", event);
649
+ return true;
650
+ });
651
+ tabbarContainer.addView(tabbar);
652
+ if (root != null) {
653
+ root.addView(tabbarContainer);
654
+ } else {
655
+ getActivity().addContentView(
656
+ tabbarContainer,
657
+ new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(DEFAULT_TABBAR_DP))
658
+ );
659
+ }
660
+ return tabbar;
661
+ }
662
+
663
+ private int labelVisibilityMode(String mode) {
664
+ if ("auto".equals(mode)) {
665
+ return NavigationBarView.LABEL_VISIBILITY_AUTO;
666
+ }
667
+ if ("selected".equals(mode)) {
668
+ return NavigationBarView.LABEL_VISIBILITY_SELECTED;
669
+ }
670
+ if ("unlabeled".equals(mode)) {
671
+ return NavigationBarView.LABEL_VISIBILITY_UNLABELED;
672
+ }
673
+ return NavigationBarView.LABEL_VISIBILITY_LABELED;
674
+ }
675
+
676
+ private Drawable tabIconFrom(JSONObject tab) {
677
+ Drawable icon = iconFrom(tab.optJSONObject("icon"));
678
+ Drawable selectedIcon = iconFrom(tab.optJSONObject("selectedIcon"));
679
+ if (selectedIcon == null) {
680
+ return icon;
681
+ }
682
+ StateListDrawable stateList = new StateListDrawable();
683
+ stateList.addState(new int[] { android.R.attr.state_checked }, selectedIcon);
684
+ if (icon != null) {
685
+ stateList.addState(new int[] {}, icon);
686
+ }
687
+ return stateList;
688
+ }
689
+
690
+ private Drawable iconFrom(JSONObject descriptor) {
691
+ if (descriptor == null) {
692
+ return null;
693
+ }
694
+ String svg = svgFrom(descriptor);
695
+ if (svg != null && !svg.isEmpty()) {
696
+ return SvgIconRenderer.render(getContext().getResources(), svg, iconSizeDp(descriptor));
697
+ }
698
+ JSONObject android = descriptor.optJSONObject("android");
699
+ String resource = android == null ? null : android.optString("resource", null);
700
+ if (resource == null || resource.isEmpty()) {
701
+ resource = android == null ? null : android.optString("image", null);
702
+ }
703
+ if (resource == null || resource.isEmpty()) {
704
+ resource = descriptor.optString("src", null);
705
+ }
706
+ String inlineSvg = inlineSvgFrom(resource);
707
+ if (inlineSvg != null) {
708
+ return SvgIconRenderer.render(getContext().getResources(), inlineSvg, iconSizeDp(descriptor));
709
+ }
710
+ if (resource == null || resource.isEmpty()) {
711
+ return null;
712
+ }
713
+ int id = getContext().getResources().getIdentifier(resource, "drawable", getContext().getPackageName());
714
+ if (id == 0) {
715
+ id = getContext().getResources().getIdentifier(resource, "mipmap", getContext().getPackageName());
716
+ }
717
+ if (id == 0) {
718
+ id = getContext().getResources().getIdentifier(resource, "drawable", "android");
719
+ }
720
+ return id == 0 ? null : AppCompatResources.getDrawable(getContext(), id);
721
+ }
722
+
723
+ private String svgFrom(JSONObject descriptor) {
724
+ JSONObject android = descriptor.optJSONObject("android");
725
+ if (android != null) {
726
+ String svg = android.optString("svg", null);
727
+ if (svg != null && !svg.isEmpty()) {
728
+ return svg;
729
+ }
730
+ }
731
+ String svg = descriptor.optString("svg", null);
732
+ if (svg != null && !svg.isEmpty()) {
733
+ return svg;
734
+ }
735
+ return inlineSvgFrom(descriptor.optString("src", null));
736
+ }
737
+
738
+ private String inlineSvgFrom(String value) {
739
+ if (value == null) {
740
+ return null;
741
+ }
742
+ String trimmed = value.trim();
743
+ if (trimmed.startsWith("<svg")) {
744
+ return trimmed;
745
+ }
746
+ String lower = trimmed.toLowerCase();
747
+ if (!lower.startsWith("data:image/svg+xml")) {
748
+ return null;
749
+ }
750
+ int comma = trimmed.indexOf(',');
751
+ if (comma < 0) {
752
+ return null;
753
+ }
754
+ String meta = trimmed.substring(0, comma);
755
+ String payload = trimmed.substring(comma + 1);
756
+ try {
757
+ if (meta.contains(";base64")) {
758
+ byte[] decoded = android.util.Base64.decode(payload, android.util.Base64.DEFAULT);
759
+ return new String(decoded, "UTF-8");
760
+ }
761
+ return URLDecoder.decode(payload, "UTF-8");
762
+ } catch (Exception ignored) {
763
+ return null;
764
+ }
765
+ }
766
+
767
+ private int iconSizeDp(JSONObject descriptor) {
768
+ double width = descriptor.optDouble("width", 24);
769
+ return (int) Math.max(1, Math.round(width));
770
+ }
771
+
772
+ private static String attr(XmlPullParser parser, String name) {
773
+ return parser.getAttributeValue(null, name);
774
+ }
775
+
776
+ private static Float length(String value) {
777
+ if (value == null || value.trim().isEmpty()) {
778
+ return null;
779
+ }
780
+ Matcher matcher = SvgIconRenderer.NUMBER_PATTERN.matcher(value.trim());
781
+ return matcher.find() ? Float.parseFloat(matcher.group()) : null;
782
+ }
783
+
784
+ private static final class SvgStyle {
785
+
786
+ boolean fill = true;
787
+ boolean stroke = false;
788
+ float strokeWidth = 2f;
789
+ Paint.Cap lineCap = Paint.Cap.BUTT;
790
+ Paint.Join lineJoin = Paint.Join.MITER;
791
+ int alpha = 255;
792
+
793
+ SvgStyle copy() {
794
+ SvgStyle copy = new SvgStyle();
795
+ copy.fill = fill;
796
+ copy.stroke = stroke;
797
+ copy.strokeWidth = strokeWidth;
798
+ copy.lineCap = lineCap;
799
+ copy.lineJoin = lineJoin;
800
+ copy.alpha = alpha;
801
+ return copy;
802
+ }
803
+
804
+ void apply(XmlPullParser parser) {
805
+ String fillValue = attr(parser, "fill");
806
+ if (fillValue != null) {
807
+ fill = !"none".equalsIgnoreCase(fillValue);
808
+ }
809
+ String strokeValue = attr(parser, "stroke");
810
+ if (strokeValue != null) {
811
+ stroke = !"none".equalsIgnoreCase(strokeValue);
812
+ }
813
+ Float width = length(attr(parser, "stroke-width"));
814
+ if (width != null) {
815
+ strokeWidth = width;
816
+ }
817
+ Float opacity = length(attr(parser, "opacity"));
818
+ if (opacity != null) {
819
+ alpha = Math.max(0, Math.min(255, Math.round(opacity * 255)));
820
+ }
821
+ String cap = attr(parser, "stroke-linecap");
822
+ if ("round".equalsIgnoreCase(cap)) {
823
+ lineCap = Paint.Cap.ROUND;
824
+ } else if ("square".equalsIgnoreCase(cap)) {
825
+ lineCap = Paint.Cap.SQUARE;
826
+ } else if (cap != null) {
827
+ lineCap = Paint.Cap.BUTT;
828
+ }
829
+ String join = attr(parser, "stroke-linejoin");
830
+ if ("round".equalsIgnoreCase(join)) {
831
+ lineJoin = Paint.Join.ROUND;
832
+ } else if ("bevel".equalsIgnoreCase(join)) {
833
+ lineJoin = Paint.Join.BEVEL;
834
+ } else if (join != null) {
835
+ lineJoin = Paint.Join.MITER;
836
+ }
837
+ }
838
+ }
839
+
840
+ private static final class SvgIconRenderer {
841
+
842
+ private static final Pattern NUMBER_PATTERN = Pattern.compile("[-+]?(?:\\d*\\.\\d+|\\d+\\.?)(?:[eE][-+]?\\d+)?");
843
+
844
+ static Drawable render(Resources resources, String svg, int iconSizeDp) {
845
+ int sizePx = Math.max(1, Math.round(iconSizeDp * resources.getDisplayMetrics().density));
846
+ Bitmap bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888);
847
+ Canvas canvas = new Canvas(bitmap);
848
+ RectF viewBox = viewBox(svg, iconSizeDp);
849
+ canvas.scale(sizePx / Math.max(viewBox.width(), 1f), sizePx / Math.max(viewBox.height(), 1f));
850
+ canvas.translate(-viewBox.left, -viewBox.top);
851
+
852
+ try {
853
+ XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
854
+ parser.setInput(new StringReader(svg));
855
+ ArrayDeque<SvgStyle> styles = new ArrayDeque<>();
856
+ styles.push(new SvgStyle());
857
+ int event = parser.getEventType();
858
+ while (event != XmlPullParser.END_DOCUMENT) {
859
+ if (event == XmlPullParser.START_TAG) {
860
+ SvgStyle style = styles.peek().copy();
861
+ style.apply(parser);
862
+ styles.push(style);
863
+ drawElement(canvas, parser, style);
864
+ } else if (event == XmlPullParser.END_TAG && styles.size() > 1) {
865
+ styles.pop();
866
+ }
867
+ event = parser.next();
868
+ }
869
+ } catch (Exception ignored) {}
870
+
871
+ BitmapDrawable drawable = new BitmapDrawable(resources, bitmap);
872
+ drawable.setBounds(0, 0, sizePx, sizePx);
873
+ return drawable;
874
+ }
875
+
876
+ private static void drawElement(Canvas canvas, XmlPullParser parser, SvgStyle style) {
877
+ String name = parser.getName().toLowerCase();
878
+ if ("path".equals(name)) {
879
+ Path path = path(attr(parser, "d"));
880
+ if (path != null) {
881
+ drawPath(canvas, path, style);
882
+ }
883
+ } else if ("line".equals(name)) {
884
+ Path path = new Path();
885
+ path.moveTo(value(attr(parser, "x1")), value(attr(parser, "y1")));
886
+ path.lineTo(value(attr(parser, "x2")), value(attr(parser, "y2")));
887
+ drawPath(canvas, path, style);
888
+ } else if ("polyline".equals(name) || "polygon".equals(name)) {
889
+ Path path = pointsPath(attr(parser, "points"), "polygon".equals(name));
890
+ if (path != null) {
891
+ drawPath(canvas, path, style);
892
+ }
893
+ } else if ("circle".equals(name)) {
894
+ float cx = value(attr(parser, "cx"));
895
+ float cy = value(attr(parser, "cy"));
896
+ float radius = value(attr(parser, "r"));
897
+ Path path = new Path();
898
+ path.addOval(new RectF(cx - radius, cy - radius, cx + radius, cy + radius), Path.Direction.CW);
899
+ drawPath(canvas, path, style);
900
+ } else if ("rect".equals(name)) {
901
+ float x = value(attr(parser, "x"));
902
+ float y = value(attr(parser, "y"));
903
+ float width = value(attr(parser, "width"));
904
+ float height = value(attr(parser, "height"));
905
+ float radius = Math.max(value(attr(parser, "rx")), value(attr(parser, "ry")));
906
+ Path path = new Path();
907
+ RectF rect = new RectF(x, y, x + width, y + height);
908
+ if (radius > 0) {
909
+ path.addRoundRect(rect, radius, radius, Path.Direction.CW);
910
+ } else {
911
+ path.addRect(rect, Path.Direction.CW);
912
+ }
913
+ drawPath(canvas, path, style);
914
+ }
915
+ }
916
+
917
+ private static void drawPath(Canvas canvas, Path path, SvgStyle style) {
918
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
919
+ paint.setColor(Color.BLACK);
920
+ paint.setAlpha(style.alpha);
921
+ if (style.fill) {
922
+ paint.setStyle(Paint.Style.FILL);
923
+ canvas.drawPath(path, paint);
924
+ }
925
+ if (style.stroke) {
926
+ paint.setStyle(Paint.Style.STROKE);
927
+ paint.setStrokeWidth(style.strokeWidth);
928
+ paint.setStrokeCap(style.lineCap);
929
+ paint.setStrokeJoin(style.lineJoin);
930
+ canvas.drawPath(path, paint);
931
+ }
932
+ }
933
+
934
+ private static Path path(String data) {
935
+ if (data == null || data.isEmpty()) {
936
+ return null;
937
+ }
938
+ try {
939
+ return PathParser.createPathFromPathData(data);
940
+ } catch (RuntimeException ignored) {
941
+ return null;
942
+ }
943
+ }
944
+
945
+ private static Path pointsPath(String value, boolean closed) {
946
+ List<Float> numbers = numbers(value);
947
+ if (numbers.size() < 2) {
948
+ return null;
949
+ }
950
+ Path path = new Path();
951
+ path.moveTo(numbers.get(0), numbers.get(1));
952
+ for (int index = 2; index + 1 < numbers.size(); index += 2) {
953
+ path.lineTo(numbers.get(index), numbers.get(index + 1));
954
+ }
955
+ if (closed) {
956
+ path.close();
957
+ }
958
+ return path;
959
+ }
960
+
961
+ private static RectF viewBox(String svg, int iconSizeDp) {
962
+ List<Float> viewBoxValues = numbers(attribute(svg, "viewBox"));
963
+ if (viewBoxValues.size() >= 4) {
964
+ return new RectF(
965
+ viewBoxValues.get(0),
966
+ viewBoxValues.get(1),
967
+ viewBoxValues.get(0) + viewBoxValues.get(2),
968
+ viewBoxValues.get(1) + viewBoxValues.get(3)
969
+ );
970
+ }
971
+ float width = value(attribute(svg, "width"));
972
+ float height = value(attribute(svg, "height"));
973
+ if (width <= 0 || height <= 0) {
974
+ width = iconSizeDp;
975
+ height = iconSizeDp;
976
+ }
977
+ return new RectF(0, 0, width, height);
978
+ }
979
+
980
+ private static String attribute(String svg, String name) {
981
+ if (svg == null) {
982
+ return null;
983
+ }
984
+ Pattern pattern = Pattern.compile(name + "\\s*=\\s*[\"']([^\"']+)[\"']", Pattern.CASE_INSENSITIVE);
985
+ Matcher matcher = pattern.matcher(svg);
986
+ return matcher.find() ? matcher.group(1) : null;
987
+ }
988
+
989
+ private static float value(String value) {
990
+ Float parsed = length(value);
991
+ return parsed == null ? 0f : parsed;
992
+ }
993
+
994
+ private static List<Float> numbers(String value) {
995
+ List<Float> numbers = new ArrayList<>();
996
+ if (value == null) {
997
+ return numbers;
998
+ }
999
+ Matcher matcher = NUMBER_PATTERN.matcher(value);
1000
+ while (matcher.find()) {
1001
+ numbers.add(Float.parseFloat(matcher.group()));
1002
+ }
1003
+ return numbers;
1004
+ }
1005
+ }
1006
+
1007
+ private void applyToolbarColors(Toolbar nativeToolbar, JSObject colors) {
1008
+ boolean dynamic = Boolean.TRUE.equals(colors.getBool("dynamic"));
1009
+ int tintFallback = dynamic ? dynamicColor("system_accent1_600", tintColor) : tintColor;
1010
+ int backgroundFallback = dynamic
1011
+ ? withAlpha(dynamicColor(isNightMode() ? "system_neutral1_900" : "system_neutral1_50", Color.WHITE), 235)
1012
+ : Color.argb(225, 255, 255, 255);
1013
+ int foregroundFallback = dynamic
1014
+ ? dynamicColor(isNightMode() ? "system_neutral1_50" : "system_neutral1_900", Color.rgb(20, 24, 32))
1015
+ : Color.rgb(20, 24, 32);
1016
+ int tint = parseColor(colors.getString("tint", null), tintFallback);
1017
+ int background = parseColor(colors.getString("background", null), backgroundFallback);
1018
+ int foreground = parseColor(colors.getString("foreground", null), foregroundFallback);
1019
+ tintColor = tint;
1020
+ nativeToolbar.setTitleTextColor(foreground);
1021
+ nativeToolbar.setSubtitleTextColor(withAlpha(foreground, 190));
1022
+ Drawable navigationIcon = nativeToolbar.getNavigationIcon();
1023
+ if (navigationIcon != null) {
1024
+ Drawable tintedIcon = navigationIcon.mutate();
1025
+ tintedIcon.setTint(tint);
1026
+ nativeToolbar.setNavigationIcon(tintedIcon);
1027
+ }
1028
+ if (navbarContainer != null) {
1029
+ navbarContainer.setBackgroundColor(background);
1030
+ }
1031
+ for (int index = 0; index < nativeToolbar.getMenu().size(); index++) {
1032
+ Drawable icon = nativeToolbar.getMenu().getItem(index).getIcon();
1033
+ if (icon != null) {
1034
+ icon.mutate().setTint(tint);
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ private void applyTabbarColors(BottomNavigationView nativeTabbar, PluginCall call, JSObject colors) {
1040
+ boolean dynamic = Boolean.TRUE.equals(colors.getBool("dynamic"));
1041
+ int tintFallback = dynamic ? dynamicColor("system_accent1_600", tintColor) : tintColor;
1042
+ int inactiveFallback = dynamic ? dynamicColor("system_neutral2_600", inactiveTintColor) : inactiveTintColor;
1043
+ int backgroundFallback = dynamic
1044
+ ? withAlpha(dynamicColor(isNightMode() ? "system_neutral1_900" : "system_neutral1_50", Color.WHITE), 245)
1045
+ : Color.argb(235, 255, 255, 255);
1046
+ int tint = parseColor(colors.getString("tint", null), tintFallback);
1047
+ int inactiveTint = parseColor(colors.getString("inactiveTint", null), inactiveFallback);
1048
+ int background = parseColor(colors.getString("background", null), backgroundFallback);
1049
+ tintColor = tint;
1050
+ inactiveTintColor = inactiveTint;
1051
+ int[][] states = new int[][] { new int[] { android.R.attr.state_checked }, new int[] {} };
1052
+ int[] colorState = new int[] { tint, inactiveTint };
1053
+ ColorStateList colorStateList = new ColorStateList(states, colorState);
1054
+ nativeTabbar.setItemIconTintList(colorStateList);
1055
+ nativeTabbar.setItemTextColor(colorStateList);
1056
+ nativeTabbar.setItemActiveIndicatorEnabled(!call.getBoolean("disableIndicator", false));
1057
+ Integer indicator = colorOption(call, colors, "indicatorColor", "indicator", null);
1058
+ nativeTabbar.setItemActiveIndicatorColor(indicator == null ? null : ColorStateList.valueOf(indicator));
1059
+ Integer ripple = colorOption(call, colors, "rippleColor", "ripple", null);
1060
+ nativeTabbar.setItemRippleColor(ripple == null ? null : ColorStateList.valueOf(ripple));
1061
+ nativeTabbar.setBackgroundColor(Color.TRANSPARENT);
1062
+ if (tabbarContainer != null) {
1063
+ GradientDrawable capsule = new GradientDrawable();
1064
+ capsule.setColor(background);
1065
+ capsule.setCornerRadius(dp(DEFAULT_TABBAR_DP) / 2f);
1066
+ tabbarContainer.setBackground(capsule);
1067
+ }
1068
+ }
1069
+
1070
+ private int parseColor(String value, int fallback) {
1071
+ if (value == null || value.isEmpty()) {
1072
+ return fallback;
1073
+ }
1074
+ if ("android:dynamicPrimary".equals(value) || "system:primary".equals(value)) {
1075
+ return dynamicColor("system_accent1_600", fallback);
1076
+ }
1077
+ if ("android:dynamicSurface".equals(value) || "system:surface".equals(value)) {
1078
+ return dynamicColor(isNightMode() ? "system_neutral1_900" : "system_neutral1_50", fallback);
1079
+ }
1080
+ try {
1081
+ return Color.parseColor(value);
1082
+ } catch (IllegalArgumentException ignored) {
1083
+ return fallback;
1084
+ }
1085
+ }
1086
+
1087
+ private Integer parseColorOrNull(String value) {
1088
+ if (value == null || value.isEmpty()) {
1089
+ return null;
1090
+ }
1091
+ if ("android:dynamicPrimary".equals(value) || "system:primary".equals(value)) {
1092
+ return dynamicColor("system_accent1_600", tintColor);
1093
+ }
1094
+ if ("android:dynamicSurface".equals(value) || "system:surface".equals(value)) {
1095
+ return dynamicColor(isNightMode() ? "system_neutral1_900" : "system_neutral1_50", Color.WHITE);
1096
+ }
1097
+ try {
1098
+ return Color.parseColor(value);
1099
+ } catch (IllegalArgumentException ignored) {
1100
+ return null;
1101
+ }
1102
+ }
1103
+
1104
+ private Integer colorOption(PluginCall call, JSObject colors, String directKey, String colorKey, Integer fallback) {
1105
+ Integer direct = parseColorOrNull(call.getString(directKey, null));
1106
+ if (direct != null) {
1107
+ return direct;
1108
+ }
1109
+ Integer nested = parseColorOrNull(colors.getString(colorKey, null));
1110
+ return nested == null ? fallback : nested;
1111
+ }
1112
+
1113
+ private int dynamicColor(String name, int fallback) {
1114
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
1115
+ return fallback;
1116
+ }
1117
+ int id = Resources.getSystem().getIdentifier(name, "color", "android");
1118
+ if (id == 0) {
1119
+ return fallback;
1120
+ }
1121
+ return getContext().getColor(id);
1122
+ }
1123
+
1124
+ private int withAlpha(int color, int alpha) {
1125
+ return Color.argb(Math.max(0, Math.min(255, alpha)), Color.red(color), Color.green(color), Color.blue(color));
1126
+ }
1127
+
1128
+ private boolean isNightMode() {
1129
+ int mode = getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1130
+ return mode == Configuration.UI_MODE_NIGHT_YES;
1131
+ }
1132
+
1133
+ private void layoutChrome() {
1134
+ FrameLayout root = contentRoot();
1135
+ if (root == null) {
1136
+ return;
1137
+ }
1138
+ int status = statusBarInset();
1139
+ int bottom = navigationBarInset();
1140
+ int navbarHeight = navbarVisible ? status + dp(DEFAULT_NAVBAR_DP) : 0;
1141
+ int tabbarHeight = dp(DEFAULT_TABBAR_DP);
1142
+ int tabbarBottomMargin = tabbarVisible ? bottom + dp(10) : bottom;
1143
+
1144
+ if (navbarContainer != null) {
1145
+ FrameLayout.LayoutParams containerParams = new FrameLayout.LayoutParams(
1146
+ ViewGroup.LayoutParams.MATCH_PARENT,
1147
+ navbarHeight,
1148
+ Gravity.TOP
1149
+ );
1150
+ navbarContainer.setLayoutParams(containerParams);
1151
+ FrameLayout.LayoutParams toolbarParams = new FrameLayout.LayoutParams(
1152
+ ViewGroup.LayoutParams.MATCH_PARENT,
1153
+ dp(DEFAULT_NAVBAR_DP),
1154
+ Gravity.TOP
1155
+ );
1156
+ toolbarParams.topMargin = status;
1157
+ toolbar.setLayoutParams(toolbarParams);
1158
+ }
1159
+
1160
+ if (tabbarContainer != null) {
1161
+ int rootWidth = root.getWidth() > 0 ? root.getWidth() : Resources.getSystem().getDisplayMetrics().widthPixels;
1162
+ int tabbarWidth = Math.min(Math.max(0, rootWidth - dp(48)), dp(420));
1163
+ FrameLayout.LayoutParams tabbarContainerParams = new FrameLayout.LayoutParams(
1164
+ tabbarWidth,
1165
+ tabbarHeight,
1166
+ Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL
1167
+ );
1168
+ tabbarContainerParams.bottomMargin = tabbarBottomMargin;
1169
+ tabbarContainer.setLayoutParams(tabbarContainerParams);
1170
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1171
+ tabbarContainer.invalidateOutline();
1172
+ }
1173
+ }
1174
+
1175
+ if (tabbar != null) {
1176
+ FrameLayout.LayoutParams tabbarParams = new FrameLayout.LayoutParams(
1177
+ ViewGroup.LayoutParams.MATCH_PARENT,
1178
+ ViewGroup.LayoutParams.MATCH_PARENT
1179
+ );
1180
+ tabbar.setLayoutParams(tabbarParams);
1181
+ tabbar.setPadding(0, 0, 0, 0);
1182
+ }
1183
+
1184
+ bringChromeToFront();
1185
+ }
1186
+
1187
+ private void bringChromeToFront() {
1188
+ if (navbarContainer != null) {
1189
+ navbarContainer.bringToFront();
1190
+ }
1191
+ if (tabbar != null) {
1192
+ tabbar.bringToFront();
1193
+ }
1194
+ if (tabbarContainer != null) {
1195
+ tabbarContainer.bringToFront();
1196
+ }
1197
+ }
1198
+
1199
+ private void updateInsetsAndNotify() {
1200
+ layoutChrome();
1201
+ JSObject insets = currentInsets();
1202
+ JSObject event = new JSObject();
1203
+ event.put("insets", insets);
1204
+ notifyListeners("safeAreaChanged", event);
1205
+ if ("none".equals(contentInsetMode) || getBridge() == null || getBridge().getWebView() == null) {
1206
+ return;
1207
+ }
1208
+ String script =
1209
+ "(() => {" +
1210
+ "const root=document.documentElement;" +
1211
+ "root.style.setProperty('--cap-native-navigation-top','" +
1212
+ insets.getInteger("top", 0) +
1213
+ "px');" +
1214
+ "root.style.setProperty('--cap-native-navigation-right','" +
1215
+ insets.getInteger("right", 0) +
1216
+ "px');" +
1217
+ "root.style.setProperty('--cap-native-navigation-bottom','" +
1218
+ insets.getInteger("bottom", 0) +
1219
+ "px');" +
1220
+ "root.style.setProperty('--cap-native-navigation-left','" +
1221
+ insets.getInteger("left", 0) +
1222
+ "px');" +
1223
+ "root.style.setProperty('--cap-native-navbar-height','" +
1224
+ insets.getInteger("navbarHeight", 0) +
1225
+ "px');" +
1226
+ "root.style.setProperty('--cap-native-tabbar-height','" +
1227
+ insets.getInteger("tabbarHeight", 0) +
1228
+ "px');" +
1229
+ "window.dispatchEvent(new CustomEvent('capNativeNavigation:safeAreaChanged',{detail:{insets:" +
1230
+ insets.toString() +
1231
+ "}}));" +
1232
+ "})();";
1233
+ getBridge().getWebView().evaluateJavascript(script, null);
1234
+ }
1235
+
1236
+ private JSObject currentInsets() {
1237
+ int top = navbarVisible ? statusBarInset() + dp(DEFAULT_NAVBAR_DP) : 0;
1238
+ int bottom = tabbarVisible ? navigationBarInset() + dp(DEFAULT_TABBAR_DP) + dp(10) : 0;
1239
+ JSObject insets = new JSObject();
1240
+ insets.put("top", top);
1241
+ insets.put("right", 0);
1242
+ insets.put("bottom", bottom);
1243
+ insets.put("left", 0);
1244
+ insets.put("navbarHeight", top);
1245
+ insets.put("tabbarHeight", bottom);
1246
+ return insets;
1247
+ }
1248
+
1249
+ private JSObject insetsResult() {
1250
+ JSObject result = new JSObject();
1251
+ result.put("insets", currentInsets());
1252
+ return result;
1253
+ }
1254
+
1255
+ private JSObject transitionEvent(String id, String direction, int duration) {
1256
+ JSObject event = new JSObject();
1257
+ event.put("id", id);
1258
+ event.put("direction", direction);
1259
+ event.put("duration", duration);
1260
+ return event;
1261
+ }
1262
+
1263
+ private FrameLayout contentRoot() {
1264
+ Activity activity = getActivity();
1265
+ return activity == null ? null : activity.findViewById(android.R.id.content);
1266
+ }
1267
+
1268
+ private void enableEdgeToEdge() {
1269
+ Activity activity = getActivity();
1270
+ if (activity == null) {
1271
+ return;
1272
+ }
1273
+ Window window = activity.getWindow();
1274
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
1275
+ window.setDecorFitsSystemWindows(false);
1276
+ } else {
1277
+ window
1278
+ .getDecorView()
1279
+ .setSystemUiVisibility(
1280
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
1281
+ );
1282
+ }
1283
+ }
1284
+
1285
+ private int statusBarInset() {
1286
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1287
+ WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
1288
+ if (insets != null) {
1289
+ return insets.getStableInsetTop();
1290
+ }
1291
+ }
1292
+ return systemDimension("status_bar_height");
1293
+ }
1294
+
1295
+ private int navigationBarInset() {
1296
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1297
+ WindowInsets insets = getActivity().getWindow().getDecorView().getRootWindowInsets();
1298
+ if (insets != null) {
1299
+ return insets.getStableInsetBottom();
1300
+ }
1301
+ }
1302
+ return systemDimension("navigation_bar_height");
1303
+ }
1304
+
1305
+ private int systemDimension(String name) {
1306
+ int id = getContext().getResources().getIdentifier(name, "dimen", "android");
1307
+ return id == 0 ? 0 : getContext().getResources().getDimensionPixelSize(id);
1308
+ }
1309
+
1310
+ private int dp(int value) {
1311
+ return Math.round(value * getContext().getResources().getDisplayMetrics().density);
1312
+ }
1313
+
1314
+ private void runOnUiThread(Runnable runnable) {
1315
+ Activity activity = getActivity();
1316
+ if (activity == null) {
1317
+ runnable.run();
1318
+ return;
1319
+ }
1320
+ activity.runOnUiThread(runnable);
1321
+ }
1322
+ }