feat(widgets): replace custom animationmanager with new Animator-based implementation
Replace legacy animation manager with a unified Animator utility, improving performance and simplifying animation control. - New Features - Integrated Animator with custom easing functions (ease_out_quint, ease_in_cubic) for consistent 120 FPS animations. - SlideRevealer now uses Animator.play/stop API, exposing simplified control. - Enhancements - Simplified animation lifecycle: start/stop handled via Animator instead of manual timer. - Pixel‑accurate positioning using int(round(...)) for smoother motion. - Reduced memory usage by removing unused timer and container tracking. - Bug Fixes - Fixed stale timer references and redraw queue clearing race conditions. - Corrected handling of zero‑size dimensions to properly finalize animations. - Removed redundant AnimationManager import and idle_add boilerplate.
This commit is contained in:
1 file changed
+48
-176
+48
-176
@@ -1,117 +1,15 @@
|
||||
from fabric.utils import GLib, Gtk
|
||||
from fabric.utils import Gtk
|
||||
import math
|
||||
|
||||
# TODO: UsE BETTER APPROACH IF POSSIBLE
|
||||
from utils.animator import Animator
|
||||
|
||||
|
||||
class AnimationManager:
|
||||
_instance = None
|
||||
_animating_widgets = set()
|
||||
_timer_id = None
|
||||
_containers_to_redraw = set()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def add_widget(self, widget):
|
||||
self._animating_widgets.add(widget)
|
||||
if self._timer_id is None:
|
||||
# Use 120 FPS for ultra-smooth animations like macOS
|
||||
self._timer_id = GLib.timeout_add(8, self._animate_all) # 120 FPS
|
||||
|
||||
def remove_widget(self, widget):
|
||||
self._animating_widgets.discard(widget)
|
||||
if not self._animating_widgets and self._timer_id:
|
||||
# Stop timer when no widgets are animating
|
||||
GLib.source_remove(self._timer_id)
|
||||
self._timer_id = None
|
||||
# Clear any pending redraws
|
||||
self._containers_to_redraw.clear()
|
||||
|
||||
def _animate_all(self):
|
||||
# Clear previous frame's redraw queue
|
||||
self._containers_to_redraw.clear()
|
||||
|
||||
widgets_to_remove = []
|
||||
|
||||
# Process all animations in a single frame
|
||||
for widget in list(self._animating_widgets):
|
||||
if not widget._calculate_position():
|
||||
widgets_to_remove.append(widget)
|
||||
else:
|
||||
container = widget._get_container_for_redraw()
|
||||
if container:
|
||||
self._containers_to_redraw.add(container)
|
||||
|
||||
# Apply all position changes at once to prevent conflicts
|
||||
for widget in self._animating_widgets:
|
||||
widget._apply_position()
|
||||
|
||||
# Batch redraw calls to minimize performance impact
|
||||
for container in self._containers_to_redraw:
|
||||
container.queue_draw()
|
||||
|
||||
# Remove completed animations
|
||||
for widget in widgets_to_remove:
|
||||
self.remove_widget(widget)
|
||||
|
||||
return len(self._animating_widgets) > 0 # Continue if widgets remain
|
||||
|
||||
def get_active_widget_count(self):
|
||||
"""Return the number of currently animating widgets"""
|
||||
return len(self._animating_widgets)
|
||||
|
||||
def _get_optimal_interval(self):
|
||||
"""Keep consistent 120 FPS for macOS-like smoothness"""
|
||||
return 8 # 120 FPS
|
||||
|
||||
def _start_timer(self):
|
||||
interval = self._get_optimal_interval()
|
||||
self._timer_id = GLib.timeout_add(interval, self._animate_all)
|
||||
|
||||
def _adjust_frame_rate(self):
|
||||
# No need to adjust frame rate anymore - keep it consistent
|
||||
pass
|
||||
# macOS-style easing functions for natural motion
|
||||
def ease_out_quint(progress):
|
||||
return 1 - math.pow(1 - progress, 5)
|
||||
|
||||
|
||||
class MacOSEasing:
|
||||
"""macOS-style easing functions for natural motion"""
|
||||
|
||||
@staticmethod
|
||||
def ease_out_expo(t):
|
||||
"""Exponential ease out - fast start, slow end"""
|
||||
return 1 - math.pow(2, -10 * t) if t != 1 else 1
|
||||
|
||||
@staticmethod
|
||||
def ease_in_out_quart(t):
|
||||
"""Quartic ease in-out for smooth acceleration/deceleration"""
|
||||
return 8 * t * t * t * t if t < 0.5 else 1 - math.pow(-2 * t + 2, 4) / 2
|
||||
|
||||
@staticmethod
|
||||
def ease_out_back(t):
|
||||
"""Back ease out for slight overshoot effect"""
|
||||
c1 = 1.70158
|
||||
c3 = c1 + 1
|
||||
return 1 + c3 * math.pow(t - 1, 3) + c1 * math.pow(t - 1, 2)
|
||||
|
||||
@staticmethod
|
||||
def ease_out_cubic_bezier(t):
|
||||
"""Custom cubic bezier similar to macOS default (0.25, 0.1, 0.25, 1.0)"""
|
||||
# Approximation of cubic-bezier(0.25, 0.1, 0.25, 1.0)
|
||||
return t * t * t * (t * (6 * t - 15) + 10)
|
||||
|
||||
@staticmethod
|
||||
def ease_in_cubic(t):
|
||||
"""Cubic ease in for smooth acceleration"""
|
||||
return t * t * t
|
||||
|
||||
@staticmethod
|
||||
def ease_out_quint(t):
|
||||
"""Quintic ease out for very smooth deceleration"""
|
||||
return 1 - math.pow(1 - t, 5)
|
||||
def ease_in_cubic(progress):
|
||||
return progress * progress * progress
|
||||
|
||||
|
||||
class SlideRevealer(Gtk.Overlay):
|
||||
@@ -120,15 +18,19 @@ class SlideRevealer(Gtk.Overlay):
|
||||
|
||||
self.child = child
|
||||
self.direction = direction
|
||||
self.duration = duration # Slightly faster for snappier feel
|
||||
self.duration = duration
|
||||
self.fixed_size = size
|
||||
self._revealed = False
|
||||
self._animating = False
|
||||
self._start_time = None
|
||||
self._show_animation = False
|
||||
self._pending_position = None
|
||||
self._current_position = (0.0, 0.0) # Use float for sub-pixel positioning
|
||||
self._animation_id = None # Track individual animation instances
|
||||
self._cached_dimensions = None
|
||||
|
||||
self.animator = Animator(
|
||||
duration=duration / 1000.0,
|
||||
timing_function=ease_out_quint,
|
||||
tick_interval=8, # 120 FPS for smoothness
|
||||
tick_widget=self,
|
||||
)
|
||||
self.animator.connect("notify::value", self._on_animator_value_changed)
|
||||
self.animator.connect("finished", self._on_animator_finished)
|
||||
|
||||
self._fixed = Gtk.Fixed()
|
||||
self._fixed.set_has_window(False)
|
||||
@@ -160,15 +62,14 @@ class SlideRevealer(Gtk.Overlay):
|
||||
self.hide()
|
||||
|
||||
def reveal(self):
|
||||
if self._revealed and not self._animating:
|
||||
if self._revealed and not self.animator.playing:
|
||||
return
|
||||
self._revealed = True
|
||||
|
||||
# Ensure widget is properly laid out before starting animation
|
||||
if self.get_realized():
|
||||
self._start_animation(show=True)
|
||||
else:
|
||||
# Wait for widget to be realized
|
||||
|
||||
def on_realize(*_):
|
||||
self._start_animation(show=True)
|
||||
self.disconnect_by_func(on_realize)
|
||||
@@ -176,75 +77,50 @@ class SlideRevealer(Gtk.Overlay):
|
||||
self.connect("realize", on_realize)
|
||||
|
||||
def hide(self):
|
||||
if not self._revealed and not self._animating:
|
||||
if not self._revealed and not self.animator.playing:
|
||||
return
|
||||
self._revealed = False
|
||||
self._start_animation(show=False)
|
||||
|
||||
def _start_animation(self, show: bool):
|
||||
# Stop any existing animation for this widget
|
||||
if self._animating:
|
||||
AnimationManager.get_instance().remove_widget(self)
|
||||
self.animator.stop()
|
||||
|
||||
self._cached_dimensions = self._get_dimensions()
|
||||
if self._cached_dimensions[0] == 0 or self._cached_dimensions[1] == 0:
|
||||
self._animating = False
|
||||
return
|
||||
|
||||
# Use high-precision monotonic time for smooth animations
|
||||
self._start_time = GLib.get_monotonic_time()
|
||||
self._animating = True
|
||||
self._show_animation = show
|
||||
self._animation_id = id(self) # Unique ID for this animation instance
|
||||
|
||||
# Configure animator based on direction
|
||||
if show:
|
||||
self.child.show()
|
||||
pos = self._get_offscreen_pos_cached()
|
||||
self._current_position = (float(pos[0]), float(pos[1]))
|
||||
self._fixed.move(self.child, int(pos[0]), int(pos[1]))
|
||||
|
||||
def start_with_dimensions():
|
||||
AnimationManager.get_instance().add_widget(self)
|
||||
return False
|
||||
|
||||
# Use idle_add to ensure layout is complete
|
||||
GLib.idle_add(start_with_dimensions)
|
||||
|
||||
def _calculate_position(self):
|
||||
if not self._animating:
|
||||
return False
|
||||
|
||||
elapsed = (GLib.get_monotonic_time() - self._start_time) / 1000
|
||||
t = min(elapsed / self.duration, 1.0)
|
||||
|
||||
# Use different easing functions for better smoothness
|
||||
if self._show_animation:
|
||||
# Use quintic ease out for very smooth revealing
|
||||
eased = MacOSEasing.ease_out_quint(t)
|
||||
self.animator.timing_function = ease_out_quint
|
||||
# We animate the 'progress' from 0 to 1
|
||||
self.animator.min_value = 0.0
|
||||
self.animator.max_value = 1.0
|
||||
else:
|
||||
# Use cubic ease in for smooth hiding
|
||||
eased = MacOSEasing.ease_in_cubic(t)
|
||||
self.animator.timing_function = ease_in_cubic
|
||||
self.animator.min_value = 0.0
|
||||
self.animator.max_value = 1.0
|
||||
|
||||
self._pending_position = self._get_position_at_progress_cached(eased)
|
||||
self.animator.play()
|
||||
|
||||
if t >= 1.0:
|
||||
self._animating = False
|
||||
self._cached_dimensions = None
|
||||
self._animation_id = None
|
||||
if not self._show_animation:
|
||||
GLib.idle_add(lambda: self.child.hide())
|
||||
return False
|
||||
return True
|
||||
def _on_animator_value_changed(self, animator, _pspec):
|
||||
if not self._cached_dimensions:
|
||||
return
|
||||
|
||||
def _apply_position(self):
|
||||
if self._pending_position:
|
||||
x, y = self._pending_position
|
||||
# Use sub-pixel positioning for smoother motion
|
||||
self._current_position = (x, y)
|
||||
# Round to nearest pixel for actual positioning
|
||||
pixel_x, pixel_y = int(round(x)), int(round(y))
|
||||
self._fixed.move(self.child, pixel_x, pixel_y)
|
||||
self._pending_position = None
|
||||
progress = animator.value
|
||||
x, y = self._get_position_at_progress_cached(progress)
|
||||
|
||||
# Round to nearest pixel for actual positioning
|
||||
pixel_x, pixel_y = int(round(x)), int(round(y))
|
||||
self._fixed.move(self.child, pixel_x, pixel_y)
|
||||
self.queue_draw()
|
||||
|
||||
def _on_animator_finished(self, _animator):
|
||||
self._cached_dimensions = None
|
||||
if not self._revealed:
|
||||
self.child.hide()
|
||||
|
||||
def _get_container_for_redraw(self):
|
||||
return self
|
||||
@@ -299,17 +175,13 @@ class SlideRevealer(Gtk.Overlay):
|
||||
return self._revealed
|
||||
|
||||
def is_animating(self):
|
||||
return self._animating
|
||||
return self.animator.playing
|
||||
|
||||
def get_child_revealed(self):
|
||||
return self._revealed
|
||||
|
||||
def stop_animation(self):
|
||||
if self._animating:
|
||||
AnimationManager.get_instance().remove_widget(self)
|
||||
self._animating = False
|
||||
self._cached_dimensions = None
|
||||
self._animation_id = None
|
||||
self.animator.stop()
|
||||
|
||||
def destroy(self):
|
||||
self.stop_animation()
|
||||
|
||||
Reference in new issue
Block a user