From 8762c1392093339f263d2d124970532ac0be8403 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 29 Apr 2024 18:52:31 +0600 Subject: [PATCH] Fix quick download button animation (#2711) Task/Issue URL: https://app.asana.com/0/1177771139624306/1207169957308547/f --- .../View/AppKit/CircularProgressView.swift | 116 ++++++++++++++++-- .../View/NavigationBarViewController.swift | 20 +-- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift index 204d950546..3e1190a9e9 100644 --- a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift @@ -164,6 +164,14 @@ final class CircularProgressView: NSView { guard !isBackgroundAnimating || !animated else { // will call `updateProgressState` on animation completion completion(false) + // if background animation is in progress but 1.0 was received before + // the `progress = nil` update – complete the progress animation + // before hiding + if progress == nil && oldValue == 1.0, animated, + // shouldn‘t be already animating to 100% + progressLayer.strokeStart != 0.0 { + updateProgress(from: 0, to: 1, animated: animated) { _ in } + } return } @@ -177,7 +185,7 @@ final class CircularProgressView: NSView { completion(true) } case (true, true): - updateProgress(oldValue: oldValue, animated: animated, completion: completion) + updateProgress(from: oldValue, to: progress, animated: animated, completion: completion) case (false, false): backgroundLayer.removeAllAnimations() progressLayer.removeAllAnimations() @@ -216,17 +224,16 @@ final class CircularProgressView: NSView { } } - private func updateProgress(oldValue: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { + private func updateProgress(from oldValue: Double?, to progress: Double?, animated: Bool, completion: @escaping (Bool) -> Void) { guard let progress else { assertionFailure("Unexpected flow") completion(false) return } - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart let newStrokeStart = 1.0 - (progress >= 0.0 ? CGFloat(progress) : max(Constants.indeterminateProgressValue, min(0.9, 1.0 - currentStrokeStart))) - guard animated else { progressLayer.strokeStart = newStrokeStart @@ -274,7 +281,7 @@ final class CircularProgressView: NSView { guard let progress, progress == value else { return } if let oldValue, oldValue < 0, value != progress, animated { - updateProgress(oldValue: value, animated: animated) { _ in } + updateProgress(from: value, to: progress, animated: animated) { _ in } return } @@ -356,7 +363,7 @@ final class CircularProgressView: NSView { progressLayer.add(progressEndAnimation, forKey: #keyPath(CAShapeLayer.strokeEnd)) let progressAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeStart)) - let currentStrokeStart = (progressLayer.presentation() ?? progressLayer).strokeStart + let currentStrokeStart = progressLayer.currentStrokeStart progressLayer.removeAnimation(forKey: #keyPath(CAShapeLayer.strokeStart)) progressLayer.strokeStart = 0.0 @@ -375,6 +382,14 @@ final class CircularProgressView: NSView { private extension CAShapeLayer { + var currentStrokeStart: CGFloat { + if animation(forKey: #keyPath(CAShapeLayer.strokeStart)) != nil, + let presentation = self.presentation() { + return presentation.strokeStart + } + return strokeStart + } + func configureCircle(radius: CGFloat, lineWidth: CGFloat) { self.bounds = CGRect(x: 0, y: 0, width: (radius + lineWidth) * 2, height: (radius + lineWidth) * 2) @@ -530,14 +545,97 @@ struct CircularProgress: NSViewRepresentable { perform { progress = 1 } - perform { - progress = nil + Task { + perform { + progress = nil + } } } } label: { Text(verbatim: "0->1->nil").frame(width: 120) } + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } label: { + Text(verbatim: "nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->1->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 1 + } + Task { + perform { + progress = nil + } + Task { + perform { + progress = nil + } + } + } + } + } label: { + Text(verbatim: "nil->1->nil->nil").frame(width: 120) + } + + Button { + Task { + progress = nil + perform { + progress = 0 + } + try await Task.sleep(interval: 0.2) + for p in [0.26, 0.64, 0.95, 1, nil] { + perform { + progress = p + } + try await Task.sleep(interval: 0.001) + } + } + } label: { + Text(verbatim: "nil->0.2…1->nil").frame(width: 120) + } + Button { Task { perform { @@ -581,7 +679,7 @@ struct CircularProgress: NSViewRepresentable { .background(Color.white) Spacer() } - }.frame(width: 600, height: 400) + }.frame(width: 600, height: 500) } } return ProgressPreview() diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index 7f916c8cd2..6b81061e5e 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -579,7 +579,7 @@ final class NavigationBarViewController: NSViewController { } let heightChange: () -> Void - if animated && view.window != nil { + if animated, let window = view.window, window.isVisible == true { heightChange = { NSAnimationContext.runAnimationGroup { ctx in ctx.duration = 0.1 @@ -601,13 +601,13 @@ final class NavigationBarViewController: NSViewController { performResize() } } - if view.window == nil { - // update synchronously for off-screen view - heightChange() - } else { + if let window = view.window, window.isVisible { let dispatchItem = DispatchWorkItem(block: heightChange) DispatchQueue.main.async(execute: dispatchItem) self.heightChangeAnimation = dispatchItem + } else { + // update synchronously for off-screen view + heightChange() } } @@ -642,13 +642,19 @@ final class NavigationBarViewController: NSViewController { downloadListCoordinator.progress.publisher(for: \.totalUnitCount) .combineLatest(downloadListCoordinator.progress.publisher(for: \.completedUnitCount)) - .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .map { (total, completed) -> Double? in guard total > 0, completed < total else { return nil } return Double(completed) / Double(total) } + .dropFirst() + .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true) .sink { [weak downloadsProgressView] progress in - downloadsProgressView?.setProgress(progress, animated: true) + guard let downloadsProgressView else { return } + if progress == nil, downloadsProgressView.progress != 1 { + // show download completed animation before hiding + downloadsProgressView.setProgress(1, animated: true) + } + downloadsProgressView.setProgress(progress, animated: true) } .store(in: &downloadsCancellables) }