| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 828bbb0 commit 2b80032
9 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -176,6 +176,30 @@ pn.Modal(*children, visible=show_modal, on_dismiss=handler, title="Confirm", | |||
| 176 | 176 | ||
| 177 | 177 | Overlay dialog shown when `visible=True`. | |
| 178 | 178 | ||
| 179 | + ## TabBar | ||
| 180 | + | ||
| 181 | + ```python | ||
| 182 | + pn.Element("TabBar", { | ||
| 183 | + "items": [ | ||
| 184 | + {"name": "Home", "title": "Home"}, | ||
| 185 | + {"name": "Settings", "title": "Settings"}, | ||
| 186 | + ], | ||
| 187 | + "active_tab": "Home", | ||
| 188 | + "on_tab_select": handler, | ||
| 189 | + }) | ||
| 190 | + ``` | ||
| 191 | + | ||
| 192 | + Native tab bar — typically created automatically by `Tab.Navigator`. | ||
| 193 | + | ||
| 194 | + | Platform | Native view | | ||
| 195 | + |----------|--------------------------| | ||
| 196 | + | Android | `BottomNavigationView` | | ||
| 197 | + | iOS | `UITabBar` | | ||
| 198 | + | ||
| 199 | + - `items` — list of `{"name": str, "title": str}` dicts defining each tab | ||
| 200 | + - `active_tab` — the `name` of the currently active tab | ||
| 201 | + - `on_tab_select` — callback `(str) -> None` receiving the selected tab name | ||
| 202 | + | ||
| 179 | 203 | ## FlatList | |
| 180 | 204 | ||
| 181 | 205 | ```python | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -3,7 +3,7 @@ | |||
| 3 | 3 | PythonNative offers two approaches to navigation: | |
| 4 | 4 | ||
| 5 | 5 | 1. **Declarative navigators** (recommended) — component-based, inspired by React Navigation | |
| 6 | - 2. **Legacy push/pop** — imperative navigation via `use_navigation()` | ||
| 6 | + 2. **Page-level push/pop** — imperative navigation via `use_navigation()` (for native page transitions) | ||
| 7 | 7 | ||
| 8 | 8 | ## Declarative Navigation | |
| 9 | 9 | ||
@@ -54,7 +54,9 @@ def DetailScreen(): | |||
| 54 | 54 | ||
| 55 | 55 | ### Tab Navigator | |
| 56 | 56 | ||
| 57 | - A tab navigator renders a tab bar and switches between screens. | ||
| 57 | + A tab navigator renders a **native tab bar** and switches between screens. | ||
| 58 | + On Android the tab bar is a `BottomNavigationView` from Material Components; | ||
| 59 | + on iOS it is a `UITabBar`. | ||
| 58 | 60 | ||
| 59 | 61 | ```python | |
| 60 | 62 | from pythonnative.navigation import create_tab_navigator | |
@@ -71,6 +73,13 @@ def App(): | |||
| 71 | 73 | ) | |
| 72 | 74 | ``` | |
| 73 | 75 | ||
| 76 | + The tab bar emits a `TabBar` element that maps to platform-native views: | ||
| 77 | + | ||
| 78 | + | Platform | Native view | | ||
| 79 | + |----------|------------------------------| | ||
| 80 | + | Android | `BottomNavigationView` | | ||
| 81 | + | iOS | `UITabBar` | | ||
| 82 | + | ||
| 74 | 83 | ### Drawer Navigator | |
| 75 | 84 | ||
| 76 | 85 | A drawer navigator provides a side menu for switching screens. | |
@@ -100,7 +109,10 @@ def HomeScreen(): | |||
| 100 | 109 | ||
| 101 | 110 | ### Nesting Navigators | |
| 102 | 111 | ||
| 103 | - Navigators can be nested — for example, tabs containing stacks: | ||
| 112 | + Navigators can be nested — for example, tabs containing stacks. | ||
| 113 | + When a child navigator receives a `navigate()` call for an unknown route, | ||
| 114 | + it automatically **forwards** the request to its parent navigator. | ||
| 115 | + Similarly, `go_back()` at the root of a child stack forwards to the parent. | ||
| 104 | 116 | ||
| 105 | 117 | ```python | |
| 106 | 118 | Stack = create_stack_navigator() | |
@@ -123,6 +135,9 @@ def App(): | |||
| 123 | 135 | ) | |
| 124 | 136 | ``` | |
| 125 | 137 | ||
| 138 | + Inside `FeedScreen`, calling `nav.navigate("Settings")` will forward to the | ||
| 139 | + parent tab navigator and switch to the Settings tab. | ||
| 140 | + | ||
| 126 | 141 | ## NavigationHandle API | |
| 127 | 142 | ||
| 128 | 143 | Inside any screen rendered by a navigator, `pn.use_navigation()` returns a handle with: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,9 +1,11 @@ | |||
| 1 | 1 | import emoji | |
| 2 | 2 | ||
| 3 | 3 | import pythonnative as pn | |
| 4 | + from pythonnative.navigation import NavigationContainer, create_tab_navigator | ||
| 4 | 5 | ||
| 5 | 6 | MEDALS = [":1st_place_medal:", ":2nd_place_medal:", ":3rd_place_medal:"] | |
| 6 | 7 | ||
| 8 | + Tab = create_tab_navigator() | ||
| 7 | 9 | ||
| 8 | 10 | styles = pn.StyleSheet.create( | |
| 9 | 11 | title={"font_size": 24, "bold": True}, | |
@@ -39,7 +41,8 @@ def counter_badge(initial: int = 0) -> pn.Element: | |||
| 39 | 41 | ||
| 40 | 42 | ||
| 41 | 43 | @pn.component | |
| 42 | - def MainPage() -> pn.Element: | ||
| 44 | + def HomeTab() -> pn.Element: | ||
| 45 | + """Home tab — counter demo and push-navigation to other pages.""" | ||
| 43 | 46 | nav = pn.use_navigation() | |
| 44 | 47 | return pn.ScrollView( | |
| 45 | 48 | pn.Column( | |
@@ -55,3 +58,29 @@ def MainPage() -> pn.Element: | |||
| 55 | 58 | style=styles["section"], | |
| 56 | 59 | ) | |
| 57 | 60 | ) | |
| 61 | + | ||
| 62 | + | ||
| 63 | + @pn.component | ||
| 64 | + def SettingsTab() -> pn.Element: | ||
| 65 | + """Settings tab — simple placeholder content.""" | ||
| 66 | + return pn.ScrollView( | ||
| 67 | + pn.Column( | ||
| 68 | + pn.Text("Settings", style=styles["title"]), | ||
| 69 | + pn.Text("App version: 0.7.0", style=styles["subtitle"]), | ||
| 70 | + pn.Text( | ||
| 71 | + "This tab uses a native UITabBar on iOS " "and BottomNavigationView on Android.", | ||
| 72 | + style=styles["subtitle"], | ||
| 73 | + ), | ||
| 74 | + style=styles["section"], | ||
| 75 | + ) | ||
| 76 | + ) | ||
| 77 | + | ||
| 78 | + | ||
| 79 | + @pn.component | ||
| 80 | + def MainPage() -> pn.Element: | ||
| 81 | + return NavigationContainer( | ||
| 82 | + Tab.Navigator( | ||
| 83 | + Tab.Screen("Home", component=HomeTab, options={"title": "Home"}), | ||
| 84 | + Tab.Screen("Settings", component=SettingsTab, options={"title": "Settings"}), | ||
| 85 | + ) | ||
| 86 | + ) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -618,6 +618,148 @@ def onStopTrackingTouch(self, seekBar: Any) -> None: | |||
| 618 | 618 | sb.setOnSeekBarChangeListener(SeekProxy(cb, min_val, rng)) | |
| 619 | 619 | ||
| 620 | 620 | ||
| 621 | + _android_tabbar_state: dict = {"callback": None, "items": []} | ||
| 622 | + | ||
| 623 | + | ||
| 624 | + class TabBarHandler(ViewHandler): | ||
| 625 | + """Native tab bar using ``BottomNavigationView`` from Material Components. | ||
| 626 | + | ||
| 627 | + Falls back to a horizontal ``LinearLayout`` with ``Button`` children | ||
| 628 | + when Material Components is unavailable. | ||
| 629 | + """ | ||
| 630 | + | ||
| 631 | + _is_material: bool = True | ||
| 632 | + | ||
| 633 | + def create(self, props: Dict[str, Any]) -> Any: | ||
| 634 | + try: | ||
| 635 | + bnv = jclass("com.google.android.material.bottomnavigation.BottomNavigationView")(_ctx()) | ||
| 636 | + bnv.setBackgroundColor(parse_color_int("#FFFFFF")) | ||
| 637 | + ViewGroupLP = jclass("android.view.ViewGroup$LayoutParams") | ||
| 638 | + LayoutParams = jclass("android.widget.LinearLayout$LayoutParams") | ||
| 639 | + lp = LayoutParams(ViewGroupLP.MATCH_PARENT, ViewGroupLP.WRAP_CONTENT) | ||
| 640 | + bnv.setLayoutParams(lp) | ||
| 641 | + self._is_material = True | ||
| 642 | + self._apply_full(bnv, props) | ||
| 643 | + return bnv | ||
| 644 | + except Exception: | ||
| 645 | + self._is_material = False | ||
| 646 | + return self._create_fallback(props) | ||
| 647 | + | ||
| 648 | + def _create_fallback(self, props: Dict[str, Any]) -> Any: | ||
| 649 | + """Horizontal LinearLayout with Button children as a tab-bar fallback.""" | ||
| 650 | + LinearLayout = jclass("android.widget.LinearLayout") | ||
| 651 | + ll = LinearLayout(_ctx()) | ||
| 652 | + ll.setOrientation(LinearLayout.HORIZONTAL) | ||
| 653 | + ll.setBackgroundColor(parse_color_int("#F8F8F8")) | ||
| 654 | + self._apply_fallback(ll, props) | ||
| 655 | + return ll | ||
| 656 | + | ||
| 657 | + def update(self, native_view: Any, changed: Dict[str, Any]) -> None: | ||
| 658 | + if self._is_material: | ||
| 659 | + self._apply_partial(native_view, changed) | ||
| 660 | + else: | ||
| 661 | + self._apply_fallback(native_view, changed) | ||
| 662 | + | ||
| 663 | + def _apply_full(self, bnv: Any, props: Dict[str, Any]) -> None: | ||
| 664 | + """Initial creation — all props are present.""" | ||
| 665 | + items = props.get("items", []) | ||
| 666 | + self._set_menu(bnv, items) | ||
| 667 | + self._set_active(bnv, props.get("active_tab"), items) | ||
| 668 | + cb = props.get("on_tab_select") | ||
| 669 | + if cb is not None: | ||
| 670 | + self._set_listener(bnv, cb, items) | ||
| 671 | + | ||
| 672 | + def _apply_partial(self, bnv: Any, changed: Dict[str, Any]) -> None: | ||
| 673 | + """Reconciler update — only changed props are present.""" | ||
| 674 | + prev_items = _android_tabbar_state["items"] | ||
| 675 | + | ||
| 676 | + if "items" in changed: | ||
| 677 | + items = changed["items"] | ||
| 678 | + self._set_menu(bnv, items) | ||
| 679 | + else: | ||
| 680 | + items = prev_items | ||
| 681 | + | ||
| 682 | + if "active_tab" in changed: | ||
| 683 | + self._set_active(bnv, changed["active_tab"], items) | ||
| 684 | + | ||
| 685 | + if "on_tab_select" in changed: | ||
| 686 | + cb = changed["on_tab_select"] | ||
| 687 | + if cb is not None: | ||
| 688 | + self._set_listener(bnv, cb, items) | ||
| 689 | + | ||
| 690 | + def _set_menu(self, bnv: Any, items: list) -> None: | ||
| 691 | + _android_tabbar_state["items"] = items | ||
| 692 | + try: | ||
| 693 | + menu = bnv.getMenu() | ||
| 694 | + menu.clear() | ||
| 695 | + for i, item in enumerate(items): | ||
| 696 | + title = item.get("title", item.get("name", "")) | ||
| 697 | + menu.add(0, i, i, str(title)) | ||
| 698 | + except Exception: | ||
| 699 | + pass | ||
| 700 | + | ||
| 701 | + def _set_active(self, bnv: Any, active: Any, items: list) -> None: | ||
| 702 | + if active and items: | ||
| 703 | + for i, item in enumerate(items): | ||
| 704 | + if item.get("name") == active: | ||
| 705 | + try: | ||
| 706 | + bnv.setSelectedItemId(i) | ||
| 707 | + except Exception: | ||
| 708 | + pass | ||
| 709 | + break | ||
| 710 | + | ||
| 711 | + def _set_listener(self, bnv: Any, cb: Callable, items: list) -> None: | ||
| 712 | + _android_tabbar_state["callback"] = cb | ||
| 713 | + _android_tabbar_state["items"] = items | ||
| 714 | + try: | ||
| 715 | + listener_cls = jclass("com.google.android.material.navigation.NavigationBarView$OnItemSelectedListener") | ||
| 716 | + | ||
| 717 | + class _TabSelectProxy(dynamic_proxy(listener_cls)): | ||
| 718 | + def __init__(self, callback: Callable, tab_items: list) -> None: | ||
| 719 | + super().__init__() | ||
| 720 | + self.callback = callback | ||
| 721 | + self.tab_items = tab_items | ||
| 722 | + | ||
| 723 | + def onNavigationItemSelected(self, menu_item: Any) -> bool: | ||
| 724 | + idx = menu_item.getItemId() | ||
| 725 | + if 0 <= idx < len(self.tab_items): | ||
| 726 | + self.callback(self.tab_items[idx].get("name", "")) | ||
| 727 | + return True | ||
| 728 | + | ||
| 729 | + bnv.setOnItemSelectedListener(_TabSelectProxy(cb, items)) | ||
| 730 | + except Exception: | ||
| 731 | + pass | ||
| 732 | + | ||
| 733 | + def _apply_fallback(self, ll: Any, props: Dict[str, Any]) -> None: | ||
| 734 | + items = props.get("items", []) | ||
| 735 | + active = props.get("active_tab") | ||
| 736 | + cb = props.get("on_tab_select") | ||
| 737 | + if "items" in props: | ||
| 738 | + ll.removeAllViews() | ||
| 739 | + for item in items: | ||
| 740 | + name = item.get("name", "") | ||
| 741 | + title = item.get("title", name) | ||
| 742 | + btn = jclass("android.widget.Button")(_ctx()) | ||
| 743 | + btn.setText(str(title)) | ||
| 744 | + btn.setEnabled(name != active) | ||
| 745 | + if cb is not None: | ||
| 746 | + tab_name = name | ||
| 747 | + | ||
| 748 | + def _make_click(n: str) -> Callable[[], None]: | ||
| 749 | + return lambda: cb(n) | ||
| 750 | + | ||
| 751 | + class _ClickProxy(dynamic_proxy(jclass("android.view.View").OnClickListener)): | ||
| 752 | + def __init__(self, callback: Callable[[], None]) -> None: | ||
| 753 | + super().__init__() | ||
| 754 | + self.callback = callback | ||
| 755 | + | ||
| 756 | + def onClick(self, view: Any) -> None: | ||
| 757 | + self.callback() | ||
| 758 | + | ||
| 759 | + btn.setOnClickListener(_ClickProxy(_make_click(tab_name))) | ||
| 760 | + ll.addView(btn) | ||
| 761 | + | ||
| 762 | + | ||
| 621 | 763 | class PressableHandler(ViewHandler): | |
| 622 | 764 | def create(self, props: Dict[str, Any]) -> Any: | |
| 623 | 765 | fl = jclass("android.widget.FrameLayout")(_ctx()) | |
@@ -686,4 +828,5 @@ def register_handlers(registry: Any) -> None: | |||
| 686 | 828 | registry.register("SafeAreaView", SafeAreaViewHandler()) | |
| 687 | 829 | registry.register("Modal", ModalHandler()) | |
| 688 | 830 | registry.register("Slider", SliderHandler()) | |
| 831 | + registry.register("TabBar", TabBarHandler()) | ||
| 689 | 832 | registry.register("Pressable", PressableHandler()) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments