Swiftgram/TelegramUI/RadialCloudProgressContentNode.swift
Peter fc8fa045a6 Fixed Apple Pay
Added ability to download music without streaming
Added progress indicators for various blocking tasks
Fixed image gallery swipe to dismiss after zooming
Added online member count indication in supergroups
Fixed contact statuses in contact search
2018-10-13 03:31:39 +03:00

302 lines
11 KiB
Swift

import Foundation
import Display
import AsyncDisplayKit
import LegacyComponents
private final class RadialCloudProgressContentCancelNodeParameters: NSObject {
let color: UIColor
init(color: UIColor) {
self.color = color
}
}
private final class RadialCloudProgressContentSpinnerNodeParameters: NSObject {
let color: UIColor
let backgroundStrokeColor: UIColor
let progress: CGFloat
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, progress: CGFloat, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.progress = progress
self.lineWidth = lineWidth
}
}
private final class RadialCloudProgressContentSpinnerNode: ASDisplayNode {
var progressAnimationCompleted: (() -> Void)?
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
}
}
private var effectiveProgress: CGFloat = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
var progress: CGFloat? {
didSet {
self.pop_removeAnimation(forKey: "progress")
if let progress = self.progress {
self.pop_removeAnimation(forKey: "indefiniteProgress")
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty
animation.fromValue = CGFloat(self.effectiveProgress) as NSNumber
animation.toValue = CGFloat(progress) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 0.2
animation.completionBlock = { [weak self] _, _ in
self?.progressAnimationCompleted?()
}
self.pop_add(animation, forKey: "progress")
} else if self.pop_animation(forKey: "indefiniteProgress") == nil {
let animation = POPBasicAnimation()
animation.property = POPAnimatableProperty.property(withName: "progress", initializer: { property in
property?.readBlock = { node, values in
values?.pointee = (node as! RadialCloudProgressContentSpinnerNode).effectiveProgress
}
property?.writeBlock = { node, values in
(node as! RadialCloudProgressContentSpinnerNode).effectiveProgress = values!.pointee
}
property?.threshold = 0.01
}) as! POPAnimatableProperty
animation.fromValue = CGFloat(0.0) as NSNumber
animation.toValue = CGFloat(2.0) as NSNumber
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.duration = 2.5
animation.repeatForever = true
self.pop_add(animation, forKey: "indefiniteProgress")
}
}
}
var isAnimatingProgress: Bool {
return self.pop_animation(forKey: "progress") != nil
}
let lineWidth: CGFloat?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.lineWidth = lineWidth
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentSpinnerNodeParameters(color: self.color, backgroundStrokeColor: self.backgroundStrokeColor, progress: self.effectiveProgress, lineWidth: self.lineWidth)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentSpinnerNodeParameters {
let factor = bounds.size.width / 50.0
var progress = parameters.progress
var startAngle = -CGFloat.pi / 2.0
var endAngle = CGFloat(progress) * 2.0 * CGFloat.pi + startAngle
if progress > 1.0 {
progress = 2.0 - progress
let tmp = startAngle
startAngle = endAngle
endAngle = tmp
}
progress = min(1.0, progress)
let lineWidth: CGFloat = parameters.lineWidth ?? max(1.6, 2.25 * factor)
let pathDiameter: CGFloat
if parameters.lineWidth != nil {
pathDiameter = bounds.size.width - lineWidth
} else {
pathDiameter = bounds.size.width - lineWidth - 2.5 * 2.0
}
context.setStrokeColor(parameters.backgroundStrokeColor.cgColor)
let backgroundPath = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: 0.0, endAngle: 2.0 * CGFloat.pi, clockwise:true)
backgroundPath.lineWidth = lineWidth
backgroundPath.stroke()
context.setStrokeColor(parameters.color.cgColor)
let path = UIBezierPath(arcCenter: CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise:true)
path.lineWidth = lineWidth
path.lineCapStyle = .round
path.stroke()
}
}
override func willEnterHierarchy() {
super.willEnterHierarchy()
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
basicAnimation.duration = 2.0
basicAnimation.fromValue = NSNumber(value: Float(0.0))
basicAnimation.toValue = NSNumber(value: Float.pi * 2.0)
basicAnimation.repeatCount = Float.infinity
basicAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
basicAnimation.beginTime = 1.0
self.layer.add(basicAnimation, forKey: "progressRotation")
}
override func didExitHierarchy() {
super.didExitHierarchy()
self.layer.removeAnimation(forKey: "progressRotation")
}
}
private final class RadialCloudProgressContentCancelNode: ASDisplayNode {
var color: UIColor {
didSet {
self.setNeedsDisplay()
}
}
init(color: UIColor) {
self.color = color
super.init()
self.isLayerBacked = true
self.displaysAsynchronously = true
self.isOpaque = false
}
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
return RadialCloudProgressContentCancelNodeParameters(color: self.color)
}
@objc override class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
let context = UIGraphicsGetCurrentContext()!
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
context.fill(bounds)
}
if let parameters = parameters as? RadialCloudProgressContentCancelNodeParameters {
let size: CGFloat = 8.0
context.setFillColor(parameters.color.cgColor)
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: floor((bounds.size.width - size) / 2.0), y: floor((bounds.size.height - size) / 2.0)), size: CGSize(width: size, height: size)), cornerRadius: 1.0)
path.fill()
}
}
}
final class RadialCloudProgressContentNode: RadialStatusContentNode {
private let spinnerNode: RadialCloudProgressContentSpinnerNode
private let cancelNode: RadialCloudProgressContentCancelNode
var color: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.color = self.color
}
}
var backgroundStrokeColor: UIColor {
didSet {
self.setNeedsDisplay()
self.spinnerNode.backgroundStrokeColor = self.backgroundStrokeColor
}
}
var progress: CGFloat? = 0.0 {
didSet {
self.spinnerNode.progress = self.progress
}
}
private var enqueuedReadyForTransition: (() -> Void)?
init(color: UIColor, backgroundStrokeColor: UIColor, lineWidth: CGFloat?) {
self.color = color
self.backgroundStrokeColor = backgroundStrokeColor
self.spinnerNode = RadialCloudProgressContentSpinnerNode(color: color, backgroundStrokeColor: backgroundStrokeColor, lineWidth: lineWidth)
self.cancelNode = RadialCloudProgressContentCancelNode(color: color)
super.init()
self.isLayerBacked = true
self.addSubnode(self.spinnerNode)
self.addSubnode(self.cancelNode)
self.spinnerNode.progressAnimationCompleted = { [weak self] in
if let strongSelf = self {
if let enqueuedReadyForTransition = strongSelf.enqueuedReadyForTransition {
strongSelf.enqueuedReadyForTransition = nil
enqueuedReadyForTransition()
}
}
}
}
override func enqueueReadyForTransition(_ f: @escaping () -> Void) {
if self.spinnerNode.isAnimatingProgress {
self.enqueuedReadyForTransition = f
} else {
f()
}
}
override func layout() {
super.layout()
let bounds = self.bounds
self.spinnerNode.bounds = bounds
self.spinnerNode.position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
self.cancelNode.frame = bounds
}
override func animateOut(completion: @escaping () -> Void) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
self.cancelNode.layer.animateScale(from: 1.0, to: 0.3, duration: 0.15, removeOnCompletion: false)
}
override func animateIn() {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
self.cancelNode.layer.animateScale(from: 0.3, to: 1.0, duration: 0.15)
}
}