Swiftgram/submodules/RadialStatusNode/Sources/RadialDownloadContentNode.swift
2022-04-23 05:28:29 +04:00

225 lines
11 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import LegacyComponents
import SwiftSignalKit
private extension CAShapeLayer {
func animateStrokeStart(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeStart", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
}
func animateStrokeEnd(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "strokeEnd", timingFunction: timingFunction, duration: duration, delay: delay, removeOnCompletion: removeOnCompletion, completion: completion)
}
}
final class RadialDownloadContentNode: RadialStatusContentNode {
var color: UIColor {
didSet {
self.leftLine.strokeColor = self.color.cgColor
self.rightLine.strokeColor = self.color.cgColor
self.arrowBody.strokeColor = self.color.cgColor
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 1.0 {
didSet {
self.setNeedsDisplay()
}
}
private var enqueuedReadyForTransition: (() -> Void)?
private var isAnimatingTransition = false
private let leftLine = CAShapeLayer()
private let rightLine = CAShapeLayer()
private let arrowBody = CAShapeLayer()
init(color: UIColor) {
self.color = color
super.init()
self.leftLine.fillColor = UIColor.clear.cgColor
self.leftLine.strokeColor = self.color.cgColor
self.leftLine.lineCap = .round
self.leftLine.lineJoin = .round
self.rightLine.fillColor = UIColor.clear.cgColor
self.rightLine.strokeColor = self.color.cgColor
self.rightLine.lineCap = .round
self.rightLine.lineJoin = .round
self.arrowBody.fillColor = UIColor.clear.cgColor
self.arrowBody.strokeColor = self.color.cgColor
self.arrowBody.lineCap = .round
self.arrowBody.lineJoin = .round
self.isLayerBacked = true
self.isOpaque = false
self.layer.addSublayer(self.arrowBody)
self.layer.addSublayer(self.leftLine)
self.layer.addSublayer(self.rightLine)
}
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
if self.isAnimatingTransition {
self.enqueuedReadyForTransition = f
} else {
f()
}
}
private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1.0), offset: CGPoint = CGPoint()) throws -> UIBezierPath {
var index: UnsafePointer<UInt8> = path.utf8Start
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
let path = UIBezierPath()
while index < end {
let c = index.pointee
index = index.successor()
if c == 77 { // M
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.move(to: CGPoint(x: x, y: y))
} else if c == 76 { // L
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.addLine(to: CGPoint(x: x, y: y))
} else if c == 67 { // C
let x1 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y1 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
let x2 = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y2 = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
let x = try readCGFloat(&index, end: end, separator: 44) * scale.x + offset.x
let y = try readCGFloat(&index, end: end, separator: 32) * scale.y + offset.y
path.addCurve(to: CGPoint(x: x, y: y), controlPoint1: CGPoint(x: x1, y: y1), controlPoint2: CGPoint(x: x2, y: y2))
} else if c == 32 { // space
continue
}
}
return path
}
override func layout() {
super.layout()
let bounds = self.bounds
let diameter = min(bounds.size.width, bounds.size.height)
let factor = diameter / 50.0
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
self.leftLine.lineWidth = lineWidth
self.rightLine.lineWidth = lineWidth
self.arrowBody.lineWidth = lineWidth
let arrowHeadSize: CGFloat = 15.0 * factor
let arrowLength: CGFloat = 18.0 * factor
let arrowHeadOffset: CGFloat = 1.0 * factor
let leftPath = UIBezierPath()
leftPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
leftPath.addLine(to: CGPoint(x: diameter / 2.0 - arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
self.leftLine.path = leftPath.cgPath
let rightPath = UIBezierPath()
rightPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0 + arrowHeadOffset))
rightPath.addLine(to: CGPoint(x: diameter / 2.0 + arrowHeadSize / 2.0, y: diameter / 2.0 + arrowLength / 2.0 - arrowHeadSize / 2.0 + arrowHeadOffset))
self.rightLine.path = rightPath.cgPath
if self.delayPrepareAnimateIn {
self.delayPrepareAnimateIn = false
self.prepareAnimateIn(from: nil)
}
}
private let duration: Double = 0.2
override func prepareAnimateOut(completion: @escaping (Double) -> Void) {
let bounds = self.bounds
let diameter = min(bounds.size.width, bounds.size.height)
let factor = diameter / 50.0
var bodyPath = UIBezierPath()
if let path = try? svgPath("M1.10890748,47.3077093 C2.74202161,51.7201715 4.79761832,55.7299828 7.15775768,59.3122505 C25.4413606,87.0634763 62.001605,89.1563513 62.0066002,54.0178571 L62.0066002,0.625 ", scale: CGPoint(x: 0.333333 * factor, y: 0.333333 * factor), offset: CGPoint(x: (4.0 + UIScreenPixel) * factor, y: (17.0 - UIScreenPixel) * factor)) {
bodyPath = path
}
self.arrowBody.path = bodyPath.cgPath
self.arrowBody.strokeStart = 0.65
self.leftLine.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.rightLine.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.leftLine.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.07, removeOnCompletion: false) { finished in
completion(0.0)
}
self.rightLine.animateAlpha(from: 1.0, to: 0.0, duration: 0.02, delay: 0.15, removeOnCompletion: false) { finished in
self.leftLine.strokeColor = UIColor.clear.cgColor
self.rightLine.strokeColor = UIColor.clear.cgColor
}
}
override func animateOut(to: RadialStatusNodeState, completion: @escaping () -> Void) {
if self.bounds.width < 21.0 {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in
completion()
})
self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false)
} else {
self.isAnimatingTransition = true
self.arrowBody.animateStrokeStart(from: 0.65, to: 0.0, duration: 0.5, removeOnCompletion: false, completion: { [weak self] _ in
completion()
if let strongSelf = self, strongSelf.isAnimatingTransition, let f = strongSelf.enqueuedReadyForTransition {
strongSelf.isAnimatingTransition = false
f()
}
})
self.arrowBody.animateStrokeEnd(from: 1.0, to: 0.0, duration: 0.5, removeOnCompletion: false, completion: nil)
self.arrowBody.animateAlpha(from: 1.0, to: 0.0, duration: 0.01, delay: 0.4, removeOnCompletion: false)
}
}
private var delayPrepareAnimateIn = false
override func prepareAnimateIn(from: RadialStatusNodeState?) {
let bounds = self.bounds
let diameter = min(bounds.size.width, bounds.size.height)
guard !diameter.isZero else {
self.delayPrepareAnimateIn = true
return
}
let factor = diameter / 50.0
var bodyPath = UIBezierPath()
if let path = try? svgPath("M1.10890748,47.3077093 C2.74202161,51.7201715 4.79761832,55.7299828 7.15775768,59.3122505 C25.4413606,87.0634763 62.001605,89.1563513 62.0066002,54.0178571 L62.0066002,0.625 ", scale: CGPoint(x: -0.333333 * factor, y: 0.333333 * factor), offset: CGPoint(x: (46.0 - UIScreenPixel) * factor, y: (17.0 - UIScreenPixel) * factor)) {
bodyPath = path
}
self.arrowBody.path = bodyPath.cgPath
self.arrowBody.strokeStart = 0.65
}
override func animateIn(from: RadialStatusNodeState, delay: Double) {
if case .progress = from {
self.arrowBody.animateStrokeStart(from: 0.65, to: 0.65, duration: 0.25, delay: delay, removeOnCompletion: false, completion: nil)
self.arrowBody.animateStrokeEnd(from: 0.65, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false, completion: nil)
self.leftLine.animateStrokeEnd(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.rightLine.animateStrokeEnd(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.arrowBody.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.leftLine.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
self.rightLine.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: delay, removeOnCompletion: false)
} else {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, delay: delay)
self.layer.animateScale(from: 0.7, to: 1.0, duration: duration, delay: delay)
}
}
}