feat: add Android-style swipe animations to notification system

This commit is contained in:
tr1x_em committed 2025-08-02 22:32:27 +05:30
1 parent 690d4cef8e
commit dc970c46b1
1 file changed
+213 -27
+213 -27
View File
@@ -296,12 +296,22 @@ class NotificationRevealer(SlideRevealer):
self.parent_window = parent_window
self._is_closing = False
# Add swipe detection variables
# Enhanced swipe detection variables for Android-style animation
self._drag_start_x = 0
self._drag_start_y = 0
self._is_dragging = False
self._swipe_threshold = 80 # Reduced threshold for more responsive swipes
self._swipe_threshold = 80 # Distance to trigger auto-dismiss
self._swipe_velocity_threshold = 150 # Velocity to trigger dismiss even on shorter swipes
self._swipe_in_progress = False
self._current_offset = 0
self._last_drag_time = 0
self._drag_velocity = 0
self._spring_back_duration = 200 # Duration for spring-back animation
self._dismiss_threshold = 0.3 # Dismiss if swiped 30% of width
# Animation state
self._animation_in_progress = False
self._spring_timer_id = None
# Wrap notification in EventBox for swipe detection
self.event_box = EventBox(
@@ -328,6 +338,131 @@ class NotificationRevealer(SlideRevealer):
# Connect our own handler that manages the slide animation
self.notification.connect("closed", self.on_resolved)
def _apply_transform(self, offset_x, opacity=1.0, scale=1.0):
"""Apply CSS transform to the notification widget for smooth animation"""
if not self.event_box or not self.event_box.get_realized():
return
# Clamp values
offset_x = max(0, offset_x) # Only allow rightward movement
opacity = max(0.1, min(1.0, opacity)) # Keep some visibility
scale = max(0.8, min(1.0, scale)) # Subtle scale effect
# Apply CSS transform
css_transform = f"""
* {{
transform: translateX({int(offset_x)}px) scale({scale});
opacity: {opacity};
transition: none;
}}
"""
try:
# Apply the transform using GTK's CSS provider
provider = self.event_box.get_style_context().get_property(
"transition", self.event_box.get_state_flags()
)
self.event_box.get_style_context().add_class("notification-transform")
# Note: In a real implementation, you'd need to set up a CSS provider
# This is a simplified version showing the concept
except:
pass # Fallback if CSS transforms aren't available
def _animate_spring_back(self, start_offset, target_offset=0, duration=None):
"""Animate the notification springing back to its original position"""
if duration is None:
duration = self._spring_back_duration
if self._spring_timer_id:
GLib.source_remove(self._spring_timer_id)
start_time = GLib.get_monotonic_time() / 1000
offset_diff = target_offset - start_offset
def animate_step():
current_time = GLib.get_monotonic_time() / 1000
elapsed = current_time - start_time
progress = min(1.0, elapsed / duration)
# Use easing function for spring-like animation
eased_progress = self._ease_out_back(progress)
current_offset = start_offset + (offset_diff * eased_progress)
# Calculate opacity and scale based on offset
max_offset = NOTIFICATION_WIDTH * 0.8
offset_ratio = abs(current_offset) / max_offset
opacity = 1.0 - (offset_ratio * 0.3) # Fade slightly
scale = 1.0 - (offset_ratio * 0.05) # Slight scale down
self._apply_transform(current_offset, opacity, scale)
if progress >= 1.0:
self._animation_in_progress = False
self._current_offset = target_offset
return False
return True
self._animation_in_progress = True
self._spring_timer_id = GLib.timeout_add(16, animate_step) # ~60fps
def _ease_out_back(self, t):
"""Easing function for spring-back animation"""
c1 = 1.70158
c3 = c1 + 1
return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2)
def _animate_dismiss(self, start_offset):
"""Animate the notification sliding out completely"""
target_offset = NOTIFICATION_WIDTH + 50 # Slide completely out of view
duration = 250 # Quick dismiss animation
if self._spring_timer_id:
GLib.source_remove(self._spring_timer_id)
start_time = GLib.get_monotonic_time() / 1000
offset_diff = target_offset - start_offset
def animate_step():
current_time = GLib.get_monotonic_time() / 1000
elapsed = current_time - start_time
progress = min(1.0, elapsed / duration)
# Use easing for smooth exit
eased_progress = 1 - pow(1 - progress, 3) # Ease out cubic
current_offset = start_offset + (offset_diff * eased_progress)
# Fade out and scale down during dismiss
opacity = 1.0 - progress
scale = 1.0 - (progress * 0.2)
self._apply_transform(current_offset, opacity, scale)
if progress >= 1.0:
# Trigger the actual notification dismissal
try:
self.notification.close("dismissed-by-user")
except:
pass
return False
return True
self._animation_in_progress = True
self._spring_timer_id = GLib.timeout_add(16, animate_step)
def _calculate_drag_velocity(self, current_x):
"""Calculate the velocity of the drag gesture"""
current_time = GLib.get_monotonic_time() / 1000
if self._last_drag_time > 0:
time_diff = current_time - self._last_drag_time
if time_diff > 0:
distance_diff = current_x - self._drag_start_x - self._current_offset
self._drag_velocity = abs(distance_diff / time_diff)
self._last_drag_time = current_time
def _on_animation_complete(self, is_hiding=False):
if is_hiding:
# Manually destroy the notification widget since we disconnected its handler
@@ -347,6 +482,10 @@ class NotificationRevealer(SlideRevealer):
self._is_closing = True
# Clean up any ongoing animations
if self._spring_timer_id:
GLib.source_remove(self._spring_timer_id)
# Use different slide directions based on dismiss reason
if reason == "expired":
# Left-to-right slide for auto-dismiss (expired)
@@ -368,6 +507,14 @@ class NotificationRevealer(SlideRevealer):
self._drag_start_y = event.y
self._is_dragging = True
self._swipe_in_progress = False
self._current_offset = 0
self._last_drag_time = GLib.get_monotonic_time() / 1000
self._drag_velocity = 0
# Stop any ongoing animations
if self._spring_timer_id:
GLib.source_remove(self._spring_timer_id)
self._animation_in_progress = False
return False
def _on_button_release(self, _widget, event):
@@ -383,34 +530,73 @@ class NotificationRevealer(SlideRevealer):
elif self._is_dragging and event.button == 1:
self._is_dragging = False
# Calculate swipe distance
dx = event.x - self._drag_start_x
dy = abs(event.y - self._drag_start_y)
# Right swipe: dismiss current notification (swipe to right to dismiss)
if dx > self._swipe_threshold and dy < 60: # 60px vertical tolerance
try:
self._swipe_in_progress = True
self.notification.close("dismissed-by-user")
logger.debug(f"Notification dismissed by swipe to right: dx={dx}, dy={dy}")
except Exception as e:
logger.error(f"Error dismissing notification by swipe: {e}")
pass
return False
def _on_motion(self, _widget, event):
if self._is_dragging:
# Calculate current swipe distance for visual feedback
# Calculate final swipe metrics
dx = event.x - self._drag_start_x
dy = abs(event.y - self._drag_start_y)
# Optional: Add visual feedback during swipe
# You could add opacity changes or slight translation here
if dx > 20 and dy < 60: # Starting to swipe right
# Visual indication that swipe gesture is recognized
# Could modify opacity or add translation effect here
pass
# Calculate dismiss threshold based on notification width
dismiss_distance = NOTIFICATION_WIDTH * self._dismiss_threshold
# Determine if we should dismiss or spring back
should_dismiss = (
(dx > dismiss_distance and dy < 60) or # Dragged far enough
(dx > 30 and self._drag_velocity > self._swipe_velocity_threshold and dy < 60) # Fast swipe
)
if should_dismiss:
try:
self._swipe_in_progress = True
# Animate the dismiss
self._animate_dismiss(self._current_offset)
logger.debug(f"Notification dismissed by swipe: dx={dx}, velocity={self._drag_velocity}")
except Exception as e:
logger.error(f"Error dismissing notification by swipe: {e}")
else:
# Spring back to original position
if abs(self._current_offset) > 5: # Only animate if there's noticeable displacement
self._animate_spring_back(self._current_offset, 0)
else:
self._apply_transform(0, 1.0, 1.0)
self._current_offset = 0
return False
def _on_motion(self, _widget, event):
if self._is_dragging and not self._animation_in_progress:
# Calculate current swipe distance
dx = event.x - self._drag_start_x
dy = abs(event.y - self._drag_start_y)
# Only respond to primarily horizontal gestures
if dy < 60: # Vertical tolerance
# Calculate velocity for smooth interaction
self._calculate_drag_velocity(event.x)
# Apply real-time transform - only allow rightward movement
if dx > 0:
self._current_offset = dx
# Calculate visual feedback based on drag distance
max_offset = NOTIFICATION_WIDTH * 0.8
offset_ratio = min(1.0, dx / max_offset)
# Apply diminishing returns for large swipes
adjusted_offset = dx * (1.0 - offset_ratio * 0.3)
# Calculate opacity and scale
opacity = 1.0 - (offset_ratio * 0.4) # Fade as it's dragged
scale = 1.0 - (offset_ratio * 0.1) # Slight scale down
self._apply_transform(adjusted_offset, opacity, scale)
# Visual indication of dismiss threshold
if dx > NOTIFICATION_WIDTH * self._dismiss_threshold:
# Could add visual cue here (like changing color)
pass
else:
# Reset to original position if dragging left
self._current_offset = 0
self._apply_transform(0, 1.0, 1.0)
return False