mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 23:24:32 +00:00
Working Animation Lock Screen
This commit is contained in:
parent
68b912463c
commit
71e6be4218
9 changed files with 158 additions and 30 deletions
121
DynamicIsland/components/LockScreen/LockIconAnimationView.swift
Normal file
121
DynamicIsland/components/LockScreen/LockIconAnimationView.swift
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// LockIconAnimationView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Shared animator and SwiftUI Lottie rendering for the lock/unlock sequence.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Lottie
|
||||
|
||||
@MainActor
|
||||
final class LockIconAnimator: ObservableObject {
|
||||
@Published private(set) var progress: CGFloat
|
||||
|
||||
private var animationTask: Task<Void, Never>?
|
||||
private let animationDuration: TimeInterval = 0.35
|
||||
private let animationSteps: Int = 48
|
||||
|
||||
init(initiallyLocked: Bool) {
|
||||
progress = initiallyLocked ? 1.0 : 0.0
|
||||
}
|
||||
|
||||
deinit {
|
||||
animationTask?.cancel()
|
||||
}
|
||||
|
||||
func update(isLocked: Bool, animated: Bool = true) {
|
||||
let target = isLocked ? 1.0 : 0.0
|
||||
let clampedTarget = max(0.0, min(1.0, target))
|
||||
|
||||
if !animated {
|
||||
animationTask?.cancel()
|
||||
progress = clampedTarget
|
||||
return
|
||||
}
|
||||
|
||||
guard abs(progress - clampedTarget) > 0.0005 else {
|
||||
progress = clampedTarget
|
||||
return
|
||||
}
|
||||
|
||||
animationTask?.cancel()
|
||||
|
||||
let startProgress = progress
|
||||
let delta = clampedTarget - startProgress
|
||||
let stepDuration = animationDuration / Double(animationSteps)
|
||||
let stepNanoseconds = UInt64(stepDuration * 1_000_000_000)
|
||||
|
||||
animationTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
for step in 0...animationSteps {
|
||||
if Task.isCancelled { return }
|
||||
|
||||
if step > 0 {
|
||||
try? await Task.sleep(nanoseconds: stepNanoseconds)
|
||||
}
|
||||
|
||||
let fraction = Double(step) / Double(animationSteps)
|
||||
let eased = easeOutCubic(fraction)
|
||||
progress = startProgress + CGFloat(eased) * CGFloat(delta)
|
||||
}
|
||||
|
||||
progress = clampedTarget
|
||||
}
|
||||
}
|
||||
|
||||
private func easeOutCubic(_ t: Double) -> Double {
|
||||
let clamped = max(0.0, min(1.0, t))
|
||||
return 1.0 - pow(1.0 - clamped, 3)
|
||||
}
|
||||
}
|
||||
|
||||
struct LockIconProgressView: View {
|
||||
var progress: CGFloat
|
||||
var iconColor: Color = .white
|
||||
|
||||
var body: some View {
|
||||
if LockIconLottieView.isAvailable {
|
||||
Rectangle()
|
||||
.fill(iconColor)
|
||||
.mask {
|
||||
LockIconLottieView(progress: 1 - progress)
|
||||
.scaleEffect(1.12)
|
||||
}
|
||||
} else {
|
||||
Image(systemName: progress >= 0.5 ? "lock.fill" : "lock.open.fill")
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.foregroundStyle(iconColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LockIconLottieView: View {
|
||||
var progress: CGFloat
|
||||
|
||||
private static let animation: LottieAnimation? = {
|
||||
if let animation = LottieAnimation.named("Lock") {
|
||||
return animation
|
||||
} else {
|
||||
print("⚠️ [LockIconLottieView] Missing Lock.json animation – falling back to SF Symbols")
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
static var isAvailable: Bool {
|
||||
animation != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let animation = Self.animation {
|
||||
LottieView(animation: animation)
|
||||
.currentProgress(progress)
|
||||
.configuration(.init(renderingEngine: .mainThread))
|
||||
} else {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue