|
| 1 | +from android.runnable import Runnable |
| 2 | +from jnius import autoclass, java_method, PythonJavaClass |
| 3 | +from typing import Literal |
| 4 | + |
| 5 | +__all__ = ('update_system_ui') |
| 6 | + |
| 7 | +def update_system_ui( |
| 8 | + status_bar_color: list[float] | str, |
| 9 | + navigation_bar_color: list[float] | str, |
| 10 | + icon_style: Literal["Light", "Dark"] = "Dark", |
| 11 | + pad_status: bool = True, |
| 12 | + pad_nav: bool = False, |
| 13 | +) -> None: |
| 14 | + """ |
| 15 | + Provides control of colors for the status and navigation bar and also handle insets padding on Android 15 and above. |
| 16 | +
|
| 17 | + For `status_bar_color` and `navigation_bar_color` either provide a hex color code or rgba (tuple or list) values. |
| 18 | + `pad_status` and `pad_nav` will take effect only above Android 15. |
| 19 | + IF `icon_style` IS `Dark` THE ICONS WILL BE DARK. |
| 20 | + IF `icon_style` IS `Light` THE ICONS WILL BE LIGHT. |
| 21 | +
|
| 22 | + Original code at https://github.com/CarbonKivy/CarbonKivy/blob/39e360314a3885f3b462add4475e6c609b5bef53/carbonkivy/utils.py#L43 (subject to active changes ahead). |
| 23 | + """ |
| 24 | + |
| 25 | + Color = autoclass("android.graphics.Color") |
| 26 | + Build_VERSION = autoclass("android.os.Build$VERSION") |
| 27 | + WindowInsetsType = autoclass("android.view.WindowInsets$Type") |
| 28 | + PythonActivity = autoclass("org.kivy.android.PythonActivity") |
| 29 | + View = autoclass("android.view.View") |
| 30 | + |
| 31 | + activity = PythonActivity.mActivity |
| 32 | + window = activity.getWindow() |
| 33 | + decor_view = window.getDecorView() |
| 34 | + content_view = window.findViewById(autoclass("android.R$id").content) |
| 35 | + |
| 36 | + try: |
| 37 | + WindowCompat = autoclass("androidx.core.view.WindowCompat") |
| 38 | + inset_controller = WindowCompat.getInsetsController(window, decor_view) |
| 39 | + except Exception as e: |
| 40 | + inset_controller = None |
| 41 | + |
| 42 | + def parse_color(value): |
| 43 | + if isinstance(value, str): |
| 44 | + return Color.parseColor(value) |
| 45 | + elif isinstance(value, (list, tuple)) and len(value) == 4: |
| 46 | + r, g, b, a = value |
| 47 | + return Color.argb(a, r, g, b) |
| 48 | + else: |
| 49 | + raise ValueError("Color must be hex string or RGBA tuple") |
| 50 | + |
| 51 | + def apply_system_bars(): |
| 52 | + status_color_int = parse_color(status_bar_color) |
| 53 | + navigation_color_int = parse_color(navigation_bar_color) |
| 54 | + |
| 55 | + # Beleive me, I once drew `dark icons over dark` and `light icons over light` but this won't happen ever again! |
| 56 | + if (Build_VERSION.SDK_INT >= 30): |
| 57 | + # API 30+ (Android 10+) |
| 58 | + if inset_controller and "WindowInsetsControllerCompat" in str(type(inset_controller)): |
| 59 | + # Compat wrapper (AndroidX) |
| 60 | + # I suggest to include androidx in builds, it actually helps! |
| 61 | + if icon_style == "Dark": |
| 62 | + inset_controller.setAppearanceLightStatusBars(False) |
| 63 | + inset_controller.setAppearanceLightNavigationBars(False) |
| 64 | + else: |
| 65 | + inset_controller.setAppearanceLightStatusBars(True) |
| 66 | + inset_controller.setAppearanceLightNavigationBars(True) |
| 67 | + else: |
| 68 | + # Platform controller |
| 69 | + controller = inset_controller or window.getInsetsController() |
| 70 | + WindowInsetsController = autoclass("android.view.WindowInsetsController") |
| 71 | + if icon_style == "Dark": |
| 72 | + controller.setSystemBarsAppearance( |
| 73 | + 0, |
| 74 | + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
| 75 | + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, |
| 76 | + ) |
| 77 | + else: |
| 78 | + controller.setSystemBarsAppearance( |
| 79 | + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
| 80 | + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, |
| 81 | + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS |
| 82 | + | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS, |
| 83 | + ) |
| 84 | + |
| 85 | + else: |
| 86 | + # Legacy flags for API 23–29 |
| 87 | + # Yepp, python3.14 with ndk 28c doesn't support building for android <= 11 with 32 bit armeabi-v7a cpu so this may never be called but who knows?? |
| 88 | + visibility_flags = decor_view.getSystemUiVisibility() |
| 89 | + |
| 90 | + if icon_style == "Dark": |
| 91 | + visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
| 92 | + if Build_VERSION.SDK_INT >= 26: |
| 93 | + visibility_flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR |
| 94 | + else: |
| 95 | + visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR |
| 96 | + if Build_VERSION.SDK_INT >= 26: |
| 97 | + visibility_flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR |
| 98 | + |
| 99 | + decor_view.setSystemUiVisibility(visibility_flags) |
| 100 | + |
| 101 | + # Oops!! android 15+ needs a listener |
| 102 | + if Build_VERSION.SDK_INT >= 35: |
| 103 | + |
| 104 | + class InsetsListener(PythonJavaClass): |
| 105 | + __javainterfaces__ = [ |
| 106 | + "android/view/View$OnApplyWindowInsetsListener" |
| 107 | + ] |
| 108 | + __javacontext__ = "app" |
| 109 | + |
| 110 | + def __init__(self, status_color, navigation_color): |
| 111 | + super().__init__() |
| 112 | + self.status_color = status_color |
| 113 | + self.navigation_color = navigation_color |
| 114 | + |
| 115 | + @java_method( |
| 116 | + "(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;" |
| 117 | + ) |
| 118 | + def onApplyWindowInsets(self, view, insets): |
| 119 | + try: |
| 120 | + status_insets = insets.getInsets( |
| 121 | + WindowInsetsType.statusBars() |
| 122 | + ) |
| 123 | + nav_insets = insets.getInsets( |
| 124 | + WindowInsetsType.navigationBars() |
| 125 | + ) |
| 126 | + |
| 127 | + top_pad = status_insets.top if pad_status else 0 |
| 128 | + bottom_pad = nav_insets.bottom if pad_nav else 0 |
| 129 | + |
| 130 | + content_view.setPadding(0, top_pad, 0, bottom_pad) |
| 131 | + content_view.setBackgroundColor(self.status_color) |
| 132 | + |
| 133 | + window.setNavigationBarColor(self.navigation_color) |
| 134 | + except Exception as e: |
| 135 | + print("Insets error:", e) |
| 136 | + import traceback |
| 137 | + traceback.print_exc() |
| 138 | + return insets |
| 139 | + |
| 140 | + listener = InsetsListener(status_color_int, navigation_color_int) |
| 141 | + # I don't know why but sometimes pyjnius failed to find invoke, maybe due to garbage collection and so I made a reference |
| 142 | + activity._system_ui_listener = listener |
| 143 | + decor_view.setOnApplyWindowInsetsListener(listener) |
| 144 | + decor_view.requestApplyInsets() |
| 145 | + else: |
| 146 | + window.setStatusBarColor(status_color_int) |
| 147 | + window.setNavigationBarColor(navigation_color_int) |
| 148 | + |
| 149 | + # even if it fails it fails in a separate thread |
| 150 | + Runnable(apply_system_bars)() |
0 commit comments