Skip to content

Commit 4e5bf8f

Browse files
committed
fix notification collapse animation
1 parent 8180421 commit 4e5bf8f

File tree

1 file changed

+99
-63
lines changed

1 file changed

+99
-63
lines changed
Lines changed: 99 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import Cocoa
22

3+
private enum Timing {
4+
static let slideIn: TimeInterval = 0.3
5+
static let expansion: TimeInterval = 0.25
6+
static let fadeIn: TimeInterval = 0.15
7+
}
8+
9+
private enum Padding {
10+
static let horizontal: CGFloat = 16
11+
static let vertical: CGFloat = 14
12+
}
13+
314
extension NotificationManager {
415
func showWithAnimation(
516
notification: NotificationInstance, screen: NSScreen, timeoutSeconds: Double
617
) {
718
let screenRect = screen.visibleFrame
8-
let finalXPos = screenRect.maxX - panelWidth() - Config.rightMargin + Config.buttonOverhang
9-
let currentFrame = notification.panel.frame
19+
let finalX = screenRect.maxX - panelWidth() - Config.rightMargin + Config.buttonOverhang
20+
let y = notification.panel.frame.minY
1021

1122
notification.panel.setFrame(
1223
NSRect(
1324
x: screenRect.maxX + Config.slideInOffset,
14-
y: currentFrame.minY,
25+
y: y,
1526
width: panelWidth(),
1627
height: panelHeight()
1728
),
@@ -21,93 +32,118 @@ extension NotificationManager {
2132
notification.panel.orderFrontRegardless()
2233
notification.panel.makeKeyAndOrderFront(nil)
2334

24-
NSAnimationContext.runAnimationGroup({ context in
25-
context.duration = 0.3
26-
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
35+
animate(duration: Timing.slideIn, timing: .easeOut) {
2736
notification.panel.animator().setFrame(
28-
NSRect(
29-
x: finalXPos, y: currentFrame.minY, width: panelWidth(),
30-
height: panelHeight()),
37+
NSRect(x: finalX, y: y, width: self.panelWidth(), height: self.panelHeight()),
3138
display: true
3239
)
3340
notification.panel.animator().alphaValue = 1.0
34-
}) {
35-
DispatchQueue.main.async {
36-
notification.clickableView.updateTrackingAreas()
37-
notification.clickableView.window?.invalidateCursorRects(for: notification.clickableView)
38-
notification.clickableView.window?.resetCursorRects()
39-
self.updateHoverForAll(atScreenPoint: NSEvent.mouseLocation)
40-
}
41+
} completion: {
42+
self.refreshTrackingAreas(for: notification)
43+
self.updateHoverForAll(atScreenPoint: NSEvent.mouseLocation)
4144
notification.startDismissTimer(timeoutSeconds: timeoutSeconds)
4245
}
4346
}
4447

4548
func animateExpansion(notification: NotificationInstance, isExpanded: Bool) {
46-
let targetHeight = panelHeight(expanded: isExpanded)
4749
let currentFrame = notification.panel.frame
48-
49-
let heightDiff = targetHeight - currentFrame.height
50+
let targetHeight = panelHeight(expanded: isExpanded)
5051
let newFrame = NSRect(
5152
x: currentFrame.minX,
52-
y: currentFrame.minY - heightDiff,
53+
y: currentFrame.minY - (targetHeight - currentFrame.height),
5354
width: currentFrame.width,
5455
height: targetHeight
5556
)
5657

57-
guard
58-
let effectView = notification.clickableView.subviews.first?.subviews.first
59-
as? NSVisualEffectView
60-
else {
58+
guard let effectView = findEffectView(in: notification) else {
6159
notification.isAnimating = false
6260
return
6361
}
6462

6563
if isExpanded {
66-
notification.compactContentView?.alphaValue = 1.0
64+
animateToExpanded(notification: notification, effectView: effectView, frame: newFrame)
65+
} else {
66+
animateToCompact(notification: notification, frame: newFrame)
67+
}
68+
}
6769

68-
let expandedView = createExpandedNotificationView(notification: notification)
69-
expandedView.translatesAutoresizingMaskIntoConstraints = false
70-
expandedView.alphaValue = 0
71-
effectView.addSubview(expandedView)
70+
private func animateToExpanded(
71+
notification: NotificationInstance, effectView: NSVisualEffectView, frame: NSRect
72+
) {
73+
notification.compactContentView?.isHidden = true
7274

73-
NSLayoutConstraint.activate([
74-
expandedView.leadingAnchor.constraint(equalTo: effectView.leadingAnchor, constant: 16),
75-
expandedView.trailingAnchor.constraint(equalTo: effectView.trailingAnchor, constant: -16),
76-
expandedView.topAnchor.constraint(equalTo: effectView.topAnchor, constant: 14),
77-
expandedView.bottomAnchor.constraint(equalTo: effectView.bottomAnchor, constant: -14),
78-
])
75+
let expandedView = createExpandedNotificationView(notification: notification)
76+
expandedView.translatesAutoresizingMaskIntoConstraints = false
77+
expandedView.alphaValue = 0
78+
notification.expandedContentView = expandedView
7979

80-
notification.expandedContentView = expandedView
80+
animate(duration: Timing.expansion, timing: .easeInEaseOut) {
81+
notification.panel.animator().setFrame(frame, display: true)
82+
} completion: {
83+
effectView.addSubview(expandedView)
84+
self.pinToEdges(expandedView, in: effectView)
8185

82-
NSAnimationContext.runAnimationGroup({ context in
83-
context.duration = 0.25
84-
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
85-
notification.panel.animator().setFrame(newFrame, display: true)
86-
notification.compactContentView?.animator().alphaValue = 0
86+
self.animate(duration: Timing.fadeIn, timing: .easeOut) {
8787
expandedView.animator().alphaValue = 1.0
88-
}) {
89-
notification.compactContentView?.isHidden = true
90-
notification.isAnimating = false
91-
notification.clickableView.updateTrackingAreas()
92-
self.repositionNotifications()
93-
}
94-
} else {
95-
notification.stopCountdown()
96-
notification.expandedContentView?.removeFromSuperview()
97-
notification.expandedContentView = nil
98-
notification.compactContentView?.alphaValue = 0
99-
notification.compactContentView?.isHidden = false
100-
101-
NSAnimationContext.runAnimationGroup({ context in
102-
context.duration = 0.25
103-
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
104-
notification.panel.animator().setFrame(newFrame, display: true)
105-
notification.compactContentView?.animator().alphaValue = 1.0
106-
}) {
107-
notification.isAnimating = false
108-
notification.clickableView.updateTrackingAreas()
109-
self.repositionNotifications()
88+
} completion: {
89+
self.finishExpansionAnimation(notification)
11090
}
11191
}
11292
}
93+
94+
private func animateToCompact(notification: NotificationInstance, frame: NSRect) {
95+
notification.stopCountdown()
96+
notification.expandedContentView?.removeFromSuperview()
97+
notification.expandedContentView = nil
98+
notification.compactContentView?.alphaValue = 0
99+
notification.compactContentView?.isHidden = false
100+
101+
animate(duration: Timing.expansion, timing: .easeInEaseOut) {
102+
notification.panel.animator().setFrame(frame, display: true)
103+
notification.compactContentView?.animator().alphaValue = 1.0
104+
} completion: {
105+
self.finishExpansionAnimation(notification)
106+
}
107+
}
108+
109+
private func finishExpansionAnimation(_ notification: NotificationInstance) {
110+
notification.isAnimating = false
111+
notification.clickableView.updateTrackingAreas()
112+
repositionNotifications()
113+
}
114+
115+
private func findEffectView(in notification: NotificationInstance) -> NSVisualEffectView? {
116+
notification.clickableView.subviews.first?.subviews.first as? NSVisualEffectView
117+
}
118+
119+
private func pinToEdges(_ view: NSView, in container: NSView) {
120+
NSLayoutConstraint.activate([
121+
view.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.horizontal),
122+
view.trailingAnchor.constraint(
123+
equalTo: container.trailingAnchor, constant: -Padding.horizontal),
124+
view.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.vertical),
125+
view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -Padding.vertical),
126+
])
127+
}
128+
129+
private func refreshTrackingAreas(for notification: NotificationInstance) {
130+
DispatchQueue.main.async {
131+
notification.clickableView.updateTrackingAreas()
132+
notification.clickableView.window?.invalidateCursorRects(for: notification.clickableView)
133+
notification.clickableView.window?.resetCursorRects()
134+
}
135+
}
136+
137+
private func animate(
138+
duration: TimeInterval,
139+
timing: CAMediaTimingFunctionName,
140+
animations: @escaping () -> Void,
141+
completion: @escaping () -> Void
142+
) {
143+
NSAnimationContext.runAnimationGroup({ context in
144+
context.duration = duration
145+
context.timingFunction = CAMediaTimingFunction(name: timing)
146+
animations()
147+
}, completionHandler: completion)
148+
}
113149
}

0 commit comments

Comments
 (0)