Skip to content

Commit

Permalink
Fix quick download button animation (#2711)
Browse files Browse the repository at this point in the history
  • Loading branch information
mallexxx authored Apr 29, 2024
1 parent 956e526 commit 8762c13
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 16 deletions.
116 changes: 107 additions & 9 deletions DuckDuckGo/Common/View/AppKit/CircularProgressView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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()
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -581,7 +679,7 @@ struct CircularProgress: NSViewRepresentable {
.background(Color.white)
Spacer()
}
}.frame(width: 600, height: 400)
}.frame(width: 600, height: 500)
}
}
return ProgressPreview()
Expand Down
20 changes: 13 additions & 7 deletions DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
}
}

Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit 8762c13

Please sign in to comment.