1- from jnius import autoclass
1+ from android import mActivity
2+ from android .runnable import run_on_ui_thread
3+ from jnius import autoclass , java_method , PythonJavaClass
4+ from typing import Literal
5+
26from kivy .core .window import Window
37
4- from android import mActivity
8+ __all__ = (
9+ "get_cutout_pos" ,
10+ "get_cutout_size" ,
11+ "get_width_of_bar" ,
12+ "get_height_of_bar" ,
13+ "get_size_of_bar" ,
14+ "get_width_of_bar" ,
15+ "get_cutout_mode" ,
16+ "update_system_ui" ,
17+ )
18+
19+ Color = autoclass ("android.graphics.Color" )
20+ Build_VERSION = autoclass ("android.os.Build$VERSION" )
21+ WindowInsetsType = autoclass ("android.view.WindowInsets$Type" )
22+ View = autoclass ("android.view.View" )
23+ window = mActivity .getWindow ()
24+ decor_view = window .getDecorView ()
25+ content_view = window .findViewById (autoclass ("android.R$id" ).content )
26+
27+
28+ def parse_color (value ):
29+ if isinstance (value , str ):
30+ return Color .parseColor (value )
31+ elif isinstance (value , (list , tuple )) and len (value ) == 4 :
32+ r , g , b , a = value
33+ return Color .argb (a , r , g , b )
34+ else :
35+ raise ValueError ("Color must be hex string or RGBA tuple" )
36+
37+
38+ # Oops!! android 15+ needs a listener
39+ if Build_VERSION .SDK_INT >= 35 :
40+
41+ class InsetsListener (PythonJavaClass ):
42+ __javainterfaces__ = ["android/view/View$OnApplyWindowInsetsListener" ]
43+ __javacontext__ = "app"
44+
45+ def __init__ (self , status_color , navigation_color , pad_status , pad_nav ):
46+ super ().__init__ ()
47+ self .status_color = status_color
48+ self .navigation_color = navigation_color
49+ self .pad_status = pad_status
50+ self .pad_nav = pad_nav
551
6- __all__ = ('get_cutout_pos' , 'get_cutout_size' , 'get_width_of_bar' ,
7- 'get_height_of_bar' , 'get_size_of_bar' , 'get_width_of_bar' ,
8- 'get_cutout_mode' )
52+ @java_method (
53+ "(Landroid/view/View;Landroid/view/WindowInsets;)Landroid/view/WindowInsets;"
54+ )
55+ def onApplyWindowInsets (self , view , insets ):
56+ try :
57+ status_insets = insets .getInsets (WindowInsetsType .statusBars ())
58+ nav_insets = insets .getInsets (WindowInsetsType .navigationBars ())
59+
60+ top_pad = status_insets .top if self .pad_status else 0
61+ bottom_pad = nav_insets .bottom if self .pad_nav else 0
62+
63+ content_view .setPadding (0 , top_pad , 0 , bottom_pad )
64+ content_view .setBackgroundColor (self .status_color )
65+
66+ window .setNavigationBarColor (self .navigation_color )
67+ except Exception as e :
68+ print ("Insets error:" , e )
69+ import traceback
70+
71+ traceback .print_exc ()
72+ return insets
73+
74+ _global_listener : InsetsListener = None
975
1076
1177def _core_cutout ():
@@ -17,7 +83,7 @@ def _core_cutout():
1783
1884def get_cutout_pos ():
1985 """Get position of the display-cutout.
20- Returns integer for each positions (xy)
86+ Returns integer for each positions (xy)
2187 """
2288 try :
2389 cutout = _core_cutout ()
@@ -29,36 +95,37 @@ def get_cutout_pos():
2995
3096def get_cutout_size ():
3197 """Get the size (xy) of the front camera.
32- Returns size with float values
98+ Returns size with float values
3399 """
34100 try :
35101 cutout = _core_cutout ()
36102 return float (cutout .width ()), float (cutout .height ())
37103 except Exception :
38104 # Doesn't have a camera builtin with the display
39- return 0. , 0.
105+ return 0.0 , 0.0
40106
41107
42108def get_height_of_bar (bar_target = None ):
43109 """Get the height of either statusbar or navigationbar
44- bar_target = status or navigation and defaults to status
110+ bar_target = status or navigation and defaults to status
45111 """
46- bar_target = bar_target or ' status'
112+ bar_target = bar_target or " status"
47113
48- if bar_target not in (' status' , ' navigation' ):
114+ if bar_target not in (" status" , " navigation" ):
49115 raise Exception ("bar_target must be 'status' or 'navigation'" )
50116
51117 try :
52- displayMetrics = autoclass (' android.util.DisplayMetrics' )
118+ displayMetrics = autoclass (" android.util.DisplayMetrics" )
53119 mActivity .getWindowManager ().getDefaultDisplay ().getMetrics (displayMetrics ())
54120 resources = mActivity .getResources ()
55- resourceId = resources .getIdentifier (f'{ bar_target } _bar_height' , 'dimen' ,
56- 'android' )
121+ resourceId = resources .getIdentifier (
122+ f"{ bar_target } _bar_height" , "dimen" , "android"
123+ )
57124
58125 return float (max (resources .getDimensionPixelSize (resourceId ), 0 ))
59126 except Exception :
60127 # Getting the size is not supported on older Androids
61- return 0.
128+ return 0.0
62129
63130
64131def get_width_of_bar (bar_target = None ):
@@ -68,32 +135,156 @@ def get_width_of_bar(bar_target=None):
68135
69136def get_size_of_bar (bar_target = None ):
70137 """Get the size of either statusbar or navigationbar
71- bar_target = status or navigation and defaults to status
138+ bar_target = status or navigation and defaults to status
72139 """
73140 return get_width_of_bar (), get_height_of_bar (bar_target )
74141
75142
76143def get_heights_of_both_bars ():
77144 """Return heights of both bars"""
78- return get_height_of_bar (' status' ), get_height_of_bar (' navigation' )
145+ return get_height_of_bar (" status" ), get_height_of_bar (" navigation" )
79146
80147
81148def get_cutout_mode ():
82149 """Return mode for cutout supported applications"""
83- BuildVersion = autoclass (' android.os.Build$VERSION' )
150+ BuildVersion = autoclass (" android.os.Build$VERSION" )
84151 cutout_modes = {}
85152
86153 if BuildVersion .SDK_INT >= 28 :
87- LayoutParams = autoclass (' android.view.WindowManager$LayoutParams' )
154+ LayoutParams = autoclass (" android.view.WindowManager$LayoutParams" )
88155 window = mActivity .getWindow ()
89156 layout_params = window .getAttributes ()
90157 cutout_mode = layout_params .layoutInDisplayCutoutMode
91- cutout_modes .update ({LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT : 'default' ,
92- LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES : 'shortEdges' })
158+ cutout_modes .update (
159+ {
160+ LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT : "default" ,
161+ LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES : "shortEdges" ,
162+ }
163+ )
93164
94165 if BuildVersion .SDK_INT >= 30 :
95- cutout_modes [LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS ] = ' always'
166+ cutout_modes [LayoutParams .LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS ] = " always"
96167
97- return cutout_modes .get (cutout_mode , ' never' )
168+ return cutout_modes .get (cutout_mode , " never" )
98169
99170 return None
171+
172+
173+ @run_on_ui_thread
174+ def set_immersive_mode (hide_nav = True , hide_status = True , disable_contrast = True ):
175+ """
176+ Configures Android UI visibility, optionally hiding the navigation and status bars,
177+ and optionally disabling system bar contrast on Android Q (API 29+).
178+ """
179+ if not mActivity :
180+ return
181+
182+ window = mActivity .getWindow ()
183+ decor_view = window .getDecorView ()
184+
185+ ui_flags = View .SYSTEM_UI_FLAG_LAYOUT_STABLE
186+
187+ if hide_nav :
188+ ui_flags |= View .SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
189+ ui_flags |= View .SYSTEM_UI_FLAG_HIDE_NAVIGATION
190+
191+ if hide_status :
192+ ui_flags |= View .SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
193+ ui_flags |= View .SYSTEM_UI_FLAG_FULLSCREEN
194+
195+ if hide_nav or hide_status :
196+ ui_flags |= View .SYSTEM_UI_FLAG_IMMERSIVE_STICKY
197+
198+ decor_view .setSystemUiVisibility (ui_flags )
199+
200+ if disable_contrast and Build_VERSION .SDK_INT >= 29 :
201+ window .setNavigationBarContrastEnforced (False )
202+ window .setStatusBarContrastEnforced (False )
203+
204+
205+ @run_on_ui_thread
206+ def update_system_ui (
207+ status_bar_color : list [float ] | str ,
208+ navigation_bar_color : list [float ] | str ,
209+ icon_style : Literal ["Light" , "Dark" ] = "Dark" ,
210+ pad_status : bool = True ,
211+ pad_nav : bool = False ,
212+ ) -> None :
213+ """
214+ Provides control of colors for the status and navigation bar and also handle insets padding on Android 15 and above.
215+
216+ For `status_bar_color` and `navigation_bar_color` either provide a hex color code or rgba (tuple or list) values.
217+ `pad_status` and `pad_nav` will take effect only above Android 15.
218+ IF `icon_style` IS `Dark` THE ICONS WILL BE DARK.
219+ IF `icon_style` IS `Light` THE ICONS WILL BE LIGHT.
220+
221+ Adapted from https://github.com/CarbonKivy/CarbonKivy/blob/39e360314a3885f3b462add4475e6c609b5bef53/carbonkivy/utils.py#L43
222+ """
223+
224+ try :
225+ WindowCompat = autoclass ("androidx.core.view.WindowCompat" )
226+ inset_controller = WindowCompat .getInsetsController (window , decor_view )
227+ except Exception :
228+ inset_controller = None
229+
230+ status_color_int = parse_color (status_bar_color )
231+ navigation_color_int = parse_color (navigation_bar_color )
232+
233+ # Beleive me, I once drew `dark icons over dark` and `light icons over light` but this won't happen ever again!
234+ if Build_VERSION .SDK_INT >= 30 :
235+ # API 30+ (Android 10+)
236+ if inset_controller and "WindowInsetsControllerCompat" in str (
237+ type (inset_controller )
238+ ):
239+ # Compat wrapper (AndroidX)
240+ # I suggest to include androidx in builds, it actually helps!
241+ if icon_style == "Light" :
242+ inset_controller .setAppearanceLightStatusBars (False )
243+ inset_controller .setAppearanceLightNavigationBars (False )
244+ else :
245+ inset_controller .setAppearanceLightStatusBars (True )
246+ inset_controller .setAppearanceLightNavigationBars (True )
247+ else :
248+ # Platform controller
249+ controller = window .getInsetsController ()
250+ WindowInsetsController = autoclass ("android.view.WindowInsetsController" )
251+ if icon_style == "Light" :
252+ controller .setSystemBarsAppearance (
253+ 0 ,
254+ WindowInsetsController .APPEARANCE_LIGHT_STATUS_BARS
255+ | WindowInsetsController .APPEARANCE_LIGHT_NAVIGATION_BARS ,
256+ )
257+ else :
258+ controller .setSystemBarsAppearance (
259+ WindowInsetsController .APPEARANCE_LIGHT_STATUS_BARS
260+ | WindowInsetsController .APPEARANCE_LIGHT_NAVIGATION_BARS ,
261+ WindowInsetsController .APPEARANCE_LIGHT_STATUS_BARS
262+ | WindowInsetsController .APPEARANCE_LIGHT_NAVIGATION_BARS ,
263+ )
264+ else :
265+ # Legacy flags for API 23–29
266+ # 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??
267+ visibility_flags = decor_view .getSystemUiVisibility ()
268+
269+ if icon_style == "Light" :
270+ visibility_flags &= ~ View .SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
271+ if Build_VERSION .SDK_INT >= 26 :
272+ visibility_flags &= ~ View .SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
273+ else :
274+ visibility_flags |= View .SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
275+ if Build_VERSION .SDK_INT >= 26 :
276+ visibility_flags |= View .SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
277+
278+ decor_view .setSystemUiVisibility (visibility_flags )
279+
280+ if Build_VERSION .SDK_INT >= 35 :
281+ # I don't know why but sometimes pyjnius failed to find invoke, maybe due to garbage collection and so I made a reference
282+ global _global_listener
283+ _global_listener = InsetsListener (
284+ status_color_int , navigation_color_int , pad_status , pad_nav
285+ )
286+ decor_view .setOnApplyWindowInsetsListener (_global_listener )
287+ decor_view .requestApplyInsets ()
288+ else :
289+ window .setStatusBarColor (status_color_int )
290+ window .setNavigationBarColor (navigation_color_int )
0 commit comments