mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
800 lines
33 KiB
Swift
800 lines
33 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Display
|
|
import SwiftSignalKit
|
|
|
|
public enum SemanticStatusNodeState: Equatable {
|
|
public struct ProgressAppearance: Equatable {
|
|
public var inset: CGFloat
|
|
public var lineWidth: CGFloat
|
|
|
|
public init(inset: CGFloat, lineWidth: CGFloat) {
|
|
self.inset = inset
|
|
self.lineWidth = lineWidth
|
|
}
|
|
}
|
|
|
|
public struct CheckAppearance: Equatable {
|
|
public var lineWidth: CGFloat
|
|
|
|
public init(lineWidth: CGFloat) {
|
|
self.lineWidth = lineWidth
|
|
}
|
|
}
|
|
|
|
case none
|
|
case download
|
|
case play
|
|
case pause
|
|
case check(appearance: CheckAppearance?)
|
|
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?)
|
|
case customIcon(UIImage)
|
|
}
|
|
|
|
private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
|
|
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor)
|
|
}
|
|
|
|
private protocol SemanticStatusNodeStateContext: class {
|
|
var isAnimating: Bool { get }
|
|
|
|
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState
|
|
}
|
|
|
|
private enum SemanticStatusNodeIcon: Equatable {
|
|
case none
|
|
case download
|
|
case play
|
|
case pause
|
|
case custom(UIImage)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext {
|
|
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
|
|
let transitionFraction: CGFloat
|
|
let icon: SemanticStatusNodeIcon
|
|
|
|
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon) {
|
|
self.transitionFraction = transitionFraction
|
|
self.icon = icon
|
|
|
|
super.init()
|
|
}
|
|
|
|
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
|
|
context.saveGState()
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
|
|
if foregroundColor.alpha.isZero {
|
|
context.setBlendMode(.destinationOut)
|
|
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
} else {
|
|
context.setBlendMode(.normal)
|
|
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
}
|
|
|
|
switch self.icon {
|
|
case .none:
|
|
break
|
|
case .play:
|
|
let diameter = size.width
|
|
|
|
let factor = diameter / 50.0
|
|
|
|
let size = CGSize(width: 15.0, height: 18.0)
|
|
context.translateBy(x: (diameter - size.width) / 2.0 + 1.5, y: (diameter - size.height) / 2.0)
|
|
if (diameter < 40.0) {
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: factor, y: factor)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
}
|
|
let _ = try? drawSvgPath(context, path: "M1.71891969,0.209353049 C0.769586558,-0.350676705 0,0.0908839327 0,1.18800046 L0,16.8564753 C0,17.9569971 0.750549162,18.357187 1.67393713,17.7519379 L14.1073836,9.60224049 C15.0318735,8.99626906 15.0094718,8.04970371 14.062401,7.49100858 L1.71891969,0.209353049 ")
|
|
context.fillPath()
|
|
if (diameter < 40.0) {
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
}
|
|
context.translateBy(x: -(diameter - size.width) / 2.0 - 1.5, y: -(diameter - size.height) / 2.0)
|
|
case .pause:
|
|
let diameter = size.width
|
|
|
|
let factor = diameter / 50.0
|
|
|
|
let size = CGSize(width: 15.0, height: 16.0)
|
|
context.translateBy(x: (diameter - size.width) / 2.0, y: (diameter - size.height) / 2.0)
|
|
if (diameter < 40.0) {
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: factor, y: factor)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
}
|
|
let _ = try? drawSvgPath(context, path: "M0,1.00087166 C0,0.448105505 0.443716645,0 0.999807492,0 L4.00019251,0 C4.55237094,0 5,0.444630861 5,1.00087166 L5,14.9991283 C5,15.5518945 4.55628335,16 4.00019251,16 L0.999807492,16 C0.447629061,16 0,15.5553691 0,14.9991283 L0,1.00087166 Z M10,1.00087166 C10,0.448105505 10.4437166,0 10.9998075,0 L14.0001925,0 C14.5523709,0 15,0.444630861 15,1.00087166 L15,14.9991283 C15,15.5518945 14.5562834,16 14.0001925,16 L10.9998075,16 C10.4476291,16 10,15.5553691 10,14.9991283 L10,1.00087166 ")
|
|
context.fillPath()
|
|
if (diameter < 40.0) {
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: 1.0 / 0.8, y: 1.0 / 0.8)
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
}
|
|
context.translateBy(x: -(diameter - size.width) / 2.0, y: -(diameter - size.height) / 2.0)
|
|
case let .custom(image):
|
|
let diameter = size.width
|
|
|
|
let imageRect = CGRect(origin: CGPoint(x: floor((diameter - image.size.width) / 2.0), y: floor((diameter - image.size.height) / 2.0)), size: image.size)
|
|
|
|
context.saveGState()
|
|
context.translateBy(x: imageRect.midX, y: imageRect.midY)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
|
|
context.clip(to: imageRect, mask: image.cgImage!)
|
|
context.fill(imageRect)
|
|
context.restoreGState()
|
|
case .download:
|
|
let diameter = size.width
|
|
let factor = diameter / 50.0
|
|
let lineWidth: CGFloat = max(1.6, 2.25 * factor)
|
|
|
|
context.setLineWidth(lineWidth)
|
|
context.setLineCap(.round)
|
|
context.setLineJoin(.round)
|
|
|
|
let arrowHeadSize: CGFloat = 15.0 * factor
|
|
let arrowLength: CGFloat = 18.0 * factor
|
|
let arrowHeadOffset: CGFloat = 1.0 * factor
|
|
|
|
let leftPath = UIBezierPath()
|
|
leftPath.lineWidth = lineWidth
|
|
leftPath.lineCapStyle = .round
|
|
leftPath.lineJoinStyle = .round
|
|
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))
|
|
leftPath.stroke()
|
|
|
|
let rightPath = UIBezierPath()
|
|
rightPath.lineWidth = lineWidth
|
|
rightPath.lineCapStyle = .round
|
|
rightPath.lineJoinStyle = .round
|
|
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))
|
|
rightPath.stroke()
|
|
|
|
let bodyPath = UIBezierPath()
|
|
bodyPath.lineWidth = lineWidth
|
|
bodyPath.lineCapStyle = .round
|
|
bodyPath.lineJoinStyle = .round
|
|
bodyPath.move(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 - arrowLength / 2.0))
|
|
bodyPath.addLine(to: CGPoint(x: diameter / 2.0, y: diameter / 2.0 + arrowLength / 2.0))
|
|
bodyPath.stroke()
|
|
}
|
|
context.restoreGState()
|
|
}
|
|
}
|
|
|
|
let icon: SemanticStatusNodeIcon
|
|
|
|
init(icon: SemanticStatusNodeIcon) {
|
|
self.icon = icon
|
|
}
|
|
|
|
var isAnimating: Bool {
|
|
return false
|
|
}
|
|
|
|
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
|
return DrawingState(transitionFraction: transitionFraction, icon: self.icon)
|
|
}
|
|
}
|
|
|
|
private final class SemanticStatusNodeProgressTransition {
|
|
let beginTime: Double
|
|
let initialValue: CGFloat
|
|
|
|
init(beginTime: Double, initialValue: CGFloat) {
|
|
self.beginTime = beginTime
|
|
self.initialValue = initialValue
|
|
}
|
|
|
|
func valueAt(timestamp: Double, actualValue: CGFloat) -> CGFloat {
|
|
let duration = 0.2
|
|
var t = CGFloat((timestamp - self.beginTime) / duration)
|
|
t = min(1.0, max(0.0, t))
|
|
return t * actualValue + (1.0 - t) * self.initialValue
|
|
}
|
|
}
|
|
|
|
private final class SemanticStatusNodeProgressContext: SemanticStatusNodeStateContext {
|
|
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
|
|
let transitionFraction: CGFloat
|
|
let value: CGFloat?
|
|
let displayCancel: Bool
|
|
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
|
let timestamp: Double
|
|
|
|
init(transitionFraction: CGFloat, value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?, timestamp: Double) {
|
|
self.transitionFraction = transitionFraction
|
|
self.value = value
|
|
self.displayCancel = displayCancel
|
|
self.appearance = appearance
|
|
self.timestamp = timestamp
|
|
|
|
super.init()
|
|
}
|
|
|
|
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
|
|
let diameter = size.width
|
|
|
|
let factor = diameter / 50.0
|
|
|
|
context.saveGState()
|
|
|
|
if foregroundColor.alpha.isZero {
|
|
context.setBlendMode(.destinationOut)
|
|
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
} else {
|
|
context.setBlendMode(.normal)
|
|
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
}
|
|
|
|
var progress: CGFloat
|
|
var startAngle: CGFloat
|
|
var endAngle: CGFloat
|
|
if let value = self.value {
|
|
progress = value
|
|
startAngle = -CGFloat.pi / 2.0
|
|
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)
|
|
} else {
|
|
progress = CGFloat(1.0 + self.timestamp.remainder(dividingBy: 2.0))
|
|
|
|
startAngle = -CGFloat.pi / 2.0
|
|
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
|
|
if let appearance = self.appearance {
|
|
lineWidth = appearance.lineWidth
|
|
} else {
|
|
lineWidth = max(1.6, 2.25 * factor)
|
|
}
|
|
|
|
let pathDiameter: CGFloat
|
|
if let appearance = self.appearance {
|
|
pathDiameter = diameter - lineWidth - appearance.inset * 2.0
|
|
} else {
|
|
pathDiameter = diameter - lineWidth - 2.5 * 2.0
|
|
}
|
|
|
|
var angle = self.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0)
|
|
angle *= 4.0
|
|
|
|
context.translateBy(x: diameter / 2.0, y: diameter / 2.0)
|
|
context.rotate(by: CGFloat(angle.truncatingRemainder(dividingBy: Double.pi * 2.0)))
|
|
context.translateBy(x: -diameter / 2.0, y: -diameter / 2.0)
|
|
|
|
let path = UIBezierPath(arcCenter: CGPoint(x: diameter / 2.0, y: diameter / 2.0), radius: pathDiameter / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: true)
|
|
path.lineWidth = lineWidth
|
|
path.lineCapStyle = .round
|
|
path.stroke()
|
|
|
|
context.restoreGState()
|
|
|
|
if self.displayCancel {
|
|
if foregroundColor.alpha.isZero {
|
|
context.setBlendMode(.destinationOut)
|
|
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
} else {
|
|
context.setBlendMode(.normal)
|
|
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
}
|
|
|
|
context.saveGState()
|
|
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
|
context.scaleBy(x: max(0.01, self.transitionFraction), y: max(0.01, self.transitionFraction))
|
|
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
|
|
|
context.setLineWidth(max(1.3, 2.0 * factor))
|
|
context.setLineCap(.round)
|
|
|
|
let crossSize: CGFloat = 14.0 * factor
|
|
context.move(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
|
|
context.addLine(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
|
|
context.strokePath()
|
|
context.move(to: CGPoint(x: diameter / 2.0 + crossSize / 2.0, y: diameter / 2.0 - crossSize / 2.0))
|
|
context.addLine(to: CGPoint(x: diameter / 2.0 - crossSize / 2.0, y: diameter / 2.0 + crossSize / 2.0))
|
|
context.strokePath()
|
|
|
|
context.restoreGState()
|
|
}
|
|
}
|
|
}
|
|
|
|
var value: CGFloat?
|
|
let displayCancel: Bool
|
|
let appearance: SemanticStatusNodeState.ProgressAppearance?
|
|
var transition: SemanticStatusNodeProgressTransition?
|
|
|
|
var isAnimating: Bool {
|
|
return true
|
|
}
|
|
|
|
init(value: CGFloat?, displayCancel: Bool, appearance: SemanticStatusNodeState.ProgressAppearance?) {
|
|
self.value = value
|
|
self.displayCancel = displayCancel
|
|
self.appearance = appearance
|
|
}
|
|
|
|
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
|
let timestamp = CACurrentMediaTime()
|
|
|
|
let resolvedValue: CGFloat?
|
|
if let value = self.value {
|
|
if let transition = self.transition {
|
|
resolvedValue = transition.valueAt(timestamp: timestamp, actualValue: value)
|
|
} else {
|
|
resolvedValue = value
|
|
}
|
|
} else {
|
|
resolvedValue = nil
|
|
}
|
|
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp)
|
|
}
|
|
|
|
func updateValue(value: CGFloat?) {
|
|
if value != self.value {
|
|
let previousValue = value
|
|
self.value = value
|
|
let timestamp = CACurrentMediaTime()
|
|
if let _ = value, let previousValue = previousValue {
|
|
if let transition = self.transition {
|
|
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: transition.valueAt(timestamp: timestamp, actualValue: previousValue))
|
|
} else {
|
|
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: previousValue)
|
|
}
|
|
} else {
|
|
self.transition = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class SemanticStatusNodeCheckContext: SemanticStatusNodeStateContext {
|
|
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
|
|
let transitionFraction: CGFloat
|
|
let value: CGFloat
|
|
let appearance: SemanticStatusNodeState.CheckAppearance?
|
|
|
|
init(transitionFraction: CGFloat, value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) {
|
|
self.transitionFraction = transitionFraction
|
|
self.value = value
|
|
self.appearance = appearance
|
|
|
|
super.init()
|
|
}
|
|
|
|
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor) {
|
|
let diameter = size.width
|
|
|
|
let factor = diameter / 50.0
|
|
|
|
context.saveGState()
|
|
|
|
if foregroundColor.alpha.isZero {
|
|
context.setBlendMode(.destinationOut)
|
|
context.setFillColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
context.setStrokeColor(UIColor(white: 0.0, alpha: self.transitionFraction).cgColor)
|
|
} else {
|
|
context.setBlendMode(.normal)
|
|
context.setFillColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
context.setStrokeColor(foregroundColor.withAlphaComponent(foregroundColor.alpha * self.transitionFraction).cgColor)
|
|
}
|
|
|
|
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
|
|
|
|
let lineWidth: CGFloat
|
|
if let appearance = self.appearance {
|
|
lineWidth = appearance.lineWidth
|
|
} else {
|
|
lineWidth = max(1.6, 2.25 * factor)
|
|
}
|
|
|
|
context.setLineWidth(max(1.7, lineWidth * factor))
|
|
context.setLineCap(.round)
|
|
context.setLineJoin(.round)
|
|
context.setMiterLimit(10.0)
|
|
|
|
let progress = self.value
|
|
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
|
|
|
|
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
|
|
var p1 = CGPoint(x: 7.0 * factor, y: 7.0 * factor)
|
|
var p2 = CGPoint(x: 13.0 * factor, y: -15.0 * factor)
|
|
|
|
if diameter < 36.0 {
|
|
s = CGPoint(x: center.x - 7.0 * factor, y: center.y + 1.0 * factor)
|
|
p1 = CGPoint(x: 4.5 * factor, y: 4.5 * factor)
|
|
p2 = CGPoint(x: 10.0 * factor, y: -11.0 * factor)
|
|
}
|
|
|
|
if !firstSegment.isZero {
|
|
if firstSegment < 1.0 {
|
|
context.move(to: CGPoint(x: s.x + p1.x * firstSegment, y: s.y + p1.y * firstSegment))
|
|
context.addLine(to: s)
|
|
} else {
|
|
let secondSegment = (progress - 0.33) * 1.5
|
|
context.move(to: CGPoint(x: s.x + p1.x + p2.x * secondSegment, y: s.y + p1.y + p2.y * secondSegment))
|
|
context.addLine(to: CGPoint(x: s.x + p1.x, y: s.y + p1.y))
|
|
context.addLine(to: s)
|
|
}
|
|
}
|
|
context.strokePath()
|
|
}
|
|
}
|
|
|
|
var value: CGFloat
|
|
let appearance: SemanticStatusNodeState.CheckAppearance?
|
|
var transition: SemanticStatusNodeProgressTransition?
|
|
|
|
var isAnimating: Bool {
|
|
return true
|
|
}
|
|
|
|
init(value: CGFloat, appearance: SemanticStatusNodeState.CheckAppearance?) {
|
|
self.value = value
|
|
self.appearance = appearance
|
|
|
|
self.animate()
|
|
}
|
|
|
|
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
|
let timestamp = CACurrentMediaTime()
|
|
|
|
let resolvedValue: CGFloat
|
|
if let transition = self.transition {
|
|
resolvedValue = transition.valueAt(timestamp: timestamp, actualValue: value)
|
|
} else {
|
|
resolvedValue = value
|
|
}
|
|
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance)
|
|
}
|
|
|
|
func animate() {
|
|
guard self.value < 1.0 else {
|
|
return
|
|
}
|
|
let timestamp = CACurrentMediaTime()
|
|
self.value = 1.0
|
|
self.transition = SemanticStatusNodeProgressTransition(beginTime: timestamp, initialValue: 0.0)
|
|
}
|
|
}
|
|
|
|
private extension SemanticStatusNodeState {
|
|
func context(current: SemanticStatusNodeStateContext?) -> SemanticStatusNodeStateContext {
|
|
switch self {
|
|
case .none, .download, .play, .pause, .customIcon:
|
|
let icon: SemanticStatusNodeIcon
|
|
switch self {
|
|
case .none:
|
|
icon = .none
|
|
case .download:
|
|
icon = .download
|
|
case .play:
|
|
icon = .play
|
|
case .pause:
|
|
icon = .pause
|
|
case let .customIcon(image):
|
|
icon = .custom(image)
|
|
default:
|
|
preconditionFailure()
|
|
}
|
|
if let current = current as? SemanticStatusNodeIconContext, current.icon == icon {
|
|
return current
|
|
} else {
|
|
return SemanticStatusNodeIconContext(icon: icon)
|
|
}
|
|
case let .check(appearance):
|
|
if let current = current as? SemanticStatusNodeCheckContext {
|
|
return current
|
|
} else {
|
|
return SemanticStatusNodeCheckContext(value: 0.0, appearance: appearance)
|
|
}
|
|
case let .progress(value, cancelEnabled, appearance):
|
|
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
|
|
current.updateValue(value: value)
|
|
return current
|
|
} else {
|
|
return SemanticStatusNodeProgressContext(value: value, displayCancel: cancelEnabled, appearance: appearance)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private final class SemanticStatusNodeTransitionDrawingState {
|
|
let transition: CGFloat
|
|
let drawingState: SemanticStatusNodeStateDrawingState
|
|
|
|
init(transition: CGFloat, drawingState: SemanticStatusNodeStateDrawingState) {
|
|
self.transition = transition
|
|
self.drawingState = drawingState
|
|
}
|
|
}
|
|
|
|
private final class SemanticStatusNodeDrawingState: NSObject {
|
|
let background: UIColor
|
|
let foreground: UIColor
|
|
let hollow: Bool
|
|
let transitionState: SemanticStatusNodeTransitionDrawingState?
|
|
let drawingState: SemanticStatusNodeStateDrawingState
|
|
let backgroundImage: UIImage?
|
|
let overlayForeground: UIColor?
|
|
let cutout: SemanticStatusNode.Cutout?
|
|
|
|
init(background: UIColor, foreground: UIColor, hollow: Bool, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState, backgroundImage: UIImage?, overlayForeground: UIColor?, cutout: SemanticStatusNode.Cutout?) {
|
|
self.background = background
|
|
self.foreground = foreground
|
|
self.hollow = hollow
|
|
self.transitionState = transitionState
|
|
self.drawingState = drawingState
|
|
self.backgroundImage = backgroundImage
|
|
self.overlayForeground = overlayForeground
|
|
self.cutout = cutout
|
|
|
|
super.init()
|
|
}
|
|
}
|
|
|
|
private final class SemanticStatusNodeTransitionContext {
|
|
let startTime: Double
|
|
let duration: Double
|
|
let previousStateContext: SemanticStatusNodeStateContext
|
|
let completion: () -> Void
|
|
|
|
init(startTime: Double, duration: Double, previousStateContext: SemanticStatusNodeStateContext, completion: @escaping () -> Void) {
|
|
self.startTime = startTime
|
|
self.duration = duration
|
|
self.previousStateContext = previousStateContext
|
|
self.completion = completion
|
|
}
|
|
}
|
|
|
|
public final class SemanticStatusNode: ASControlNode {
|
|
final class Cutout {
|
|
|
|
}
|
|
|
|
public var backgroundNodeColor: UIColor {
|
|
didSet {
|
|
if !self.backgroundNodeColor.isEqual(oldValue) {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
}
|
|
|
|
public var foregroundNodeColor: UIColor {
|
|
didSet {
|
|
if !self.foregroundNodeColor.isEqual(oldValue) {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
}
|
|
|
|
public var overlayForegroundNodeColor: UIColor? {
|
|
didSet {
|
|
if !(self.overlayForegroundNodeColor?.isEqual(oldValue) ?? true) {
|
|
self.setNeedsDisplay()
|
|
}
|
|
}
|
|
}
|
|
|
|
private let hollow: Bool
|
|
|
|
private var animator: ConstantDisplayLinkAnimator?
|
|
|
|
private var hasState: Bool = false
|
|
public private(set) var state: SemanticStatusNodeState
|
|
private var transtionContext: SemanticStatusNodeTransitionContext?
|
|
private var stateContext: SemanticStatusNodeStateContext
|
|
|
|
private var disposable: Disposable?
|
|
private var backgroundNodeImage: UIImage?
|
|
|
|
public init(backgroundNodeColor: UIColor, foregroundNodeColor: UIColor, image: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? = nil, overlayForegroundNodeColor: UIColor? = nil, hollow: Bool = false) {
|
|
self.backgroundNodeColor = backgroundNodeColor
|
|
self.foregroundNodeColor = foregroundNodeColor
|
|
self.overlayForegroundNodeColor = overlayForegroundNodeColor
|
|
self.hollow = hollow
|
|
self.state = .none
|
|
self.stateContext = self.state.context(current: nil)
|
|
|
|
super.init()
|
|
|
|
self.isOpaque = false
|
|
self.displaysAsynchronously = true
|
|
|
|
if let image = image {
|
|
self.disposable = (image
|
|
|> deliverOnMainQueue).start(next: { [weak self] transform in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
let context = transform(TransformImageArguments(corners: ImageCorners(radius: strongSelf.bounds.width / 2.0), imageSize: strongSelf.bounds.size, boundingSize: strongSelf.bounds.size, intrinsicInsets: UIEdgeInsets()))
|
|
self?.backgroundNodeImage = context?.generateImage()
|
|
self?.setNeedsDisplay()
|
|
})
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
private func updateAnimations() {
|
|
var animate = false
|
|
let timestamp = CACurrentMediaTime()
|
|
|
|
if let transtionContext = self.transtionContext {
|
|
if transtionContext.startTime + transtionContext.duration < timestamp {
|
|
self.transtionContext = nil
|
|
transtionContext.completion()
|
|
} else {
|
|
animate = true
|
|
}
|
|
}
|
|
|
|
if self.stateContext.isAnimating {
|
|
animate = true
|
|
}
|
|
|
|
if animate {
|
|
let animator: ConstantDisplayLinkAnimator
|
|
if let current = self.animator {
|
|
animator = current
|
|
} else {
|
|
animator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
|
self?.updateAnimations()
|
|
})
|
|
self.animator = animator
|
|
}
|
|
animator.isPaused = false
|
|
} else {
|
|
self.animator?.isPaused = true
|
|
}
|
|
|
|
self.setNeedsDisplay()
|
|
}
|
|
|
|
public func transitionToState(_ state: SemanticStatusNodeState, animated: Bool = true, synchronous: Bool = false, completion: @escaping () -> Void = {}) {
|
|
var animated = animated
|
|
if !self.hasState {
|
|
self.hasState = true
|
|
animated = false
|
|
}
|
|
if self.state != state {
|
|
self.state = state
|
|
let previousStateContext = self.stateContext
|
|
self.stateContext = self.state.context(current: self.stateContext)
|
|
|
|
if animated && previousStateContext !== self.stateContext {
|
|
self.transtionContext = SemanticStatusNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousStateContext: previousStateContext, completion: completion)
|
|
} else {
|
|
completion()
|
|
}
|
|
|
|
self.updateAnimations()
|
|
self.setNeedsDisplay()
|
|
} else {
|
|
completion()
|
|
}
|
|
}
|
|
|
|
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
|
var transitionState: SemanticStatusNodeTransitionDrawingState?
|
|
var transitionFraction: CGFloat = 1.0
|
|
if let transitionContext = self.transtionContext {
|
|
let timestamp = CACurrentMediaTime()
|
|
var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration)
|
|
t = min(1.0, max(0.0, t))
|
|
transitionFraction = t
|
|
transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext.drawingState(transitionFraction: 1.0 - t))
|
|
}
|
|
|
|
return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, foreground: self.foregroundNodeColor, hollow: self.hollow, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction), backgroundImage: self.backgroundNodeImage, overlayForeground: self.overlayForegroundNodeColor, cutout: nil)
|
|
}
|
|
|
|
@objc override public 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? SemanticStatusNodeDrawingState else {
|
|
return
|
|
}
|
|
|
|
var foregroundColor = parameters.foreground
|
|
if let backgroundImage = parameters.backgroundImage?.cgImage {
|
|
context.saveGState()
|
|
context.translateBy(x: 0.0, y: bounds.height)
|
|
context.scaleBy(x: 1.0, y: -1.0)
|
|
context.draw(backgroundImage, in: bounds)
|
|
context.restoreGState()
|
|
|
|
if let overlayForegroundColor = parameters.overlayForeground {
|
|
foregroundColor = overlayForegroundColor
|
|
} else {
|
|
foregroundColor = .white
|
|
}
|
|
} else {
|
|
context.setFillColor(parameters.background.cgColor)
|
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: bounds.size))
|
|
}
|
|
if let transitionState = parameters.transitionState {
|
|
transitionState.drawingState.draw(context: context, size: bounds.size, foregroundColor: foregroundColor)
|
|
}
|
|
parameters.drawingState.draw(context: context, size: bounds.size, foregroundColor: foregroundColor)
|
|
|
|
if parameters.hollow {
|
|
context.setBlendMode(.clear)
|
|
context.fillEllipse(in: bounds.insetBy(dx: 8.0, dy: 8.0))
|
|
}
|
|
}
|
|
}
|