You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
Cleanup WidgetAnimator framework
This commit is contained in:
@@ -39,7 +39,7 @@ from .ui.topmenu import open_bar, close_bar, open_drawer, drawer_open, NOTIFICAT
|
||||
from .ui.focus import save_and_clear_current_focusgroup
|
||||
from .ui.gesture_navigation import handle_back_swipe, handle_top_swipe
|
||||
from .ui.util import shutdown, set_foreground_app, get_foreground_app
|
||||
from .ui.anim import smooth_show, smooth_hide
|
||||
from .ui.widget_animator import WidgetAnimator
|
||||
from .ui import focus_direction
|
||||
|
||||
# Utility modules
|
||||
@@ -78,7 +78,7 @@ __all__ = [
|
||||
"save_and_clear_current_focusgroup",
|
||||
"handle_back_swipe", "handle_top_swipe",
|
||||
"shutdown", "set_foreground_app", "get_foreground_app",
|
||||
"smooth_show", "smooth_hide",
|
||||
"WidgetAnimator",
|
||||
"focus_direction",
|
||||
# Testing utilities
|
||||
"wait_for_render", "capture_screenshot", "simulate_click", "get_widget_coords",
|
||||
|
||||
@@ -11,7 +11,7 @@ from .event import get_event_name, print_event
|
||||
from .util import shutdown, set_foreground_app, get_foreground_app
|
||||
from .setting_activity import SettingActivity
|
||||
from .settings_activity import SettingsActivity
|
||||
from .anim import smooth_show, smooth_hide
|
||||
from .widget_animator import WidgetAnimator
|
||||
from . import focus_direction
|
||||
|
||||
# main_display is assigned by board-specific initialization code
|
||||
@@ -28,6 +28,6 @@ __all__ = [
|
||||
"shutdown", "set_foreground_app", "get_foreground_app",
|
||||
"SettingActivity",
|
||||
"SettingsActivity",
|
||||
"smooth_show", "smooth_hide",
|
||||
"WidgetAnimator",
|
||||
"focus_direction"
|
||||
]
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import lvgl as lv
|
||||
|
||||
|
||||
def safe_widget_access(callback):
|
||||
"""
|
||||
Wrapper to safely access a widget, catching LvReferenceError.
|
||||
|
||||
If the widget has been deleted, the callback is silently skipped.
|
||||
This prevents crashes when animations try to access deleted widgets.
|
||||
|
||||
Args:
|
||||
callback: Function to call (should access a widget)
|
||||
|
||||
Returns:
|
||||
None (always, even if callback returns a value)
|
||||
"""
|
||||
try:
|
||||
callback()
|
||||
except Exception as e:
|
||||
# Check if it's an LvReferenceError (widget was deleted)
|
||||
if "LvReferenceError" in str(type(e).__name__) or "Referenced object was deleted" in str(e):
|
||||
# Widget was deleted - silently ignore
|
||||
pass
|
||||
else:
|
||||
# Some other error - re-raise it
|
||||
raise
|
||||
|
||||
|
||||
class WidgetAnimator:
|
||||
|
||||
# def __init__(self):
|
||||
# self.animations = {} # Store animations for each widget
|
||||
|
||||
# def stop_animation(self, widget):
|
||||
# """Stop any running animation for the widget."""
|
||||
# if widget in self.animations:
|
||||
# self.animations[widget].delete()
|
||||
# del self.animations[widget]
|
||||
|
||||
|
||||
# show_widget and hide_widget could have a (lambda) callback that sets the final state (eg: drawer_open) at the end
|
||||
@staticmethod
|
||||
def show_widget(widget, anim_type="fade", duration=500, delay=0):
|
||||
lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches
|
||||
anim = lv.anim_t()
|
||||
anim.init()
|
||||
anim.set_var(widget)
|
||||
anim.set_delay(delay)
|
||||
anim.set_duration(duration)
|
||||
# Clear HIDDEN flag to make widget visible for animation:
|
||||
anim.set_start_cb(lambda *args: safe_widget_access(lambda: widget.remove_flag(lv.obj.FLAG.HIDDEN)))
|
||||
|
||||
if anim_type == "fade":
|
||||
# Create fade-in animation (opacity from 0 to 255)
|
||||
anim.set_values(0, 255)
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_style_opa(value, 0)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Ensure opacity is reset after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_style_opa(255, 0)))
|
||||
elif anim_type == "slide_down":
|
||||
print("doing slide_down")
|
||||
# Create slide-down animation (y from -height to original y)
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y - height, original_y)
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Reset y position after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_y(original_y)))
|
||||
else: # "slide_up":
|
||||
# Create slide-up animation (y from +height to original y)
|
||||
# Seems to cause scroll bars to be added somehow if done to a keyboard at the bottom of the screen...
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y + height, original_y)
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Reset y position after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_y(original_y)))
|
||||
|
||||
anim.start()
|
||||
return anim
|
||||
|
||||
@staticmethod
|
||||
def hide_widget(widget, anim_type="fade", duration=500, delay=0, hide=True):
|
||||
lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches
|
||||
anim = lv.anim_t()
|
||||
anim.init()
|
||||
anim.set_var(widget)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
|
||||
"""Hide a widget with an animation (fade or slide)."""
|
||||
if anim_type == "fade":
|
||||
# Create fade-out animation (opacity from 255 to 0)
|
||||
anim.set_values(255, 0)
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_style_opa(value, 0)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, hide=hide)))
|
||||
elif anim_type == "slide_down":
|
||||
# Create slide-down animation (y from original y to +height)
|
||||
# Seems to cause scroll bars to be added somehow if done to a keyboard at the bottom of the screen...
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y, original_y + height)
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, original_y, hide)))
|
||||
else: # "slide_up":
|
||||
print("hide with slide_up")
|
||||
# Create slide-up animation (y from original y to -height)
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y, original_y - height)
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: WidgetAnimator.hide_complete_cb(widget, original_y, hide)))
|
||||
|
||||
anim.start()
|
||||
return anim
|
||||
|
||||
@staticmethod
|
||||
def change_widget(widget, anim_type="interpolate", duration=5000, delay=0, begin_value=0, end_value=100, display_change=None):
|
||||
"""
|
||||
Animate a widget's text by interpolating between begin_value and end_value.
|
||||
|
||||
Args:
|
||||
widget: The widget to animate (should have set_text method)
|
||||
anim_type: Type of animation (currently "interpolate" is supported)
|
||||
duration: Animation duration in milliseconds
|
||||
delay: Animation delay in milliseconds
|
||||
begin_value: Starting value for interpolation
|
||||
end_value: Ending value for interpolation
|
||||
display_change: callback to display the change in the UI
|
||||
|
||||
Returns:
|
||||
The animation object
|
||||
"""
|
||||
lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches
|
||||
anim = lv.anim_t()
|
||||
anim.init()
|
||||
anim.set_var(widget)
|
||||
anim.set_delay(delay)
|
||||
anim.set_duration(duration)
|
||||
|
||||
if anim_type == "interpolate":
|
||||
print(f"Create interpolation animation (value from {begin_value} to {end_value})")
|
||||
anim.set_values(begin_value, end_value)
|
||||
if display_change is not None:
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: display_change(value)))
|
||||
# Ensure final value is set after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: display_change(end_value)))
|
||||
else:
|
||||
anim.set_custom_exec_cb(lambda anim, value: safe_widget_access(lambda: widget.set_text(str(value))))
|
||||
# Ensure final value is set after animation
|
||||
anim.set_completed_cb(lambda *args: safe_widget_access(lambda: widget.set_text(str(end_value))))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
else:
|
||||
print(f"change_widget: unknown anim_type {anim_type}")
|
||||
return
|
||||
|
||||
anim.start()
|
||||
return anim
|
||||
|
||||
@staticmethod
|
||||
def hide_complete_cb(widget, original_y=None, hide=True):
|
||||
#print("hide_complete_cb")
|
||||
if hide:
|
||||
widget.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
if original_y:
|
||||
widget.set_y(original_y) # in case it shifted slightly due to rounding etc
|
||||
|
||||
|
||||
def smooth_show(widget, duration=500, delay=0):
|
||||
return WidgetAnimator.show_widget(widget, anim_type="fade", duration=duration, delay=delay)
|
||||
|
||||
def smooth_hide(widget, hide=True, duration=500, delay=0):
|
||||
return WidgetAnimator.hide_widget(widget, anim_type="fade", duration=duration, delay=delay, hide=hide)
|
||||
@@ -3,7 +3,7 @@ import lvgl as lv
|
||||
from ..config import SharedPreferences
|
||||
from ..app.activity import Activity
|
||||
from .display import DisplayMetrics
|
||||
from . import anim
|
||||
from .widget_animator import WidgetAnimator
|
||||
|
||||
class CameraSettingsActivity(Activity):
|
||||
|
||||
@@ -354,11 +354,11 @@ class CameraSettingsActivity(Activity):
|
||||
def exposure_ctrl_changed(e=None):
|
||||
is_auto = aec_checkbox.get_state() & lv.STATE.CHECKED
|
||||
if is_auto:
|
||||
anim.smooth_hide(me_cont, duration=1000)
|
||||
anim.smooth_show(ae_cont, delay=1000)
|
||||
WidgetAnimator.smooth_hide(me_cont, duration=1000)
|
||||
WidgetAnimator.smooth_show(ae_cont, delay=1000)
|
||||
else:
|
||||
anim.smooth_hide(ae_cont, duration=1000)
|
||||
anim.smooth_show(me_cont, delay=1000)
|
||||
WidgetAnimator.smooth_hide(ae_cont, duration=1000)
|
||||
WidgetAnimator.smooth_show(me_cont, delay=1000)
|
||||
|
||||
aec_checkbox.add_event_cb(exposure_ctrl_changed, lv.EVENT.VALUE_CHANGED, None)
|
||||
exposure_ctrl_changed()
|
||||
@@ -382,9 +382,9 @@ class CameraSettingsActivity(Activity):
|
||||
is_auto = agc_checkbox.get_state() & lv.STATE.CHECKED
|
||||
gain_slider = self.ui_controls["agc_gain"]
|
||||
if is_auto:
|
||||
anim.smooth_hide(agc_cont, duration=1000)
|
||||
WidgetAnimator.smooth_hide(agc_cont, duration=1000)
|
||||
else:
|
||||
anim.smooth_show(agc_cont, duration=1000)
|
||||
WidgetAnimator.smooth_show(agc_cont, duration=1000)
|
||||
|
||||
agc_checkbox.add_event_cb(gain_ctrl_changed, lv.EVENT.VALUE_CHANGED, None)
|
||||
gain_ctrl_changed()
|
||||
@@ -414,9 +414,9 @@ class CameraSettingsActivity(Activity):
|
||||
def whitebal_changed(e=None):
|
||||
is_auto = wbcheckbox.get_state() & lv.STATE.CHECKED
|
||||
if is_auto:
|
||||
anim.smooth_hide(wb_cont, duration=1000)
|
||||
WidgetAnimator.smooth_hide(wb_cont, duration=1000)
|
||||
else:
|
||||
anim.smooth_show(wb_cont, duration=1000)
|
||||
WidgetAnimator.smooth_show(wb_cont, duration=1000)
|
||||
wbcheckbox.add_event_cb(whitebal_changed, lv.EVENT.VALUE_CHANGED, None)
|
||||
whitebal_changed()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import lvgl as lv
|
||||
from lvgl import LvReferenceError
|
||||
from .anim import smooth_show, smooth_hide
|
||||
from .widget_animator import WidgetAnimator
|
||||
from .view import back_screen
|
||||
from mpos.ui import topmenu as topmenu
|
||||
from .display import DisplayMetrics
|
||||
@@ -50,12 +50,12 @@ def _back_swipe_cb(event):
|
||||
should_show = not is_short_movement(dx, dy)
|
||||
if should_show != backbutton_visible:
|
||||
backbutton_visible = should_show
|
||||
smooth_show(backbutton) if should_show else smooth_hide(backbutton)
|
||||
WidgetAnimator.smooth_show(backbutton) if should_show else WidgetAnimator.smooth_hide(backbutton)
|
||||
backbutton.set_pos(round(x / 10), back_start_y)
|
||||
elif event_code == lv.EVENT.RELEASED:
|
||||
if backbutton_visible:
|
||||
backbutton_visible = False
|
||||
smooth_hide(backbutton)
|
||||
WidgetAnimator.smooth_hide(backbutton)
|
||||
if x > DisplayMetrics.width() / 5:
|
||||
if topmenu.drawer_open :
|
||||
topmenu.close_drawer()
|
||||
@@ -89,12 +89,12 @@ def _top_swipe_cb(event):
|
||||
should_show = not is_short_movement(dx, dy)
|
||||
if should_show != downbutton_visible:
|
||||
downbutton_visible = should_show
|
||||
smooth_show(downbutton) if should_show else smooth_hide(downbutton)
|
||||
WidgetAnimator.smooth_show(downbutton) if should_show else WidgetAnimator.smooth_hide(downbutton)
|
||||
downbutton.set_pos(down_start_x, round(y / 10))
|
||||
elif event_code == lv.EVENT.RELEASED:
|
||||
if downbutton_visible:
|
||||
downbutton_visible = False
|
||||
smooth_hide(downbutton)
|
||||
WidgetAnimator.smooth_hide(downbutton)
|
||||
dx = abs(x - down_start_x)
|
||||
dy = abs(y - down_start_y)
|
||||
if y > DisplayMetrics.height() / 5:
|
||||
|
||||
@@ -17,6 +17,7 @@ Usage:
|
||||
|
||||
import lvgl as lv
|
||||
import mpos.ui.theme
|
||||
from .widget_animator import WidgetAnimator
|
||||
|
||||
class MposKeyboard:
|
||||
"""
|
||||
@@ -247,7 +248,7 @@ class MposKeyboard:
|
||||
|
||||
def show_keyboard(self):
|
||||
self._saved_scroll_y = self._parent.get_scroll_y()
|
||||
mpos.ui.anim.smooth_show(self._keyboard, duration=500)
|
||||
WidgetAnimator.smooth_show(self._keyboard, duration=500)
|
||||
# Scroll to view on a timer because it will be hidden initially
|
||||
lv.timer_create(self.scroll_after_show, 250, None).set_repeat_count(1)
|
||||
# When this is done from a timer, focus styling is not applied so the user doesn't see which button is selected.
|
||||
@@ -259,7 +260,7 @@ class MposKeyboard:
|
||||
self.focus_on_keyboard()
|
||||
|
||||
def hide_keyboard(self):
|
||||
mpos.ui.anim.smooth_hide(self._keyboard, duration=500)
|
||||
WidgetAnimator.smooth_hide(self._keyboard, duration=500)
|
||||
# Do this after the hide so the scrollbars disappear automatically if not needed
|
||||
scroll_timer = lv.timer_create(self.scroll_back_after_hide,550,None).set_repeat_count(1)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import lvgl as lv
|
||||
from ..app.activity import Activity
|
||||
from .camera_activity import CameraActivity
|
||||
from .display import DisplayMetrics
|
||||
from . import anim
|
||||
from .widget_animator import WidgetAnimator
|
||||
from ..camera_manager import CameraManager
|
||||
|
||||
"""
|
||||
@@ -134,7 +134,7 @@ class SettingActivity(Activity):
|
||||
|
||||
def onStop(self, screen):
|
||||
if self.keyboard:
|
||||
anim.smooth_hide(self.keyboard)
|
||||
WidgetAnimator.smooth_hide(self.keyboard)
|
||||
|
||||
def radio_event_handler(self, event):
|
||||
print("radio_event_handler called")
|
||||
|
||||
@@ -5,7 +5,7 @@ import mpos.battery_voltage
|
||||
from .display_metrics import DisplayMetrics
|
||||
from .util import (get_foreground_app)
|
||||
from . import focus_direction
|
||||
from .anim import WidgetAnimator
|
||||
from .widget_animator import WidgetAnimator
|
||||
|
||||
NOTIFICATION_BAR_HEIGHT=24
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import lvgl as lv
|
||||
|
||||
|
||||
class WidgetAnimator:
|
||||
"""
|
||||
Utility for creating smooth, non-blocking animations on LVGL widgets.
|
||||
|
||||
Provides fade, slide, and value interpolation animations with automatic
|
||||
cleanup and safe widget access handling.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _safe_widget_access(callback):
|
||||
"""
|
||||
Wrapper to safely access a widget, catching LvReferenceError.
|
||||
|
||||
If the widget has been deleted, the callback is silently skipped.
|
||||
This prevents crashes when animations try to access deleted widgets.
|
||||
|
||||
Args:
|
||||
callback: Function to call (should access a widget)
|
||||
|
||||
Returns:
|
||||
None (always, even if callback returns a value)
|
||||
"""
|
||||
try:
|
||||
callback()
|
||||
except Exception as e:
|
||||
# Check if it's an LvReferenceError (widget was deleted)
|
||||
if "LvReferenceError" in str(type(e).__name__) or "Referenced object was deleted" in str(e):
|
||||
# Widget was deleted - silently ignore
|
||||
pass
|
||||
else:
|
||||
# Some other error - re-raise it
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def show_widget(widget, anim_type="fade", duration=500, delay=0):
|
||||
"""
|
||||
Show a widget with an animation.
|
||||
|
||||
Args:
|
||||
widget (lv.obj): The widget to show
|
||||
anim_type (str): Animation type - "fade", "slide_down", or "slide_up" (default: "fade")
|
||||
duration (int): Animation duration in milliseconds (default: 500)
|
||||
delay (int): Animation delay in milliseconds (default: 0)
|
||||
|
||||
Returns:
|
||||
The animation object
|
||||
"""
|
||||
lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches
|
||||
anim = lv.anim_t()
|
||||
anim.init()
|
||||
anim.set_var(widget)
|
||||
anim.set_delay(delay)
|
||||
anim.set_duration(duration)
|
||||
# Clear HIDDEN flag to make widget visible for animation:
|
||||
anim.set_start_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.remove_flag(lv.obj.FLAG.HIDDEN)))
|
||||
|
||||
if anim_type == "fade":
|
||||
# Create fade-in animation (opacity from 0 to 255)
|
||||
anim.set_values(0, 255)
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_style_opa(value, 0)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Ensure opacity is reset after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_style_opa(255, 0)))
|
||||
elif anim_type == "slide_down":
|
||||
# Create slide-down animation (y from -height to original y)
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y - height, original_y)
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Reset y position after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_y(original_y)))
|
||||
else: # "slide_up"
|
||||
# Create slide-up animation (y from +height to original y)
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y + height, original_y)
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Reset y position after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_y(original_y)))
|
||||
|
||||
anim.start()
|
||||
return anim
|
||||
|
||||
@staticmethod
|
||||
def hide_widget(widget, anim_type="fade", duration=500, delay=0, hide=True):
|
||||
"""
|
||||
Hide a widget with an animation.
|
||||
|
||||
Args:
|
||||
widget (lv.obj): The widget to hide
|
||||
anim_type (str): Animation type - "fade", "slide_down", or "slide_up" (default: "fade")
|
||||
duration (int): Animation duration in milliseconds (default: 500)
|
||||
delay (int): Animation delay in milliseconds (default: 0)
|
||||
hide (bool): If True, adds HIDDEN flag after animation. If False, only animates opacity/position (default: True)
|
||||
|
||||
Returns:
|
||||
The animation object
|
||||
"""
|
||||
lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches
|
||||
anim = lv.anim_t()
|
||||
anim.init()
|
||||
anim.set_var(widget)
|
||||
anim.set_duration(duration)
|
||||
anim.set_delay(delay)
|
||||
|
||||
if anim_type == "fade":
|
||||
# Create fade-out animation (opacity from 255 to 0)
|
||||
anim.set_values(255, 0)
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_style_opa(value, 0)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: WidgetAnimator._hide_complete_cb(widget, hide=hide)))
|
||||
elif anim_type == "slide_down":
|
||||
# Create slide-down animation (y from original y to +height)
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y, original_y + height)
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: WidgetAnimator._hide_complete_cb(widget, original_y, hide)))
|
||||
else: # "slide_up"
|
||||
# Create slide-up animation (y from original y to -height)
|
||||
original_y = widget.get_y()
|
||||
height = widget.get_height()
|
||||
anim.set_values(original_y, original_y - height)
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_y(value)))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
# Set HIDDEN flag after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: WidgetAnimator._hide_complete_cb(widget, original_y, hide)))
|
||||
|
||||
anim.start()
|
||||
return anim
|
||||
|
||||
@staticmethod
|
||||
def change_widget(widget, anim_type="interpolate", duration=5000, delay=0, begin_value=0, end_value=100, display_change=None):
|
||||
"""
|
||||
Animate a widget's text by interpolating between begin_value and end_value.
|
||||
|
||||
Args:
|
||||
widget: The widget to animate (should have set_text method)
|
||||
anim_type: Type of animation (currently "interpolate" is supported)
|
||||
duration: Animation duration in milliseconds
|
||||
delay: Animation delay in milliseconds
|
||||
begin_value: Starting value for interpolation
|
||||
end_value: Ending value for interpolation
|
||||
display_change: callback to display the change in the UI
|
||||
|
||||
Returns:
|
||||
The animation object
|
||||
"""
|
||||
lv.anim_delete(widget, None) # stop all ongoing animations to prevent visual glitches
|
||||
anim = lv.anim_t()
|
||||
anim.init()
|
||||
anim.set_var(widget)
|
||||
anim.set_delay(delay)
|
||||
anim.set_duration(duration)
|
||||
|
||||
if anim_type == "interpolate":
|
||||
anim.set_values(begin_value, end_value)
|
||||
if display_change is not None:
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: display_change(value)))
|
||||
# Ensure final value is set after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: display_change(end_value)))
|
||||
else:
|
||||
anim.set_custom_exec_cb(lambda anim, value: WidgetAnimator._safe_widget_access(lambda: widget.set_text(str(value))))
|
||||
# Ensure final value is set after animation
|
||||
anim.set_completed_cb(lambda *args: WidgetAnimator._safe_widget_access(lambda: widget.set_text(str(end_value))))
|
||||
anim.set_path_cb(lv.anim_t.path_ease_in_out)
|
||||
else:
|
||||
return
|
||||
|
||||
anim.start()
|
||||
return anim
|
||||
|
||||
@staticmethod
|
||||
def smooth_show(widget, duration=500, delay=0):
|
||||
"""
|
||||
Fade in a widget (shorthand for show_widget with fade animation).
|
||||
|
||||
Args:
|
||||
widget: The widget to show
|
||||
duration: Animation duration in milliseconds (default: 500)
|
||||
delay: Animation delay in milliseconds (default: 0)
|
||||
|
||||
Returns:
|
||||
The animation object
|
||||
"""
|
||||
return WidgetAnimator.show_widget(widget, anim_type="fade", duration=duration, delay=delay)
|
||||
|
||||
@staticmethod
|
||||
def smooth_hide(widget, hide=True, duration=500, delay=0):
|
||||
"""
|
||||
Fade out a widget (shorthand for hide_widget with fade animation).
|
||||
|
||||
Args:
|
||||
widget: The widget to hide
|
||||
hide: If True, adds HIDDEN flag after animation (default: True)
|
||||
duration: Animation duration in milliseconds (default: 500)
|
||||
delay: Animation delay in milliseconds (default: 0)
|
||||
|
||||
Returns:
|
||||
The animation object
|
||||
"""
|
||||
return WidgetAnimator.hide_widget(widget, anim_type="fade", duration=duration, delay=delay, hide=hide)
|
||||
|
||||
@staticmethod
|
||||
def _hide_complete_cb(widget, original_y=None, hide=True):
|
||||
"""
|
||||
Internal callback for hide animation completion.
|
||||
|
||||
Args:
|
||||
widget: The widget being hidden
|
||||
original_y: Original Y position (for slide animations)
|
||||
hide: Whether to add HIDDEN flag
|
||||
"""
|
||||
if hide:
|
||||
widget.add_flag(lv.obj.FLAG.HIDDEN)
|
||||
if original_y:
|
||||
widget.set_y(original_y) # in case it shifted slightly due to rounding etc
|
||||
@@ -17,7 +17,7 @@ Usage:
|
||||
|
||||
import unittest
|
||||
import lvgl as lv
|
||||
import mpos.ui.anim
|
||||
from mpos.ui.widget_animator import WidgetAnimator
|
||||
import time
|
||||
from mpos import wait_for_render
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestAnimationDeletedWidget(unittest.TestCase):
|
||||
|
||||
# Start fade-in animation (500ms duration)
|
||||
print("Starting smooth_show animation...")
|
||||
mpos.ui.anim.smooth_show(widget)
|
||||
WidgetAnimator.smooth_show(widget)
|
||||
|
||||
# Give animation time to start
|
||||
wait_for_render(2)
|
||||
@@ -97,7 +97,7 @@ class TestAnimationDeletedWidget(unittest.TestCase):
|
||||
|
||||
# Start fade-out animation
|
||||
print("Starting smooth_hide animation...")
|
||||
mpos.ui.anim.smooth_hide(widget)
|
||||
WidgetAnimator.smooth_hide(widget)
|
||||
|
||||
# Give animation time to start
|
||||
wait_for_render(2)
|
||||
@@ -144,7 +144,7 @@ class TestAnimationDeletedWidget(unittest.TestCase):
|
||||
|
||||
# User clicks textarea - keyboard shows with animation
|
||||
print("Showing keyboard with animation...")
|
||||
mpos.ui.anim.smooth_show(keyboard)
|
||||
WidgetAnimator.smooth_show(keyboard)
|
||||
|
||||
# Give animation time to start
|
||||
wait_for_render(2)
|
||||
@@ -189,7 +189,7 @@ class TestAnimationDeletedWidget(unittest.TestCase):
|
||||
# Start animations on all widgets
|
||||
print("Starting animations on 5 widgets...")
|
||||
for w in widgets:
|
||||
mpos.ui.anim.smooth_show(w)
|
||||
WidgetAnimator.smooth_show(w)
|
||||
|
||||
wait_for_render(2)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""
|
||||
Test MposKeyboard animation support (show/hide with mpos.ui.anim).
|
||||
Test MposKeyboard animation support (show/hide with WidgetAnimator).
|
||||
|
||||
This test reproduces the bug where MposKeyboard is missing methods
|
||||
required by mpos.ui.anim.smooth_show() and smooth_hide().
|
||||
required by WidgetAnimator.smooth_show() and smooth_hide().
|
||||
|
||||
Usage:
|
||||
Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_animation.py
|
||||
@@ -12,7 +12,7 @@ Usage:
|
||||
import unittest
|
||||
import lvgl as lv
|
||||
import time
|
||||
import mpos.ui.anim
|
||||
from mpos.ui.widget_animator import WidgetAnimator
|
||||
from base import KeyboardTestBase
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class TestKeyboardAnimation(KeyboardTestBase):
|
||||
"""
|
||||
Test that MposKeyboard has set_style_opa method.
|
||||
|
||||
This method is required by mpos.ui.anim for fade animations.
|
||||
This method is required by WidgetAnimator for fade animations.
|
||||
"""
|
||||
print("Testing that MposKeyboard has set_style_opa...")
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestKeyboardAnimation(KeyboardTestBase):
|
||||
|
||||
# This should work without raising AttributeError
|
||||
try:
|
||||
mpos.ui.anim.smooth_show(self.keyboard)
|
||||
WidgetAnimator.smooth_show(self.keyboard)
|
||||
self.wait_for_render(100)
|
||||
print("smooth_show called successfully")
|
||||
except AttributeError as e:
|
||||
@@ -91,7 +91,7 @@ class TestKeyboardAnimation(KeyboardTestBase):
|
||||
|
||||
# This should work without raising AttributeError
|
||||
try:
|
||||
mpos.ui.anim.smooth_hide(self.keyboard)
|
||||
WidgetAnimator.smooth_hide(self.keyboard)
|
||||
print("smooth_hide called successfully")
|
||||
except AttributeError as e:
|
||||
self.fail(f"smooth_hide raised AttributeError: {e}\n"
|
||||
@@ -117,7 +117,7 @@ class TestKeyboardAnimation(KeyboardTestBase):
|
||||
|
||||
# Show keyboard (simulates textarea click)
|
||||
try:
|
||||
mpos.ui.anim.smooth_show(self.keyboard)
|
||||
WidgetAnimator.smooth_show(self.keyboard)
|
||||
self.wait_for_render(100)
|
||||
except AttributeError as e:
|
||||
self.fail(f"Failed during smooth_show: {e}")
|
||||
@@ -127,7 +127,7 @@ class TestKeyboardAnimation(KeyboardTestBase):
|
||||
|
||||
# Hide keyboard (simulates pressing Enter)
|
||||
try:
|
||||
mpos.ui.anim.smooth_hide(self.keyboard)
|
||||
WidgetAnimator.smooth_hide(self.keyboard)
|
||||
self.wait_for_render(100)
|
||||
except AttributeError as e:
|
||||
self.fail(f"Failed during smooth_hide: {e}")
|
||||
|
||||
Reference in New Issue
Block a user