Files
2026-01-26 12:30:50 +01:00

226 lines
9.8 KiB
Python

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, lv.PART.MAIN)))
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, lv.PART.MAIN)))
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, lv.PART.MAIN)))
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