88# All icons took: 1457ms
99# All icons took: 1250ms
1010# Most of this time is actually spent reading and parsing manifests.
11-
12- import uos
1311import lvgl as lv
14-
1512import mpos .apps
1613import mpos .ui
1714from mpos .content .pm import PackageManager
1815from mpos import Activity
16+ import time
17+ import uhashlib
18+ import ubinascii
19+
1920
2021class Launcher (Activity ):
22+ def __init__ (self ):
23+ super ().__init__ ()
24+ # Cache of the last app list + a quick hash of the icons
25+ self ._last_app_list = None # list of tuples (name, path, icon_hash)
26+ self ._last_ui_built = False # was UI built at least once?
2127
2228def onCreate (self ):
2329print ("launcher.py onCreate()" )
2430main_screen = lv .obj ()
2531main_screen .set_style_border_width (0 ,lv .PART .MAIN )
2632main_screen .set_style_radius (0 ,0 )
27- main_screen .set_pos (0 ,mpos .ui .topmenu .NOTIFICATION_BAR_HEIGHT )# leave some margin for the notification bar
28- #main_screen.set_size(lv.pct(100), lv.pct(100))
33+ main_screen .set_pos (0 ,mpos .ui .topmenu .NOTIFICATION_BAR_HEIGHT )
2934main_screen .set_style_pad_hor (mpos .ui .pct_of_display_width (2 ),0 )
3035main_screen .set_style_pad_ver (mpos .ui .topmenu .NOTIFICATION_BAR_HEIGHT ,0 )
3136main_screen .set_flex_flow (lv .FLEX_FLOW .ROW_WRAP )
3237self .setContentView (main_screen )
3338
39+ # ------------------------------------------------------------------
40+ # Helper: compute a cheap hash of a file (or return None if missing)
41+ @staticmethod
42+ def _hash_file (path ):
43+ try :
44+ with open (path ,"rb" )as f :
45+ h = uhashlib .sha1 ()
46+ while True :
47+ data = f .read (1024 )
48+ if not data :
49+ break
50+ h .update (data )
51+ return ubinascii .hexlify (h .digest ()).decode ()
52+ except Exception :
53+ return None
54+
55+ # ------------------------------------------------------------------
3456def onResume (self ,screen ):
35- # Grid parameters
36- icon_size = 64 # Adjust based on your display
37- label_height = 24
38- iconcont_width = icon_size + label_height
39- iconcont_height = icon_size + label_height
57+ # ------------------------------------------------------------------
58+ # 1. Build a *compact* representation of the current app list
59+ current_apps = []
60+ for app in PackageManager .get_app_list ():
61+ if app .category == "launcher" :
62+ continue
63+ icon_path = f"{ app .installed_path } /res/mipmap-mdpi/icon_64x64.png"
64+ icon_hash = Launcher ._hash_file (icon_path )# cheap SHA-1 of the icon file
65+ current_apps .append ((app .name ,app .installed_path ,icon_hash ))
4066
41- import time
67+ # ------------------------------------------------------------------
68+ # 2. Compare with the cached list – if identical we skip UI rebuild
4269start = time .ticks_ms ()
70+ rebuild_needed = True
71+
72+ if (self ._last_app_list is not None and
73+ len (self ._last_app_list )== len (current_apps )):
74+ # element-wise compare (name, path, icon_hash)
75+ if all (a == b for a ,b in zip (self ._last_app_list ,current_apps )):
76+ rebuild_needed = False
77+
78+ if not rebuild_needed :
79+ end = time .ticks_ms ()
80+ print (f"Redraw icons took:{ end - start } ms (cached – no change)" )
81+ return
4382
83+ # ------------------------------------------------------------------
84+ # 3. UI needs (re)building – clear screen and create widgets
4485screen .clean ()
4586
46- # Get the group for focusable objects
4787focusgroup = lv .group_get_default ()
4888if not focusgroup :
4989print ("WARNING: could not get default focusgroup" )
5090
51- # Create UI for each app
91+ # Grid parameters
92+ icon_size = 64
93+ label_height = 24
94+ iconcont_width = icon_size + label_height
95+ iconcont_height = icon_size + label_height
96+
5297for app in PackageManager .get_app_list ():
5398if app .category == "launcher" :
54- print ("Skipping launcher app from launcher apps..." )
5599continue
100+
56101app_name = app .name
57102app_dir_fullpath = app .installed_path
58103print (f"Adding app{ app_name } from{ app_dir_fullpath } " )
59- # Create container for each app (icon + label)
104+
105+ # ----- container ------------------------------------------------
60106app_cont = lv .obj (screen )
61107app_cont .set_size (iconcont_width ,iconcont_height )
62108app_cont .set_style_border_width (0 ,lv .PART .MAIN )
63109app_cont .set_style_pad_all (0 ,0 )
64- app_cont .set_style_bg_opa (lv .OPA .TRANSP ,0 ) # prevent default style from adding slight gray to this container
110+ app_cont .set_style_bg_opa (lv .OPA .TRANSP ,0 )
65111app_cont .set_scrollbar_mode (lv .SCROLLBAR_MODE .OFF )
66- # Load and display icon
112+
113+ # ----- icon ----------------------------------------------------
67114icon_path = f"{ app_dir_fullpath } /res/mipmap-mdpi/icon_64x64.png"
68115image = lv .image (app_cont )
69116try :
70117image .set_src (Launcher .load_icon (icon_path ))
71118except Exception as e :
72- print (f"Error loading icon{ icon_path } :{ e } - loading default icon " )
119+ print (f"Error loading icon{ icon_path } :{ e } - loading default" )
73120icon_path = "builtin/res/mipmap-mdpi/default_icon_64x64.png"
74121try :
75122image .set_src (Launcher .load_icon (icon_path ))
76123except Exception as e :
77- print (f"Error loading defaulticon { icon_path } :{ e } - using symbol" )
124+ print (f"Error loading default{ icon_path } :{ e } - using symbol" )
78125image .set_src (lv .SYMBOL .STOP )
126+
79127image .align (lv .ALIGN .TOP_MID ,0 ,0 )
80128image .set_size (icon_size ,icon_size )
129+
130+ # ----- label ---------------------------------------------------
81131label = lv .label (app_cont )
82- label .set_text (app_name )# Use app_name directly
132+ label .set_text (app_name )
83133label .set_long_mode (lv .label .LONG_MODE .WRAP )
84134label .set_width (iconcont_width )
85135label .align (lv .ALIGN .BOTTOM_MID ,0 ,0 )
86136label .set_style_text_align (lv .TEXT_ALIGN .CENTER ,0 )
87- app_cont .add_event_cb (lambda e ,fullname = app .fullname :mpos .apps .start_app (fullname ),lv .EVENT .CLICKED ,None )
88- app_cont .add_event_cb (lambda e ,app_cont = app_cont :self .focus_app_cont (app_cont ),lv .EVENT .FOCUSED ,None )
89- app_cont .add_event_cb (lambda e ,app_cont = app_cont :self .defocus_app_cont (app_cont ),lv .EVENT .DEFOCUSED ,None )
137+
138+ # ----- events --------------------------------------------------
139+ app_cont .add_event_cb (
140+ lambda e ,fullname = app .fullname :mpos .apps .start_app (fullname ),
141+ lv .EVENT .CLICKED ,None )
142+ app_cont .add_event_cb (
143+ lambda e ,cont = app_cont :self .focus_app_cont (cont ),
144+ lv .EVENT .FOCUSED ,None )
145+ app_cont .add_event_cb (
146+ lambda e ,cont = app_cont :self .defocus_app_cont (cont ),
147+ lv .EVENT .DEFOCUSED ,None )
148+
90149if focusgroup :
91150focusgroup .add_obj (app_cont )
92-
151+
152+ # ------------------------------------------------------------------
153+ # 4. Store the new representation for the next resume
154+ self ._last_app_list = current_apps
155+ self ._last_ui_built = True
156+
93157end = time .ticks_ms ()
94- print (f"Redraw icons took:{ end - start } ms" )
158+ print (f"Redraw icons took:{ end - start } ms (full rebuild) " )
95159
160+ # ------------------------------------------------------------------
96161@staticmethod
97162def load_icon (icon_path ):
98163with open (icon_path ,'rb' )as f :
@@ -103,12 +168,11 @@ def load_icon(icon_path):
103168 })
104169return image_dsc
105170
171+ # ------------------------------------------------------------------
106172def focus_app_cont (self ,app_cont ):
107- #print(f"app_cont {app_cont} focused, setting border...")
108- app_cont .set_style_border_color (lv .theme_get_color_primary (None ),lv .PART .MAIN )
173+ app_cont .set_style_border_color (lv .theme_get_color_primary (None ),lv .PART .MAIN )
109174app_cont .set_style_border_width (1 ,lv .PART .MAIN )
110- app_cont .scroll_to_view (True )# scroll to bring it into view
175+ app_cont .scroll_to_view (True )
111176
112177def defocus_app_cont (self ,app_cont ):
113- #print(f"app_cont {app_cont} defocused, unsetting border...")
114178app_cont .set_style_border_width (0 ,lv .PART .MAIN )