mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
481 lines
17 KiB
Swift
481 lines
17 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import LegacyComponents
|
|
import RadialStatusNode
|
|
|
|
enum ChatListStatusNodeState: Equatable {
|
|
case none
|
|
case clock(UIImage?, UIImage?)
|
|
case delivered(UIColor)
|
|
case read(UIColor)
|
|
case progress(UIColor, CGFloat)
|
|
case failed(UIColor, UIColor)
|
|
|
|
func contentNode() -> ChatListStatusContentNode? {
|
|
switch self {
|
|
case .none:
|
|
return nil
|
|
case let .clock(frameImage, minImage):
|
|
return ChatListStatusClockNode(frameImage: frameImage, minImage: minImage)
|
|
case let .delivered(color):
|
|
return ChatListStatusChecksNode(color: color)
|
|
case let .read(color):
|
|
return ChatListStatusChecksNode(color: color)
|
|
case let .progress(color, progress):
|
|
return ChatListStatusProgressNode(color: color, progress: progress)
|
|
case let .failed(fill, foreground):
|
|
return ChatListStatusFailedNode(fill: fill, foreground: foreground)
|
|
}
|
|
}
|
|
}
|
|
|
|
private let transitionDuration = 0.2
|
|
|
|
class ChatListStatusContentNode: ASDisplayNode {
|
|
var fontSize: CGFloat = 17.0
|
|
|
|
override init() {
|
|
super.init()
|
|
|
|
self.isOpaque = false
|
|
}
|
|
|
|
func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
|
|
}
|
|
|
|
func animateOut(to: ChatListStatusNodeState, completion: @escaping () -> Void) {
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration, removeOnCompletion: false, completion: { _ in
|
|
completion()
|
|
})
|
|
}
|
|
|
|
func animateIn(from: ChatListStatusNodeState) {
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: transitionDuration)
|
|
}
|
|
}
|
|
|
|
final class ChatListStatusNode: ASDisplayNode {
|
|
private(set) var state: ChatListStatusNodeState = .none
|
|
|
|
var fontSize: CGFloat = 17.0 {
|
|
didSet {
|
|
self.contentNode?.fontSize = self.fontSize
|
|
self.nextContentNode?.fontSize = self.fontSize
|
|
}
|
|
}
|
|
|
|
private var contentNode: ChatListStatusContentNode?
|
|
private var nextContentNode: ChatListStatusContentNode?
|
|
|
|
public func transitionToState(_ state: ChatListStatusNodeState, animated: Bool = false, completion: @escaping () -> Void = {}) -> Bool {
|
|
if self.state != state {
|
|
let currentState = self.state
|
|
self.state = state
|
|
|
|
let contentNode = state.contentNode()
|
|
contentNode?.fontSize = self.fontSize
|
|
if contentNode?.classForCoder != self.contentNode?.classForCoder {
|
|
contentNode?.updateWithState(state, animated: animated)
|
|
self.transitionToContentNode(contentNode, state: state, fromState: currentState, animated: animated, completion: completion)
|
|
} else {
|
|
self.contentNode?.updateWithState(state, animated: animated)
|
|
}
|
|
return true
|
|
} else {
|
|
completion()
|
|
return false
|
|
}
|
|
}
|
|
|
|
private func transitionToContentNode(_ node: ChatListStatusContentNode?, state: ChatListStatusNodeState, fromState: ChatListStatusNodeState, animated: Bool, completion: @escaping () -> Void) {
|
|
if let previousContentNode = self.contentNode {
|
|
if !animated {
|
|
previousContentNode.removeFromSupernode()
|
|
self.contentNode = node
|
|
if let contentNode = self.contentNode {
|
|
self.addSubnode(contentNode)
|
|
}
|
|
} else {
|
|
self.contentNode = node
|
|
if let contentNode = self.contentNode {
|
|
self.addSubnode(contentNode)
|
|
contentNode.frame = self.bounds
|
|
if self.isNodeLoaded {
|
|
contentNode.animateIn(from: fromState)
|
|
contentNode.layout()
|
|
}
|
|
}
|
|
previousContentNode.animateOut(to: state) {
|
|
previousContentNode.removeFromSupernode()
|
|
}
|
|
}
|
|
} else {
|
|
self.contentNode = node
|
|
if let contentNode = self.contentNode {
|
|
contentNode.frame = self.bounds
|
|
self.addSubnode(contentNode)
|
|
if self.isNodeLoaded {
|
|
contentNode.layout()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override public func layout() {
|
|
if let contentNode = self.contentNode {
|
|
contentNode.frame = self.bounds
|
|
}
|
|
}
|
|
}
|
|
|
|
class ChatListStatusClockNode: ChatListStatusContentNode {
|
|
private var clockFrameNode: ASImageNode
|
|
private var clockMinNode: ASImageNode
|
|
|
|
init(frameImage: UIImage?, minImage: UIImage?) {
|
|
self.clockFrameNode = ASImageNode()
|
|
self.clockMinNode = ASImageNode()
|
|
|
|
super.init()
|
|
|
|
self.clockFrameNode.image = frameImage
|
|
self.clockMinNode.image = minImage
|
|
|
|
self.addSubnode(self.clockFrameNode)
|
|
self.addSubnode(self.clockMinNode)
|
|
}
|
|
|
|
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
|
|
if case let .clock(frameImage, minImage) = state {
|
|
self.clockFrameNode.image = frameImage
|
|
self.clockMinNode.image = minImage
|
|
}
|
|
}
|
|
|
|
override func didEnterHierarchy() {
|
|
super.didEnterHierarchy()
|
|
|
|
maybeAddRotationAnimation(self.clockFrameNode.layer, duration: 6.0)
|
|
maybeAddRotationAnimation(self.clockMinNode.layer, duration: 1.0)
|
|
}
|
|
|
|
override func didExitHierarchy() {
|
|
super.didExitHierarchy()
|
|
|
|
self.clockFrameNode.layer.removeAllAnimations()
|
|
self.clockMinNode.layer.removeAllAnimations()
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
let bounds = self.bounds
|
|
if let frameImage = self.clockFrameNode.image {
|
|
self.clockFrameNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - frameImage.size.width) / 2.0), y: floorToScreenPixels((bounds.height - frameImage.size.height) / 2.0)), size: frameImage.size)
|
|
}
|
|
if let minImage = self.clockMinNode.image {
|
|
self.clockMinNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - minImage.size.width) / 2.0), y: floorToScreenPixels((bounds.height - minImage.size.height) / 2.0)), size: minImage.size)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func maybeAddRotationAnimation(_ layer: CALayer, duration: Double) {
|
|
if let _ = layer.animation(forKey: "clockFrameAnimation") {
|
|
return
|
|
}
|
|
|
|
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
|
|
basicAnimation.duration = duration
|
|
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
|
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
|
|
basicAnimation.repeatCount = Float.infinity
|
|
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
|
basicAnimation.beginTime = 1.0
|
|
layer.add(basicAnimation, forKey: "clockFrameAnimation")
|
|
}
|
|
|
|
private final class StatusChecksNodeParameters: NSObject {
|
|
let color: UIColor
|
|
let progress: CGFloat
|
|
let fontSize: CGFloat
|
|
|
|
init(color: UIColor, progress: CGFloat, fontSize: CGFloat) {
|
|
self.color = color
|
|
self.progress = progress
|
|
self.fontSize = fontSize
|
|
|
|
super.init()
|
|
}
|
|
}
|
|
|
|
private class ChatListStatusChecksNode: ChatListStatusContentNode {
|
|
private var state: ChatListStatusNodeState?
|
|
|
|
var color: UIColor {
|
|
didSet {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
private var effectiveProgress: CGFloat = 1.0 {
|
|
didSet {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
override var fontSize: CGFloat {
|
|
didSet {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
init(color: UIColor) {
|
|
self.color = color
|
|
|
|
super.init()
|
|
}
|
|
|
|
func animateProgress(from: CGFloat, to: CGFloat) {
|
|
self.pop_removeAllAnimations()
|
|
|
|
let animation = POPBasicAnimation()
|
|
animation.property = (POPAnimatableProperty.property(withName: "progress", initializer: { property in
|
|
property?.readBlock = { node, values in
|
|
values?.pointee = (node as! ChatListStatusChecksNode).effectiveProgress
|
|
}
|
|
property?.writeBlock = { node, values in
|
|
(node as! ChatListStatusChecksNode).effectiveProgress = values!.pointee
|
|
}
|
|
property?.threshold = 0.01
|
|
}) as! POPAnimatableProperty)
|
|
animation.fromValue = from as NSNumber
|
|
animation.toValue = to as NSNumber
|
|
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
|
animation.duration = 0.2
|
|
self.pop_add(animation, forKey: "progress")
|
|
}
|
|
|
|
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
|
return StatusChecksNodeParameters(color: self.color, progress: self.effectiveProgress, fontSize: self.fontSize)
|
|
}
|
|
|
|
override func didEnterHierarchy() {
|
|
super.didEnterHierarchy()
|
|
}
|
|
|
|
@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)
|
|
}
|
|
|
|
guard let parameters = parameters as? StatusChecksNodeParameters else {
|
|
return
|
|
}
|
|
|
|
let scaleFactor = min(1.4, parameters.fontSize / 17.0)
|
|
context.translateBy(x: bounds.width / 2.0, y: bounds.height / 2.0)
|
|
context.scaleBy(x: scaleFactor, y: scaleFactor)
|
|
context.translateBy(x: -bounds.width / 2.0, y: -bounds.height / 2.0)
|
|
|
|
let progress = parameters.progress
|
|
|
|
context.setStrokeColor(parameters.color.cgColor)
|
|
context.setLineWidth(1.0 + UIScreenPixel)
|
|
context.setLineCap(.round)
|
|
context.setLineJoin(.round)
|
|
context.setMiterLimit(10.0)
|
|
|
|
context.saveGState()
|
|
var s1 = CGPoint(x: 9.0, y: 13.0)
|
|
var s2 = CGPoint(x: 5.0, y: 13.0)
|
|
let p1 = CGPoint(x: 3.5, y: 3.5)
|
|
let p2 = CGPoint(x: 7.5 - UIScreenPixel, y: -8.0)
|
|
|
|
let check1FirstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
|
|
let check2FirstSegment: CGFloat = max(0.0, min(1.0, (progress - 1.0) * 3.0))
|
|
|
|
let firstProgress = max(0.0, min(1.0, progress))
|
|
let secondProgress = max(0.0, min(1.0, progress - 1.0))
|
|
|
|
let scale: CGFloat = 1.2
|
|
context.translateBy(x: 16.0, y: 13.0)
|
|
context.scaleBy(x: scale - abs((scale - 1.0) * (firstProgress - 0.5) / 0.5), y: scale - abs((scale - 1.0) * (firstProgress - 0.5) / 0.5))
|
|
s1 = s1.offsetBy(dx: -16.0, dy: -13.0)
|
|
|
|
if !check1FirstSegment.isZero {
|
|
if check1FirstSegment < 1.0 {
|
|
context.move(to: CGPoint(x: s1.x + p1.x * check1FirstSegment, y: s1.y + p1.y * check1FirstSegment))
|
|
context.addLine(to: s1)
|
|
} else {
|
|
let secondSegment = (min(1.0, progress) - 0.33) * 1.5
|
|
context.move(to: CGPoint(x: s1.x + p1.x + p2.x * secondSegment, y: s1.y + p1.y + p2.y * secondSegment))
|
|
context.addLine(to: CGPoint(x: s1.x + p1.x, y: s1.y + p1.y))
|
|
context.addLine(to: CGPoint(x: s1.x + p1.x * min(1.0, check2FirstSegment), y: s1.y + p1.y * min(1.0, check2FirstSegment)))
|
|
}
|
|
}
|
|
context.strokePath()
|
|
|
|
context.restoreGState()
|
|
|
|
context.translateBy(x: 12.0, y: 13.0)
|
|
context.scaleBy(x: scale - abs((scale - 1.0) * (secondProgress - 0.5) / 0.5), y: scale - abs((scale - 1.0) * (secondProgress - 0.5) / 0.5))
|
|
s2 = s2.offsetBy(dx: -12.0, dy: -13.0)
|
|
|
|
if !check2FirstSegment.isZero {
|
|
if check2FirstSegment < 1.0 {
|
|
context.move(to: CGPoint(x: s2.x + p1.x * check2FirstSegment, y: s2.y + p1.y * check2FirstSegment))
|
|
context.addLine(to: s2)
|
|
} else {
|
|
let secondSegment = (max(0.0, (progress - 1.0)) - 0.33) * 1.5
|
|
context.move(to: CGPoint(x: s2.x + p1.x + p2.x * secondSegment, y: s2.y + p1.y + p2.y * secondSegment))
|
|
context.addLine(to: CGPoint(x: s2.x + p1.x, y: s2.y + p1.y))
|
|
context.addLine(to: s2)
|
|
}
|
|
}
|
|
context.strokePath()
|
|
}
|
|
|
|
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
|
|
switch state {
|
|
case let .delivered(color), let .read(color):
|
|
self.color = color
|
|
default:
|
|
break
|
|
}
|
|
var animating = false
|
|
if let previousState = self.state, case .delivered = previousState, case .read = state, animated {
|
|
animating = true
|
|
self.animateProgress(from: 1.0, to: 2.0)
|
|
}
|
|
if !animating {
|
|
if case .delivered = state {
|
|
self.effectiveProgress = 1.0
|
|
} else if case .read = state {
|
|
self.effectiveProgress = 2.0
|
|
}
|
|
}
|
|
self.state = state
|
|
}
|
|
|
|
override func animateIn(from: ChatListStatusNodeState) {
|
|
if let state = self.state, case .delivered = state {
|
|
self.animateProgress(from: 0.0, to: 1.0)
|
|
} else {
|
|
super.animateIn(from: from)
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class ChatListStatusFailedNodeParameters: NSObject {
|
|
let fill: UIColor
|
|
let foreground: UIColor
|
|
|
|
init(fill: UIColor, foreground: UIColor) {
|
|
self.fill = fill
|
|
self.foreground = foreground
|
|
|
|
super.init()
|
|
}
|
|
}
|
|
|
|
private class ChatListStatusFailedNode: ChatListStatusContentNode {
|
|
private var state: ChatListStatusNodeState?
|
|
|
|
var fill: UIColor {
|
|
didSet {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
var foreground: UIColor {
|
|
didSet {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
init(fill: UIColor, foreground: UIColor) {
|
|
self.fill = fill
|
|
self.foreground = foreground
|
|
|
|
super.init()
|
|
}
|
|
|
|
override func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
|
return ChatListStatusFailedNodeParameters(fill: self.fill, foreground: self.foreground)
|
|
}
|
|
|
|
@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)
|
|
}
|
|
|
|
guard let parameters = parameters as? ChatListStatusFailedNodeParameters else {
|
|
return
|
|
}
|
|
|
|
let diameter: CGFloat = 14.0
|
|
let rect = CGRect(origin: CGPoint(x: floor((bounds.width - diameter) / 2.0), y: floor((bounds.height - diameter) / 2.0)), size: CGSize(width: diameter, height: diameter)).offsetBy(dx: 1.0, dy: UIScreenPixel)
|
|
|
|
context.setFillColor(parameters.fill.cgColor)
|
|
context.fillEllipse(in: rect)
|
|
context.setStrokeColor(parameters.foreground.cgColor)
|
|
|
|
let string = NSAttributedString(string: "!", font: Font.medium(12.0), textColor: parameters.foreground)
|
|
let stringRect = string.boundingRect(with: rect.size, options: .usesLineFragmentOrigin, context: nil)
|
|
|
|
UIGraphicsPushContext(context)
|
|
string.draw(at: CGPoint(x: rect.minX + floor((rect.width - stringRect.width) / 2.0), y: 1.0 - UIScreenPixel + rect.minY + floor((rect.height - stringRect.height) / 2.0)))
|
|
UIGraphicsPopContext()
|
|
}
|
|
|
|
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
|
|
switch state {
|
|
case let .failed(fill, foreground):
|
|
self.fill = fill
|
|
self.foreground = foreground
|
|
default:
|
|
break
|
|
}
|
|
self.state = state
|
|
}
|
|
}
|
|
|
|
private class ChatListStatusProgressNode: ChatListStatusContentNode {
|
|
private let statusNode: RadialStatusNode
|
|
|
|
init(color: UIColor, progress: CGFloat) {
|
|
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
|
|
|
|
super.init()
|
|
|
|
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true))
|
|
|
|
self.addSubnode(self.statusNode)
|
|
}
|
|
|
|
override func updateWithState(_ state: ChatListStatusNodeState, animated: Bool) {
|
|
if case let .progress(color, progress) = state {
|
|
self.statusNode.transitionToState(.progress(color: color, lineWidth: 1.0, value: progress, cancelEnabled: false, animateRotation: true), animated: animated, completion: {})
|
|
}
|
|
}
|
|
|
|
override func layout() {
|
|
super.layout()
|
|
|
|
let bounds = self.bounds
|
|
let size = CGSize(width: 12.0, height: 12.0)
|
|
self.statusNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.width - size.width) / 2.0), y: floorToScreenPixels((bounds.height - size.height) / 2.0)), size: size)
|
|
}
|
|
}
|