Things work - but animation is still janky

This commit is contained in:
Hariharan Mudaliar 2025-11-11 15:04:59 +05:30
commit ba27aa3d0f
4 changed files with 114 additions and 74 deletions

View file

@ -87,6 +87,12 @@ struct ContentView: View {
@Default(.showNotHumanFace) var showNotHumanFace
@Default(.useModernCloseAnimation) var useModernCloseAnimation
@Default(.enableMinimalisticUI) var enableMinimalisticUI
private var dynamicNotchResizeAnimation: Animation? {
if enableMinimalisticUI && reminderManager.activeWindowReminders.isEmpty == false {
return nil
}
return .easeInOut(duration: 0.4)
}
private let extendedHoverPadding: CGFloat = 30
private let zeroHeightHoverPadding: CGFloat = 10
@ -344,8 +350,8 @@ struct ContentView: View {
// .keyboardShortcut("E", modifiers: .command)
}
}
.frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top)
.animation(.easeInOut(duration: 0.4), value: dynamicNotchSize)
.frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top)
.animation(dynamicNotchResizeAnimation, value: dynamicNotchSize)
.animation(.easeInOut(duration: 0.4), value: coordinator.currentView)
.environmentObject(privacyManager)
.onChange(of: dynamicNotchSize) { oldSize, newSize in
@ -357,9 +363,11 @@ struct ContentView: View {
isViewTransitioning = true
let workItem = DispatchWorkItem {
isViewTransitioning = false
vm.shouldRecheckHover.toggle()
}
sizeChangeWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: workItem) // Frame animation + buffer
let delay: TimeInterval = dynamicNotchResizeAnimation == nil ? 0.25 : 1.0
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
.shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5)
@ -736,11 +744,15 @@ struct ContentView: View {
}
}
} else {
if vm.notchState == .open && isViewTransitioning {
return
}
hoverTask = Task {
try? await Task.sleep(for: .milliseconds(100))
guard !Task.isCancelled else { return }
await MainActor.run {
guard !self.isViewTransitioning else { return }
withAnimation(.bouncy.speed(1.2)) {
self.isHovering = false
}
@ -800,7 +812,7 @@ struct ContentView: View {
let stillNotHovering = !isHovering
let stillOpen = vm.notchState == .open
let stillNoPopovers = !hasAnyActivePopovers()
let notTransitioning = !isStatsTransitioning && !isSwitchingToStats
let notTransitioning = !isStatsTransitioning && !isSwitchingToStats && !isViewTransitioning
if stillNotHovering && stillOpen && stillNoPopovers && notTransitioning {
vm.close()

View file

@ -79,6 +79,12 @@ struct DynamicNotchApp: App {
}
}
extension AppDelegate {
static var shared: AppDelegate? {
NSApplication.shared.delegate as? AppDelegate
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem?
var windows: [NSScreen: NSWindow] = [:]
@ -249,69 +255,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
private func updateWindowSizeIfNeeded() {
// Calculate required size based on current state
let requiredSize = calculateRequiredNotchSize()
if Defaults[.showOnAllDisplays] {
// Update all windows if size has changed (multi-display mode)
for (screen, window) in windows {
if window.frame.size != requiredSize {
// Calculate center position BEFORE any changes
let screenFrame = screen.frame
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
let newX = centerX - (requiredSize.width / 2)
let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height
// Stop any existing animations first
NSAnimationContext.runAnimationGroup { _ in
window.animator().setFrame(window.frame, display: false)
}
// Animate window resize smoothly
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.25
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
context.allowsImplicitAnimation = true
window.animator().setFrame(NSRect(
x: newX,
y: newY,
width: requiredSize.width,
height: requiredSize.height
), display: true)
}
}
}
} else {
// Update single window if size has changed (single display mode)
if let window = window, window.frame.size != requiredSize {
// Find the screen this window is on
let currentScreen = NSScreen.screens.first { screen in
screen.frame.intersects(window.frame)
} ?? NSScreen.main ?? NSScreen.screens.first!
// Calculate center position BEFORE any changes
let screenFrame = currentScreen.frame
let centerX = screenFrame.origin.x + (screenFrame.width / 2)
let newX = centerX - (requiredSize.width / 2)
let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height
// Stop any existing animations first
NSAnimationContext.runAnimationGroup { _ in
window.animator().setFrame(window.frame, display: false)
}
// Animate window resize smoothly
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.25
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
context.allowsImplicitAnimation = true
window.animator().setFrame(NSRect(
x: newX,
y: newY,
width: requiredSize.width,
height: requiredSize.height
), display: true)
}
}
}
let animateResize = shouldAnimateResize(for: requiredSize)
resizeWindows(to: requiredSize, animated: animateResize, force: false)
}
private func calculateRequiredNotchSize() -> CGSize {
@ -365,6 +310,54 @@ class AppDelegate: NSObject, NSApplicationDelegate {
return CGSize(width: baseSize.width, height: requiredHeight)
}
func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) {
resizeWindows(to: size, animated: animated, force: force)
}
private func resizeWindows(to size: CGSize, animated: Bool, force: Bool) {
guard size.width > 0, size.height > 0 else { return }
if Defaults[.showOnAllDisplays] {
for (screen, window) in windows {
if force || window.frame.size != size {
resizeWindow(window, on: screen, to: size, animated: animated)
}
}
} else if let window {
let screen = window.screen ?? NSScreen.screens.first { $0.frame.intersects(window.frame) } ?? NSScreen.main ?? NSScreen.screens.first
guard let screen else { return }
if force || window.frame.size != size {
resizeWindow(window, on: screen, to: size, animated: animated)
}
}
}
private func resizeWindow(_ window: NSWindow, on screen: NSScreen, to size: CGSize, animated: Bool) {
let screenFrame = screen.frame
let centerX = screenFrame.midX
let newX = centerX - (size.width / 2)
let newY = screenFrame.origin.y + screenFrame.height - size.height
let targetFrame = NSRect(x: newX, y: newY, width: size.width, height: size.height)
if animated {
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.25
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
context.allowsImplicitAnimation = true
window.animator().setFrame(targetFrame, display: true)
}
} else {
window.setFrame(targetFrame, display: true)
}
}
private func shouldAnimateResize(for newSize: CGSize) -> Bool {
if Defaults[.enableMinimalisticUI] && !ReminderLiveActivityManager.shared.activeWindowReminders.isEmpty {
return false
}
return true
}
func applicationDidFinishLaunching(_ notification: Notification) {
coordinator.setupWorkersNotificationObservers()

View file

@ -65,10 +65,7 @@ struct MinimalisticMusicPlayerView: View {
playbackControls
.padding(.top, 4)
if shouldShowReminderList {
MinimalisticReminderEventListView(reminders: reminderEntries, currentDate: reminderManager.currentDate)
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
reminderList
}
.padding(.horizontal, 12)
.padding(.top, -15)
@ -84,6 +81,18 @@ struct MinimalisticMusicPlayerView: View {
enableReminderLiveActivity && !reminderEntries.isEmpty
}
private var reminderListHeight: CGFloat {
ReminderLiveActivityManager.additionalHeight(forRowCount: reminderEntries.count)
}
private var reminderList: some View {
MinimalisticReminderEventListView(reminders: reminderEntries, currentDate: reminderManager.currentDate)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: reminderListHeight, alignment: .top)
.opacity(shouldShowReminderList ? 1 : 0)
.animation(.easeInOut(duration: 0.18), value: shouldShowReminderList)
}
private struct MinimalisticReminderEventListView: View {
let reminders: [ReminderLiveActivityManager.ReminderEntry]

View file

@ -44,6 +44,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
@Published var notchSize: CGSize = getClosedNotchSize()
@Published var closedNotchSize: CGSize = getClosedNotchSize()
private var minimalisticReminderRowCount: Int = 0
deinit {
destroy()
@ -71,6 +72,19 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
.store(in: &cancellables)
setupDetectorObserver()
ReminderLiveActivityManager.shared.$activeWindowReminders
.removeDuplicates()
.receive(on: RunLoop.main)
.sink { [weak self] entries in
guard let self else { return }
self.minimalisticReminderRowCount = entries.count
let updatedTarget = self.calculateDynamicNotchSize()
guard self.notchState == .open else { return }
guard self.notchSize != updatedTarget else { return }
self.notchSize = updatedTarget
}
.store(in: &cancellables)
}
private func setupDetectorObserver() {
@ -124,8 +138,21 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
}
func open() {
let targetSize = calculateDynamicNotchSize()
let applyWindowResize: () -> Void = {
guard let delegate = AppDelegate.shared else { return }
delegate.ensureWindowSize(targetSize, animated: false, force: true)
}
if Thread.isMainThread {
applyWindowResize()
} else {
DispatchQueue.main.async(execute: applyWindowResize)
}
withAnimation(.bouncy) {
self.notchSize = calculateDynamicNotchSize()
self.notchSize = targetSize
self.notchState = .open
}
@ -138,8 +165,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
var baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize
if Defaults[.enableMinimalisticUI] {
let reminderCount = ReminderLiveActivityManager.shared.activeWindowReminders.count
let extraHeight = ReminderLiveActivityManager.additionalHeight(forRowCount: reminderCount)
let extraHeight = ReminderLiveActivityManager.additionalHeight(forRowCount: minimalisticReminderRowCount)
baseSize.height += extraHeight
}