mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
248 lines
9.9 KiB
Swift
248 lines
9.9 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
import LegacyComponents
|
|
import TelegramPresentationData
|
|
import TooltipUI
|
|
|
|
private final class ChecksNodeParameters: NSObject {
|
|
let color: UIColor
|
|
let progress: CGFloat
|
|
|
|
init(color: UIColor, progress: CGFloat) {
|
|
self.color = color
|
|
self.progress = progress
|
|
|
|
super.init()
|
|
}
|
|
}
|
|
|
|
private class ChecksNode: ASDisplayNode {
|
|
var state: Bool? = nil
|
|
|
|
var color: UIColor {
|
|
didSet {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
private var effectiveProgress: CGFloat = 1.0 {
|
|
didSet {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
|
|
init(color: UIColor) {
|
|
self.color = color
|
|
|
|
super.init()
|
|
|
|
self.backgroundColor = .clear
|
|
self.isOpaque = false
|
|
}
|
|
|
|
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! ChecksNode).effectiveProgress
|
|
}
|
|
property?.writeBlock = { node, values in
|
|
(node as! ChecksNode).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 ChecksNodeParameters(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? ChecksNodeParameters else {
|
|
return
|
|
}
|
|
|
|
let scaleFactor: CGFloat = 1.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()
|
|
}
|
|
|
|
func updateState(_ state: Bool, animated: Bool) {
|
|
guard state != self.state else {
|
|
return
|
|
}
|
|
let previousState = self.state
|
|
self.state = state
|
|
if animated {
|
|
if previousState == nil && self.state == false {
|
|
self.animateProgress(from: 0.0, to: 1.0)
|
|
} else if previousState == false && self.state == true {
|
|
self.animateProgress(from: 1.0, to: 2.0)
|
|
}
|
|
} else {
|
|
if let state = self.state {
|
|
self.effectiveProgress = state ? 2.0 : 1.0
|
|
} else {
|
|
self.effectiveProgress = 0.0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ChatStatusChecksTooltipContentNode: ASDisplayNode, TooltipControllerCustomContentNode {
|
|
private let deliveredChecksNode: ChecksNode
|
|
private let deliveredTextNode: ImmediateTextNode
|
|
private let readChecksNode: ChecksNode
|
|
private let readTextNode: ImmediateTextNode
|
|
|
|
init(presentationData: PresentationData) {
|
|
self.deliveredChecksNode = ChecksNode(color: .white)
|
|
self.deliveredTextNode = ImmediateTextNode()
|
|
self.readChecksNode = ChecksNode(color: .white)
|
|
self.readTextNode = ImmediateTextNode()
|
|
|
|
self.deliveredTextNode.attributedText = NSAttributedString(string: presentationData.strings.Conversation_ChecksTooltip_Delivered, font: Font.regular(14.0), textColor: UIColor.white)
|
|
self.readTextNode.attributedText = NSAttributedString(string: presentationData.strings.Conversation_ChecksTooltip_Read, font: Font.regular(14.0), textColor: UIColor.white)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.deliveredChecksNode)
|
|
self.addSubnode(self.deliveredTextNode)
|
|
self.addSubnode(self.readChecksNode)
|
|
self.addSubnode(self.readTextNode)
|
|
}
|
|
|
|
func animateIn() {
|
|
self.deliveredChecksNode.updateState(false, animated: true)
|
|
self.readChecksNode.updateState(false, animated: true)
|
|
|
|
Queue.mainQueue().after(0.25) {
|
|
self.deliveredChecksNode.layer.animateScale(from: 1.0, to: 1.12, duration: 0.25, delay: 0.0, removeOnCompletion: false, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.deliveredChecksNode.layer.animateScale(from: 1.12, to: 1.0, duration: 0.25)
|
|
}
|
|
})
|
|
|
|
self.deliveredTextNode.layer.animateScale(from: 1.0, to: 1.12, duration: 0.25, delay: 0.0, removeOnCompletion: false, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.deliveredTextNode.layer.animateScale(from: 1.12, to: 1.0, duration: 0.25)
|
|
}
|
|
})
|
|
|
|
Queue.mainQueue().after(0.5) {
|
|
self.readChecksNode.updateState(true, animated: true)
|
|
|
|
self.readChecksNode.layer.animateScale(from: 1.0, to: 1.12, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.readChecksNode.layer.animateScale(from: 1.12, to: 1.0, duration: 0.25)
|
|
}
|
|
})
|
|
|
|
self.readTextNode.layer.animateScale(from: 1.0, to: 1.12, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in
|
|
if let strongSelf = self {
|
|
strongSelf.readTextNode.layer.animateScale(from: 1.12, to: 1.0, duration: 0.25)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateLayout(size: CGSize) -> CGSize {
|
|
let deliveredSize = self.deliveredTextNode.updateLayout(size)
|
|
let readSize = self.readTextNode.updateLayout(size)
|
|
|
|
let checksInset: CGFloat = 8.0
|
|
let checksSize = CGSize(width: 24.0, height: 24.0)
|
|
|
|
self.deliveredChecksNode.frame = CGRect(origin: CGPoint(x: checksInset, y: 15.0), size: checksSize)
|
|
self.deliveredTextNode.frame = CGRect(origin: CGPoint(x: checksInset + checksSize.width + 5.0, y: 19.0), size: deliveredSize)
|
|
self.readChecksNode.frame = CGRect(origin: CGPoint(x: checksInset, y: 38.0), size: checksSize)
|
|
self.readTextNode.frame = CGRect(origin: CGPoint(x: checksInset + checksSize.width + 5.0, y: 43.0), size: readSize)
|
|
|
|
let contentWidth = max(deliveredSize.width, readSize.width) + checksInset + checksSize.width + 18.0
|
|
let contentHeight: CGFloat = 77.0
|
|
|
|
return CGSize(width: contentWidth, height: contentHeight)
|
|
}
|
|
}
|