mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
Refactor ChatListUI
This commit is contained in:
457
submodules/ChatListUI/Sources/Node/ChatListStatusNode.swift
Normal file
457
submodules/ChatListUI/Sources/Node/ChatListStatusNode.swift
Normal file
@@ -0,0 +1,457 @@
|
||||
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 {
|
||||
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
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
init(color: UIColor, progress: CGFloat) {
|
||||
self.color = color
|
||||
self.progress = progress
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 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: 1.0)
|
||||
|
||||
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 + 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))
|
||||
|
||||
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), 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user