mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
[WIP] View-once audio and video messages
This commit is contained in:
@@ -6,7 +6,6 @@ import SwiftSignalKit
|
||||
import RLottieBinding
|
||||
import GZip
|
||||
import AppBundle
|
||||
import ManagedAnimationNode
|
||||
|
||||
public enum SemanticStatusNodeState: Equatable {
|
||||
public struct ProgressAppearance: Equatable {
|
||||
@@ -33,25 +32,27 @@ public enum SemanticStatusNodeState: Equatable {
|
||||
case pause
|
||||
case check(appearance: CheckAppearance?)
|
||||
case progress(value: CGFloat?, cancelEnabled: Bool, appearance: ProgressAppearance?)
|
||||
case secretTimeout(position: Double, duration: Double, generationTimestamp: Double, appearance: ProgressAppearance?)
|
||||
case customIcon(UIImage)
|
||||
}
|
||||
|
||||
private protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
|
||||
protocol SemanticStatusNodeStateDrawingState: NSObjectProtocol {
|
||||
func draw(context: CGContext, size: CGSize, foregroundColor: UIColor)
|
||||
}
|
||||
|
||||
private protocol SemanticStatusNodeStateContext: AnyObject {
|
||||
protocol SemanticStatusNodeStateContext: AnyObject {
|
||||
var isAnimating: Bool { get }
|
||||
var requestUpdate: () -> Void { get set }
|
||||
|
||||
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState
|
||||
}
|
||||
|
||||
private enum SemanticStatusNodeIcon: Equatable {
|
||||
enum SemanticStatusNodeIcon: Equatable {
|
||||
case none
|
||||
case download
|
||||
case play
|
||||
case pause
|
||||
case secretTimeout
|
||||
case custom(UIImage)
|
||||
}
|
||||
|
||||
@@ -88,535 +89,6 @@ private func svgPath(_ path: StaticString, scale: CGPoint = CGPoint(x: 1.0, y: 1
|
||||
return path
|
||||
}
|
||||
|
||||
private final class SemanticStatusNodeIconContext: SemanticStatusNodeStateContext {
|
||||
final class DrawingState: NSObject, SemanticStatusNodeStateDrawingState {
|
||||
let transitionFraction: CGFloat
|
||||
let icon: SemanticStatusNodeIcon
|
||||
let iconImage: UIImage?
|
||||
let iconOffset: CGFloat
|
||||
|
||||
init(transitionFraction: CGFloat, icon: SemanticStatusNodeIcon, iconImage: UIImage?, iconOffset: CGFloat) {
|
||||
self.transitionFraction = transitionFraction
|
||||
self.icon = icon
|
||||
self.iconImage = iconImage
|
||||
self.iconOffset = iconOffset
|
||||
|
||||
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
|
||||
var offset: CGFloat = 0.0
|
||||
if let iconImage = self.iconImage {
|
||||
size = iconImage.size
|
||||
offset = self.iconOffset
|
||||
} else {
|
||||
offset = 1.5
|
||||
size = CGSize(width: 15.0, height: 18.0)
|
||||
}
|
||||
context.translateBy(x: (diameter - size.width) / 2.0 + offset, 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)
|
||||
}
|
||||
if let iconImage = self.iconImage {
|
||||
context.saveGState()
|
||||
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
context.clip(to: iconRect, mask: iconImage.cgImage!)
|
||||
context.fill(iconRect)
|
||||
context.restoreGState()
|
||||
} else {
|
||||
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 - offset, y: -(diameter - size.height) / 2.0)
|
||||
case .pause:
|
||||
let diameter = size.width
|
||||
let factor = diameter / 50.0
|
||||
|
||||
let size: CGSize
|
||||
let offset: CGFloat
|
||||
if let iconImage = self.iconImage {
|
||||
size = iconImage.size
|
||||
offset = self.iconOffset
|
||||
} else {
|
||||
size = CGSize(width: 15.0, height: 16.0)
|
||||
offset = 0.0
|
||||
}
|
||||
context.translateBy(x: (diameter - size.width) / 2.0 + offset, 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)
|
||||
}
|
||||
if let iconImage = self.iconImage {
|
||||
context.saveGState()
|
||||
let iconRect = CGRect(origin: CGPoint(), size: iconImage.size)
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
context.clip(to: iconRect, mask: iconImage.cgImage!)
|
||||
context.fill(iconRect)
|
||||
context.restoreGState()
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
var icon: SemanticStatusNodeIcon {
|
||||
didSet {
|
||||
self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: self.iconImage != nil)
|
||||
}
|
||||
}
|
||||
|
||||
var animationNode: PlayPauseIconNode?
|
||||
var iconImage: UIImage?
|
||||
var iconOffset: CGFloat = 0.0
|
||||
|
||||
init(icon: SemanticStatusNodeIcon) {
|
||||
self.icon = icon
|
||||
|
||||
if [.play, .pause].contains(icon) {
|
||||
self.animationNode = PlayPauseIconNode()
|
||||
self.animationNode?.imageUpdated = { [weak self] image in
|
||||
if let strongSelf = self {
|
||||
strongSelf.iconImage = image
|
||||
if var position = strongSelf.animationNode?.state?.position {
|
||||
position = position * 2.0
|
||||
if position > 1.0 {
|
||||
position = 2.0 - position
|
||||
}
|
||||
strongSelf.iconOffset = (1.0 - position) * 1.5
|
||||
}
|
||||
strongSelf.requestUpdate()
|
||||
}
|
||||
}
|
||||
self.animationNode?.enqueueState(self.icon == .play ? .play : .pause, animated: false)
|
||||
self.iconImage = self.animationNode?.image
|
||||
self.iconOffset = 1.5
|
||||
}
|
||||
}
|
||||
|
||||
var isAnimating: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var requestUpdate: () -> Void = {}
|
||||
|
||||
func drawingState(transitionFraction: CGFloat) -> SemanticStatusNodeStateDrawingState {
|
||||
return DrawingState(transitionFraction: transitionFraction, icon: self.icon, iconImage: self.iconImage, iconOffset: self.iconOffset)
|
||||
}
|
||||
}
|
||||
|
||||
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, Bool) {
|
||||
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, t >= 1.0 - 0.001)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var requestUpdate: () -> Void = {}
|
||||
|
||||
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 {
|
||||
let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value)
|
||||
resolvedValue = v
|
||||
if isCompleted {
|
||||
self.transition = nil
|
||||
}
|
||||
} else {
|
||||
resolvedValue = value
|
||||
}
|
||||
} else {
|
||||
resolvedValue = nil
|
||||
}
|
||||
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, displayCancel: self.displayCancel, appearance: self.appearance, timestamp: timestamp)
|
||||
}
|
||||
|
||||
func maskView() -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateValue(value: CGFloat?) {
|
||||
if value != self.value {
|
||||
let previousValue = self.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).0)
|
||||
} 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
|
||||
}
|
||||
|
||||
var requestUpdate: () -> Void = {}
|
||||
|
||||
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 {
|
||||
let (v, isCompleted) = transition.valueAt(timestamp: timestamp, actualValue: value)
|
||||
resolvedValue = v
|
||||
if isCompleted {
|
||||
self.transition = nil
|
||||
}
|
||||
} else {
|
||||
resolvedValue = value
|
||||
}
|
||||
return DrawingState(transitionFraction: transitionFraction, value: resolvedValue, appearance: self.appearance)
|
||||
}
|
||||
|
||||
func maskView() -> UIView? {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -633,6 +105,8 @@ private extension SemanticStatusNodeState {
|
||||
icon = .pause
|
||||
case let .customIcon(image):
|
||||
icon = .custom(image)
|
||||
case .secretTimeout:
|
||||
icon = .none
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
@@ -654,6 +128,13 @@ private extension SemanticStatusNodeState {
|
||||
} else {
|
||||
return SemanticStatusNodeCheckContext(value: 0.0, appearance: appearance)
|
||||
}
|
||||
case let .secretTimeout(position, duration, generationTimestamp, appearance):
|
||||
if let current = current as? SemanticStatusNodeSecretTimeoutContext {
|
||||
current.updateValue(position: position, duration: duration, generationTimestamp: generationTimestamp)
|
||||
return current
|
||||
} else {
|
||||
return SemanticStatusNodeSecretTimeoutContext(position: position, duration: duration, generationTimestamp: generationTimestamp, appearance: appearance)
|
||||
}
|
||||
case let .progress(value, cancelEnabled, appearance):
|
||||
if let current = current as? SemanticStatusNodeProgressContext, current.displayCancel == cancelEnabled {
|
||||
current.updateValue(value: value)
|
||||
@@ -1048,53 +529,3 @@ public final class SemanticStatusNode: ASControlNode {
|
||||
parameters.appearanceState.drawForeground(context: context, size: bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
private enum PlayPauseIconNodeState: Equatable {
|
||||
case play
|
||||
case pause
|
||||
}
|
||||
|
||||
private final class PlayPauseIconNode: ManagedAnimationNode {
|
||||
private let duration: Double = 0.35
|
||||
private var iconState: PlayPauseIconNodeState = .play
|
||||
|
||||
init() {
|
||||
super.init(size: CGSize(width: 36.0, height: 36.0))
|
||||
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
}
|
||||
|
||||
func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
|
||||
guard self.iconState != state else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousState = self.iconState
|
||||
self.iconState = state
|
||||
|
||||
switch previousState {
|
||||
case .pause:
|
||||
switch state {
|
||||
case .play:
|
||||
if animated {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
|
||||
} else {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
|
||||
}
|
||||
case .pause:
|
||||
break
|
||||
}
|
||||
case .play:
|
||||
switch state {
|
||||
case .pause:
|
||||
if animated {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
|
||||
} else {
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
|
||||
}
|
||||
case .play:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user