mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Voice Chat UI improvements
This commit is contained in:
parent
7f98f8918c
commit
6b75d36548
@ -442,6 +442,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
private var isHighlighted: Bool = false
|
private var isHighlighted: Bool = false
|
||||||
private var skipFadeout: Bool = false
|
private var skipFadeout: Bool = false
|
||||||
|
|
||||||
|
private var onlineIsVoiceChat: Bool = false
|
||||||
|
|
||||||
override var canBeSelected: Bool {
|
override var canBeSelected: Bool {
|
||||||
if self.selectableControlNode != nil || self.item?.editing == true {
|
if self.selectableControlNode != nil || self.item?.editing == true {
|
||||||
return false
|
return false
|
||||||
@ -695,7 +697,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
|
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
|
||||||
|
|
||||||
if let item = self.item {
|
if let item = self.item {
|
||||||
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted))
|
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: self.onlineIsVoiceChat), color: nil)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if self.highlightedBackgroundNode.supernode != nil {
|
if self.highlightedBackgroundNode.supernode != nil {
|
||||||
@ -711,11 +713,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
if let item = self.item {
|
if let item = self.item {
|
||||||
let onlineIcon: UIImage?
|
let onlineIcon: UIImage?
|
||||||
if item.index.pinningIndex != nil {
|
if item.index.pinningIndex != nil {
|
||||||
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned)
|
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: self.onlineIsVoiceChat)
|
||||||
} else {
|
} else {
|
||||||
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular)
|
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: self.onlineIsVoiceChat)
|
||||||
}
|
}
|
||||||
self.onlineNode.setImage(onlineIcon)
|
self.onlineNode.setImage(onlineIcon, color: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1439,6 +1441,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
strongSelf.currentItemHeight = itemHeight
|
strongSelf.currentItemHeight = itemHeight
|
||||||
strongSelf.cachedChatListText = chatListText
|
strongSelf.cachedChatListText = chatListText
|
||||||
strongSelf.cachedChatListSearchResult = chatListSearchResult
|
strongSelf.cachedChatListSearchResult = chatListSearchResult
|
||||||
|
strongSelf.onlineIsVoiceChat = onlineIsVoiceChat
|
||||||
|
|
||||||
strongSelf.contextContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
strongSelf.contextContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
|
||||||
|
|
||||||
@ -1524,18 +1527,23 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
|
let avatarFrame = CGRect(origin: CGPoint(x: leftInset - avatarLeftInset + editingOffset + 10.0 + revealOffset, y: floor((itemHeight - avatarDiameter) / 2.0)), size: CGSize(width: avatarDiameter, height: avatarDiameter))
|
||||||
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
|
transition.updateFrame(node: strongSelf.avatarNode, frame: avatarFrame)
|
||||||
|
|
||||||
let onlineFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - onlineLayout.width - 2.0, y: avatarFrame.maxY - onlineLayout.height - 2.0), size: onlineLayout)
|
let onlineFrame: CGRect
|
||||||
|
if onlineIsVoiceChat {
|
||||||
|
onlineFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - onlineLayout.width + 1.0 - UIScreenPixel, y: avatarFrame.maxY - onlineLayout.height + 1.0 - UIScreenPixel), size: onlineLayout)
|
||||||
|
} else {
|
||||||
|
onlineFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX - onlineLayout.width - 2.0, y: avatarFrame.maxY - onlineLayout.height - 2.0), size: onlineLayout)
|
||||||
|
}
|
||||||
transition.updateFrame(node: strongSelf.onlineNode, frame: onlineFrame)
|
transition.updateFrame(node: strongSelf.onlineNode, frame: onlineFrame)
|
||||||
|
|
||||||
let onlineIcon: UIImage?
|
let onlineIcon: UIImage?
|
||||||
if strongSelf.reallyHighlighted {
|
if strongSelf.reallyHighlighted {
|
||||||
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted)
|
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .highlighted, voiceChat: onlineIsVoiceChat)
|
||||||
} else if item.index.pinningIndex != nil {
|
} else if item.index.pinningIndex != nil {
|
||||||
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned)
|
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat)
|
||||||
} else {
|
} else {
|
||||||
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular)
|
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .regular, voiceChat: onlineIsVoiceChat)
|
||||||
}
|
}
|
||||||
strongSelf.onlineNode.setImage(onlineIcon)
|
strongSelf.onlineNode.setImage(onlineIcon, color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||||
|
|
||||||
let _ = measureApply()
|
let _ = measureApply()
|
||||||
let _ = dateApply()
|
let _ = dateApply()
|
||||||
|
@ -209,7 +209,7 @@ public final class HorizontalPeerItemNode: ListViewItemNode {
|
|||||||
strongSelf.badgeBackgroundNode.isHidden = true
|
strongSelf.badgeBackgroundNode.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.theme, state: .regular))
|
strongSelf.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(item.theme, state: .regular), color: nil)
|
||||||
strongSelf.onlineNode.frame = CGRect(x: itemLayout.size.width - onlineLayout.width - 18.0, y: itemLayout.size.height - onlineLayout.height - 18.0, width: onlineLayout.width, height: onlineLayout.height)
|
strongSelf.onlineNode.frame = CGRect(x: itemLayout.size.width - onlineLayout.width - 18.0, y: itemLayout.size.height - onlineLayout.height - 18.0, width: onlineLayout.width, height: onlineLayout.height)
|
||||||
|
|
||||||
let _ = badgeApply()
|
let _ = badgeApply()
|
||||||
|
@ -3,8 +3,106 @@ import UIKit
|
|||||||
import AsyncDisplayKit
|
import AsyncDisplayKit
|
||||||
import Display
|
import Display
|
||||||
|
|
||||||
|
private final class VoiceChatIndicatorNode: ASDisplayNode {
|
||||||
|
private let leftLine: ASDisplayNode
|
||||||
|
private let centerLine: ASDisplayNode
|
||||||
|
private let rightLine: ASDisplayNode
|
||||||
|
|
||||||
|
private var isCurrentlyInHierarchy = false
|
||||||
|
private var shouldBeAnimating = false
|
||||||
|
|
||||||
|
var color: UIColor = UIColor(rgb: 0xffffff) {
|
||||||
|
didSet {
|
||||||
|
self.leftLine.backgroundColor = self.color
|
||||||
|
self.centerLine.backgroundColor = self.color
|
||||||
|
self.rightLine.backgroundColor = self.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.leftLine = ASDisplayNode()
|
||||||
|
self.leftLine.isLayerBacked = true
|
||||||
|
self.leftLine.cornerRadius = 1.0
|
||||||
|
self.leftLine.frame = CGRect(x: 6.0, y: 6.0, width: 2.0, height: 10.0)
|
||||||
|
|
||||||
|
self.centerLine = ASDisplayNode()
|
||||||
|
self.centerLine.isLayerBacked = true
|
||||||
|
self.centerLine.cornerRadius = 1.0
|
||||||
|
self.centerLine.frame = CGRect(x: 10.0, y: 5.0, width: 2.0, height: 12.0)
|
||||||
|
|
||||||
|
self.rightLine = ASDisplayNode()
|
||||||
|
self.rightLine.isLayerBacked = true
|
||||||
|
self.rightLine.cornerRadius = 1.0
|
||||||
|
self.rightLine.frame = CGRect(x: 14.0, y: 6.0, width: 2.0, height: 10.0)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.isLayerBacked = true
|
||||||
|
|
||||||
|
self.addSubnode(self.leftLine)
|
||||||
|
self.addSubnode(self.centerLine)
|
||||||
|
self.addSubnode(self.rightLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didEnterHierarchy() {
|
||||||
|
super.didEnterHierarchy()
|
||||||
|
|
||||||
|
self.isCurrentlyInHierarchy = true
|
||||||
|
self.updateAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func didExitHierarchy() {
|
||||||
|
super.didExitHierarchy()
|
||||||
|
|
||||||
|
self.isCurrentlyInHierarchy = false
|
||||||
|
self.updateAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateAnimation() {
|
||||||
|
let shouldBeAnimating = self.isCurrentlyInHierarchy
|
||||||
|
if shouldBeAnimating != self.shouldBeAnimating {
|
||||||
|
self.shouldBeAnimating = shouldBeAnimating
|
||||||
|
if shouldBeAnimating {
|
||||||
|
let timingFunctions: [CAMediaTimingFunction] = (0 ..< 5).map { _ in CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut) }
|
||||||
|
|
||||||
|
let leftAnimation = CAKeyframeAnimation(keyPath: "bounds.size.height")
|
||||||
|
leftAnimation.timingFunctions = timingFunctions
|
||||||
|
leftAnimation.values = [NSNumber(value: 10.0), NSNumber(value: 4.0), NSNumber(value: 8.0), NSNumber(value: 4.0), NSNumber(value: 10.0)]
|
||||||
|
leftAnimation.repeatCount = Float.infinity
|
||||||
|
leftAnimation.duration = 2.0
|
||||||
|
self.leftLine.layer.add(leftAnimation, forKey: "animation")
|
||||||
|
|
||||||
|
let centerAnimation = CAKeyframeAnimation(keyPath: "bounds.size.height")
|
||||||
|
centerAnimation.timingFunctions = timingFunctions
|
||||||
|
centerAnimation.values = [NSNumber(value: 6.0), NSNumber(value: 10.0), NSNumber(value: 4.0), NSNumber(value: 12.0), NSNumber(value: 6.0)]
|
||||||
|
centerAnimation.repeatCount = Float.infinity
|
||||||
|
centerAnimation.duration = 2.0
|
||||||
|
self.centerLine.layer.add(centerAnimation, forKey: "animation")
|
||||||
|
|
||||||
|
let rightAnimation = CAKeyframeAnimation(keyPath: "bounds.size.height")
|
||||||
|
rightAnimation.timingFunctions = timingFunctions
|
||||||
|
rightAnimation.values = [NSNumber(value: 10.0), NSNumber(value: 4.0), NSNumber(value: 8.0), NSNumber(value: 4.0), NSNumber(value: 10.0)]
|
||||||
|
rightAnimation.repeatCount = Float.infinity
|
||||||
|
rightAnimation.duration = 2.0
|
||||||
|
self.rightLine.layer.add(rightAnimation, forKey: "animation")
|
||||||
|
} else {
|
||||||
|
self.leftLine.layer.removeAnimation(forKey: "animation")
|
||||||
|
self.centerLine.layer.removeAnimation(forKey: "animation")
|
||||||
|
self.rightLine.layer.removeAnimation(forKey: "animation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class PeerOnlineMarkerNode: ASDisplayNode {
|
public final class PeerOnlineMarkerNode: ASDisplayNode {
|
||||||
private let iconNode: ASImageNode
|
private let iconNode: ASImageNode
|
||||||
|
private var animationNode: VoiceChatIndicatorNode?
|
||||||
|
|
||||||
|
private var color: UIColor = UIColor(rgb: 0xffffff) {
|
||||||
|
didSet {
|
||||||
|
self.animationNode?.color = self.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override public init() {
|
override public init() {
|
||||||
self.iconNode = ASImageNode()
|
self.iconNode = ASImageNode()
|
||||||
@ -20,15 +118,30 @@ public final class PeerOnlineMarkerNode: ASDisplayNode {
|
|||||||
self.addSubnode(self.iconNode)
|
self.addSubnode(self.iconNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setImage(_ image: UIImage?) {
|
public func setImage(_ image: UIImage?, color: UIColor?) {
|
||||||
self.iconNode.image = image
|
self.iconNode.image = image
|
||||||
|
if let color = color {
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func asyncLayout() -> (Bool, Bool) -> (CGSize, (Bool) -> Void) {
|
public func asyncLayout() -> (Bool, Bool) -> (CGSize, (Bool) -> Void) {
|
||||||
return { [weak self] online, isVoiceChat in
|
return { [weak self] online, isVoiceChat in
|
||||||
return (CGSize(width: 14.0, height: 14.0), { animated in
|
let size: CGFloat = isVoiceChat ? 22.0 : 14.0
|
||||||
|
return (CGSize(width: size, height: size), { animated in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.iconNode.frame = CGRect(x: 0.0, y: 0.0, width: 14.0, height: 14.0)
|
strongSelf.iconNode.frame = CGRect(x: 0.0, y: 0.0, width: size, height: size)
|
||||||
|
|
||||||
|
if isVoiceChat {
|
||||||
|
if let _ = strongSelf.animationNode {
|
||||||
|
} else {
|
||||||
|
let animationNode = VoiceChatIndicatorNode()
|
||||||
|
animationNode.color = strongSelf.color
|
||||||
|
animationNode.frame = strongSelf.iconNode.bounds
|
||||||
|
strongSelf.animationNode = animationNode
|
||||||
|
strongSelf.iconNode.addSubnode(animationNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if animated {
|
if animated {
|
||||||
let initialScale: CGFloat = strongSelf.iconNode.isHidden ? 0.0 : CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0)
|
let initialScale: CGFloat = strongSelf.iconNode.isHidden ? 0.0 : CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0)
|
||||||
@ -37,10 +150,18 @@ public final class PeerOnlineMarkerNode: ASDisplayNode {
|
|||||||
strongSelf.iconNode.layer.animateScale(from: initialScale, to: targetScale, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
|
strongSelf.iconNode.layer.animateScale(from: initialScale, to: targetScale, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in
|
||||||
if let strongSelf = self, finished {
|
if let strongSelf = self, finished {
|
||||||
strongSelf.iconNode.isHidden = !online
|
strongSelf.iconNode.isHidden = !online
|
||||||
|
|
||||||
|
if let animationNode = strongSelf.animationNode, !isVoiceChat {
|
||||||
|
animationNode.removeFromSupernode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
strongSelf.iconNode.isHidden = !online
|
strongSelf.iconNode.isHidden = !online
|
||||||
|
|
||||||
|
if let animationNode = strongSelf.animationNode, !isVoiceChat {
|
||||||
|
animationNode.removeFromSupernode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -166,7 +166,7 @@ public final class SelectablePeerNode: ASDisplayNode {
|
|||||||
let (onlineSize, onlineApply) = onlineLayout(online, false)
|
let (onlineSize, onlineApply) = onlineLayout(online, false)
|
||||||
let _ = onlineApply(false)
|
let _ = onlineApply(false)
|
||||||
|
|
||||||
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(theme, state: .panel))
|
self.onlineNode.setImage(PresentationResourcesChatList.recentStatusOnlineIcon(theme, state: .panel), color: nil)
|
||||||
self.onlineNode.frame = CGRect(origin: CGPoint(), size: onlineSize)
|
self.onlineNode.frame = CGRect(origin: CGPoint(), size: onlineSize)
|
||||||
|
|
||||||
self.setNeedsLayout()
|
self.setNeedsLayout()
|
||||||
|
@ -25,6 +25,178 @@ private class CallStatusBarBackgroundNodeDrawingState: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class Curve {
|
||||||
|
let pointsCount: Int
|
||||||
|
let smoothness: CGFloat
|
||||||
|
|
||||||
|
let minRandomness: CGFloat
|
||||||
|
let maxRandomness: CGFloat
|
||||||
|
|
||||||
|
let minSpeed: CGFloat
|
||||||
|
let maxSpeed: CGFloat
|
||||||
|
|
||||||
|
let size: CGSize
|
||||||
|
var currentOffset: CGFloat = 1.0
|
||||||
|
var minOffset: CGFloat = 0.0
|
||||||
|
var maxOffset: CGFloat = 2.0
|
||||||
|
let scaleSpeed: CGFloat
|
||||||
|
|
||||||
|
private var speedLevel: CGFloat = 0.0
|
||||||
|
private var lastSpeedLevel: CGFloat = 0.0
|
||||||
|
|
||||||
|
private var fromPoints: [CGPoint]?
|
||||||
|
private var toPoints: [CGPoint]?
|
||||||
|
|
||||||
|
private var currentPoints: [CGPoint]? {
|
||||||
|
guard let fromPoints = self.fromPoints, let toPoints = self.toPoints else { return nil }
|
||||||
|
|
||||||
|
return fromPoints.enumerated().map { offset, fromPoint in
|
||||||
|
let toPoint = toPoints[offset]
|
||||||
|
return CGPoint(x: fromPoint.x + (toPoint.x - fromPoint.x) * transition, y: fromPoint.y + (toPoint.y - fromPoint.y) * transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentShape: UIBezierPath?
|
||||||
|
private var transition: CGFloat = 0 {
|
||||||
|
didSet {
|
||||||
|
if let currentPoints = self.currentPoints {
|
||||||
|
self.currentShape = UIBezierPath.smoothCurve(through: currentPoints, length: size.width, smoothness: smoothness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var level: CGFloat = 0.0 {
|
||||||
|
didSet {
|
||||||
|
self.currentOffset = self.minOffset + (self.maxOffset - self.minOffset) * self.level
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var transitionArguments: (startTime: Double, duration: Double)?
|
||||||
|
|
||||||
|
var loop: Bool = true {
|
||||||
|
didSet {
|
||||||
|
if let _ = transitionArguments {
|
||||||
|
} else {
|
||||||
|
self.animateToNewShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
size: CGSize,
|
||||||
|
pointsCount: Int,
|
||||||
|
minRandomness: CGFloat,
|
||||||
|
maxRandomness: CGFloat,
|
||||||
|
minSpeed: CGFloat,
|
||||||
|
maxSpeed: CGFloat,
|
||||||
|
minOffset: CGFloat,
|
||||||
|
maxOffset: CGFloat,
|
||||||
|
scaleSpeed: CGFloat
|
||||||
|
) {
|
||||||
|
self.size = size
|
||||||
|
// self.alpha = alpha
|
||||||
|
self.pointsCount = pointsCount
|
||||||
|
self.minRandomness = minRandomness
|
||||||
|
self.maxRandomness = maxRandomness
|
||||||
|
self.minSpeed = minSpeed
|
||||||
|
self.maxSpeed = maxSpeed
|
||||||
|
self.minOffset = minOffset
|
||||||
|
self.maxOffset = maxOffset
|
||||||
|
self.scaleSpeed = scaleSpeed
|
||||||
|
|
||||||
|
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||||
|
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
||||||
|
|
||||||
|
self.currentOffset = minOffset
|
||||||
|
|
||||||
|
self.animateToNewShape()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSpeedLevel(to newSpeedLevel: CGFloat) {
|
||||||
|
self.speedLevel = max(self.speedLevel, newSpeedLevel)
|
||||||
|
|
||||||
|
if abs(lastSpeedLevel - newSpeedLevel) > 0.3 {
|
||||||
|
animateToNewShape()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animateToNewShape() {
|
||||||
|
if let _ = self.transitionArguments {
|
||||||
|
self.fromPoints = self.currentPoints
|
||||||
|
self.toPoints = nil
|
||||||
|
self.transition = 0.0
|
||||||
|
self.transitionArguments = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.fromPoints == nil {
|
||||||
|
self.fromPoints = generateNextCurve(for: self.size)
|
||||||
|
}
|
||||||
|
if self.toPoints == nil {
|
||||||
|
self.toPoints = generateNextCurve(for: self.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration: Double = 1.0 / Double(minSpeed + (maxSpeed - minSpeed) * speedLevel)
|
||||||
|
self.transitionArguments = (CACurrentMediaTime(), duration)
|
||||||
|
|
||||||
|
self.lastSpeedLevel = self.speedLevel
|
||||||
|
self.speedLevel = 0
|
||||||
|
|
||||||
|
self.updateAnimations()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAnimations() {
|
||||||
|
var animate = false
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
|
if let (startTime, duration) = self.transitionArguments, duration > 0.0 {
|
||||||
|
self.transition = max(0.0, min(1.0, CGFloat((timestamp - startTime) / duration)))
|
||||||
|
if self.transition < 1.0 {
|
||||||
|
animate = true
|
||||||
|
} else {
|
||||||
|
if self.loop {
|
||||||
|
self.animateToNewShape()
|
||||||
|
} else {
|
||||||
|
self.fromPoints = self.currentPoints
|
||||||
|
self.toPoints = nil
|
||||||
|
self.transition = 0.0
|
||||||
|
self.transitionArguments = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateNextCurve(for size: CGSize) -> [CGPoint] {
|
||||||
|
let randomness = minRandomness + (maxRandomness - minRandomness) * speedLevel
|
||||||
|
return blob(pointsCount: pointsCount, randomness: randomness).map {
|
||||||
|
return CGPoint(x: size.width / 2.0 + $0.x * CGFloat(size.width), y: size.height / 2.0 + $0.y * CGFloat(size.height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func blob(pointsCount: Int, randomness: CGFloat) -> [CGPoint] {
|
||||||
|
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||||
|
|
||||||
|
let rgen = { () -> CGFloat in
|
||||||
|
let accuracy: UInt32 = 1000
|
||||||
|
let random = arc4random_uniform(accuracy)
|
||||||
|
return CGFloat(random) / CGFloat(accuracy)
|
||||||
|
}
|
||||||
|
let rangeStart: CGFloat = 1.0 / (1.0 + randomness / 10.0)
|
||||||
|
|
||||||
|
let startAngle = angle * CGFloat(arc4random_uniform(100)) / CGFloat(100)
|
||||||
|
|
||||||
|
let points = (0 ..< pointsCount).map { i -> CGPoint in
|
||||||
|
let randPointOffset = (rangeStart + CGFloat(rgen()) * (1 - rangeStart)) / 2
|
||||||
|
let angleRandomness: CGFloat = angle * 0.1
|
||||||
|
let randAngle = angle + angle * ((angleRandomness * CGFloat(arc4random_uniform(100)) / CGFloat(100)) - angleRandomness * 0.5)
|
||||||
|
let pointX = sin(startAngle + CGFloat(i) * randAngle)
|
||||||
|
let pointY = cos(startAngle + CGFloat(i) * randAngle)
|
||||||
|
return CGPoint(x: pointX * randPointOffset, y: pointY * randPointOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private class CallStatusBarBackgroundNode: ASDisplayNode {
|
private class CallStatusBarBackgroundNode: ASDisplayNode {
|
||||||
var muted = true
|
var muted = true
|
||||||
|
|
||||||
|
@ -40,19 +40,26 @@ private enum VoiceChatActionButtonBackgroundNodeType {
|
|||||||
case blob
|
case blob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private protocol VoiceChatActionButtonBackgroundNodeContext {
|
||||||
|
var type: VoiceChatActionButtonBackgroundNodeType { get }
|
||||||
|
var frameInterval: Int { get }
|
||||||
|
var isAnimating: Bool { get }
|
||||||
|
|
||||||
|
func updateAnimations()
|
||||||
|
func drawingState() -> VoiceChatActionButtonBackgroundNodeState
|
||||||
|
}
|
||||||
|
|
||||||
private protocol VoiceChatActionButtonBackgroundNodeState: NSObjectProtocol {
|
private protocol VoiceChatActionButtonBackgroundNodeState: NSObjectProtocol {
|
||||||
var blueGradient: UIImage? { get set }
|
var blueGradient: UIImage? { get set }
|
||||||
var greenGradient: UIImage? { get set }
|
var greenGradient: UIImage? { get set }
|
||||||
|
|
||||||
var frameInterval: Int { get }
|
|
||||||
var isAnimating: Bool { get }
|
|
||||||
var type: VoiceChatActionButtonBackgroundNodeType { get }
|
|
||||||
func updateAnimations()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject, VoiceChatActionButtonBackgroundNodeState {
|
private final class VoiceChatActionButtonBackgroundNodeConnectingContext: VoiceChatActionButtonBackgroundNodeContext {
|
||||||
var blueGradient: UIImage?
|
var blueGradient: UIImage?
|
||||||
var greenGradient: UIImage?
|
|
||||||
|
init(blueGradient: UIImage?) {
|
||||||
|
self.blueGradient = blueGradient
|
||||||
|
}
|
||||||
|
|
||||||
var isAnimating: Bool {
|
var isAnimating: Bool {
|
||||||
return true
|
return true
|
||||||
@ -69,15 +76,21 @@ private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject
|
|||||||
func updateAnimations() {
|
func updateAnimations() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func drawingState() -> VoiceChatActionButtonBackgroundNodeState {
|
||||||
|
return VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: self.blueGradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject, VoiceChatActionButtonBackgroundNodeState {
|
||||||
|
var blueGradient: UIImage?
|
||||||
|
var greenGradient: UIImage?
|
||||||
|
|
||||||
init(blueGradient: UIImage?) {
|
init(blueGradient: UIImage?) {
|
||||||
self.blueGradient = blueGradient
|
self.blueGradient = blueGradient
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState {
|
private final class VoiceChatActionButtonBackgroundNodeDisabledContext: VoiceChatActionButtonBackgroundNodeContext {
|
||||||
var blueGradient: UIImage?
|
|
||||||
var greenGradient: UIImage?
|
|
||||||
|
|
||||||
var isAnimating: Bool {
|
var isAnimating: Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -92,6 +105,15 @@ private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject,
|
|||||||
|
|
||||||
func updateAnimations() {
|
func updateAnimations() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func drawingState() -> VoiceChatActionButtonBackgroundNodeState {
|
||||||
|
return VoiceChatActionButtonBackgroundNodeDisabledState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState {
|
||||||
|
var blueGradient: UIImage?
|
||||||
|
var greenGradient: UIImage?
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class Blob {
|
private final class Blob {
|
||||||
@ -305,7 +327,7 @@ private final class Blob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, VoiceChatActionButtonBackgroundNodeState {
|
private final class VoiceChatActionButtonBackgroundNodeBlobContext: VoiceChatActionButtonBackgroundNodeContext {
|
||||||
var blueGradient: UIImage?
|
var blueGradient: UIImage?
|
||||||
var greenGradient: UIImage?
|
var greenGradient: UIImage?
|
||||||
|
|
||||||
@ -321,13 +343,15 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
|
|||||||
return .blob
|
return .blob
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias BlobRange = (min: CGFloat, max: CGFloat)
|
let size: CGSize
|
||||||
let blobs: [Blob]
|
|
||||||
|
|
||||||
var active: Bool
|
var active: Bool
|
||||||
var activeTransitionArguments: (startTime: Double, duration: Double)?
|
var activeTransitionArguments: (startTime: Double, duration: Double)?
|
||||||
|
|
||||||
|
typealias BlobRange = (min: CGFloat, max: CGFloat)
|
||||||
|
let blobs: [Blob]
|
||||||
|
|
||||||
init(size: CGSize, active: Bool, blueGradient: UIImage, greenGradient: UIImage) {
|
init(size: CGSize, active: Bool, blueGradient: UIImage, greenGradient: UIImage) {
|
||||||
|
self.size = size
|
||||||
self.active = active
|
self.active = active
|
||||||
self.blueGradient = blueGradient
|
self.blueGradient = blueGradient
|
||||||
self.greenGradient = greenGradient
|
self.greenGradient = greenGradient
|
||||||
@ -341,7 +365,7 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
|
|||||||
self.blobs = [largeBlob, mediumBlob]
|
self.blobs = [largeBlob, mediumBlob]
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(with state: VoiceChatActionButtonBackgroundNodeBlobState) {
|
func update(with state: VoiceChatActionButtonBackgroundNodeBlobContext) {
|
||||||
if self.active != state.active {
|
if self.active != state.active {
|
||||||
self.active = state.active
|
self.active = state.active
|
||||||
|
|
||||||
@ -364,14 +388,69 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
|
|||||||
blob.updateAnimations()
|
blob.updateAnimations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func drawingState() -> VoiceChatActionButtonBackgroundNodeState {
|
||||||
|
var blobs: [BlobDrawingState] = []
|
||||||
|
for blob in self.blobs {
|
||||||
|
if let path = blob.currentShape?.copy() as? UIBezierPath {
|
||||||
|
blobs.append(BlobDrawingState(size: blob.size, path: path, scale: blob.currentScale, alpha: blob.alpha))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return VoiceChatActionButtonBackgroundNodeBlobState(size: self.size, active: self.active, activeTransitionArguments: self.activeTransitionArguments, blueGradient: self.blueGradient, greenGradient: self.greenGradient, blobs: blobs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class VoiceChatActionButtonBackgroundNodeTransition {
|
private class BlobDrawingState: NSObject {
|
||||||
|
let size: CGSize
|
||||||
|
let path: UIBezierPath
|
||||||
|
let scale: CGFloat
|
||||||
|
let alpha: CGFloat
|
||||||
|
|
||||||
|
init(size: CGSize, path: UIBezierPath, scale: CGFloat, alpha: CGFloat) {
|
||||||
|
self.size = size
|
||||||
|
self.path = path
|
||||||
|
self.scale = scale
|
||||||
|
self.alpha = alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, VoiceChatActionButtonBackgroundNodeState {
|
||||||
|
var blueGradient: UIImage?
|
||||||
|
var greenGradient: UIImage?
|
||||||
|
|
||||||
|
let active: Bool
|
||||||
|
let activeTransitionArguments: (startTime: Double, duration: Double)?
|
||||||
|
|
||||||
|
let blobs: [BlobDrawingState]
|
||||||
|
|
||||||
|
init(size: CGSize, active: Bool, activeTransitionArguments: (startTime: Double, duration: Double)?, blueGradient: UIImage?, greenGradient: UIImage?, blobs: [BlobDrawingState]) {
|
||||||
|
self.active = active
|
||||||
|
self.activeTransitionArguments = activeTransitionArguments
|
||||||
|
self.blueGradient = blueGradient
|
||||||
|
self.greenGradient = greenGradient
|
||||||
|
self.blobs = blobs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class VoiceChatActionButtonBackgroundNodeTransitionState: NSObject {
|
||||||
|
let startTime: Double
|
||||||
|
let transition: CGFloat
|
||||||
|
let previousState: VoiceChatActionButtonBackgroundNodeType
|
||||||
|
|
||||||
|
init(startTime: Double, transition: CGFloat, previousState: VoiceChatActionButtonBackgroundNodeType) {
|
||||||
|
self.startTime = startTime
|
||||||
|
self.transition = transition
|
||||||
|
self.previousState = previousState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private final class VoiceChatActionButtonBackgroundNodeTransitionContext {
|
||||||
let startTime: Double
|
let startTime: Double
|
||||||
let duration: Double
|
let duration: Double
|
||||||
let previousState: VoiceChatActionButtonBackgroundNodeState?
|
let previousState: VoiceChatActionButtonBackgroundNodeContext
|
||||||
|
|
||||||
init(startTime: Double, duration: Double, previousState: VoiceChatActionButtonBackgroundNodeState?) {
|
init(startTime: Double, duration: Double, previousState: VoiceChatActionButtonBackgroundNodeContext) {
|
||||||
self.startTime = startTime
|
self.startTime = startTime
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.previousState = previousState
|
self.previousState = previousState
|
||||||
@ -384,15 +463,20 @@ private final class VoiceChatActionButtonBackgroundNodeTransition {
|
|||||||
return 0.0
|
return 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func drawingTransitionState(time: Double) -> VoiceChatActionButtonBackgroundNodeTransitionState {
|
||||||
|
let transition = CGFloat(max(0.0, min(1.0, (time - startTime) / duration)))
|
||||||
|
return VoiceChatActionButtonBackgroundNodeTransitionState(startTime: self.startTime, transition: transition, previousState: previousState.type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class VoiceChatActionButtonBackgroundNodeDrawingState: NSObject {
|
private class VoiceChatActionButtonBackgroundNodeDrawingState: NSObject {
|
||||||
let timestamp: Double
|
let timestamp: Double
|
||||||
let state: VoiceChatActionButtonBackgroundNodeState
|
let state: VoiceChatActionButtonBackgroundNodeState
|
||||||
let simplified: Bool
|
let simplified: Bool
|
||||||
let transition: VoiceChatActionButtonBackgroundNodeTransition?
|
let transition: VoiceChatActionButtonBackgroundNodeTransitionState?
|
||||||
|
|
||||||
init(timestamp: Double, state: VoiceChatActionButtonBackgroundNodeState, simplified: Bool, transition: VoiceChatActionButtonBackgroundNodeTransition?) {
|
init(timestamp: Double, state: VoiceChatActionButtonBackgroundNodeState, simplified: Bool, transition: VoiceChatActionButtonBackgroundNodeTransitionState?) {
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.state = state
|
self.state = state
|
||||||
self.simplified = simplified
|
self.simplified = simplified
|
||||||
@ -401,14 +485,14 @@ private class VoiceChatActionButtonBackgroundNodeDrawingState: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
||||||
private var state: VoiceChatActionButtonBackgroundNodeState
|
private var state: VoiceChatActionButtonBackgroundNodeContext
|
||||||
private var hasState = false
|
private var hasState = false
|
||||||
private var transition: VoiceChatActionButtonBackgroundNodeTransition?
|
private var transition: VoiceChatActionButtonBackgroundNodeTransitionContext?
|
||||||
private var simplified = false
|
private var simplified = false
|
||||||
|
|
||||||
var audioLevel: CGFloat = 0.0 {
|
var audioLevel: CGFloat = 0.0 {
|
||||||
didSet {
|
didSet {
|
||||||
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState {
|
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobContext {
|
||||||
for blob in blobsState.blobs {
|
for blob in blobsState.blobs {
|
||||||
blob.loop = audioLevel.isZero
|
blob.loop = audioLevel.isZero
|
||||||
blob.updateSpeedLevel(to: self.audioLevel)
|
blob.updateSpeedLevel(to: self.audioLevel)
|
||||||
@ -421,7 +505,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
private var animator: ConstantDisplayLinkAnimator?
|
private var animator: ConstantDisplayLinkAnimator?
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
self.state = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: nil)
|
self.state = VoiceChatActionButtonBackgroundNodeConnectingContext(blueGradient: nil)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -429,7 +513,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
self.displaysAsynchronously = true
|
self.displaysAsynchronously = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(state: VoiceChatActionButtonBackgroundNodeState, simplified: Bool, animated: Bool) {
|
func update(state: VoiceChatActionButtonBackgroundNodeContext, simplified: Bool, animated: Bool) {
|
||||||
var animated = animated
|
var animated = animated
|
||||||
var hadState = true
|
var hadState = true
|
||||||
if !self.hasState {
|
if !self.hasState {
|
||||||
@ -442,10 +526,10 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
|
|
||||||
if state.type != self.state.type || !hadState {
|
if state.type != self.state.type || !hadState {
|
||||||
if animated {
|
if animated {
|
||||||
self.transition = VoiceChatActionButtonBackgroundNodeTransition(startTime: CACurrentMediaTime(), duration: 0.3, previousState: self.state)
|
self.transition = VoiceChatActionButtonBackgroundNodeTransitionContext(startTime: CACurrentMediaTime(), duration: 0.3, previousState: self.state)
|
||||||
}
|
}
|
||||||
self.state = state
|
self.state = state
|
||||||
} else if let blobState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState, let nextState = state as? VoiceChatActionButtonBackgroundNodeBlobState {
|
} else if let blobState = self.state as? VoiceChatActionButtonBackgroundNodeBlobContext, let nextState = state as? VoiceChatActionButtonBackgroundNodeBlobContext {
|
||||||
blobState.update(with: nextState)
|
blobState.update(with: nextState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,7 +541,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
let timestamp = CACurrentMediaTime()
|
let timestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
self.presentationAudioLevel = self.presentationAudioLevel * 0.9 + max(0.1, self.audioLevel) * 0.1
|
self.presentationAudioLevel = self.presentationAudioLevel * 0.9 + max(0.1, self.audioLevel) * 0.1
|
||||||
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState {
|
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobContext {
|
||||||
for blob in blobsState.blobs {
|
for blob in blobsState.blobs {
|
||||||
blob.level = self.presentationAudioLevel
|
blob.level = self.presentationAudioLevel
|
||||||
}
|
}
|
||||||
@ -496,14 +580,13 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? {
|
||||||
return VoiceChatActionButtonBackgroundNodeDrawingState(timestamp: CACurrentMediaTime(), state: self.state, simplified: self.simplified, transition: self.transition)
|
let timestamp = CACurrentMediaTime()
|
||||||
|
return VoiceChatActionButtonBackgroundNodeDrawingState(timestamp: timestamp, state: self.state.drawingState(), simplified: self.simplified, transition: self.transition?.drawingTransitionState(time: timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {
|
||||||
let context = UIGraphicsGetCurrentContext()!
|
let context = UIGraphicsGetCurrentContext()!
|
||||||
|
|
||||||
// let drawStart = CACurrentMediaTime()
|
|
||||||
|
|
||||||
if !isRasterizing {
|
if !isRasterizing {
|
||||||
context.setBlendMode(.copy)
|
context.setBlendMode(.copy)
|
||||||
context.setFillColor(UIColor.clear.cgColor)
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
@ -529,8 +612,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
|
|
||||||
var appearanceProgress: CGFloat = 1.0
|
var appearanceProgress: CGFloat = 1.0
|
||||||
var glowScale: CGFloat = 0.75
|
var glowScale: CGFloat = 0.75
|
||||||
if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState {
|
if let transition = parameters.transition, transition.previousState == .connecting {
|
||||||
appearanceProgress = transition.progress(time: parameters.timestamp)
|
appearanceProgress = transition.transition
|
||||||
}
|
}
|
||||||
|
|
||||||
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
|
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
|
||||||
@ -582,13 +665,13 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
|
|
||||||
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
|
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
|
||||||
for blob in blobsState.blobs {
|
for blob in blobsState.blobs {
|
||||||
if let path = blob.currentShape, let uiPath = path.copy() as? UIBezierPath {
|
let uiPath = blob.path
|
||||||
let offset = (bounds.size.width - blob.size.width) / 2.0
|
let offset = (bounds.size.width - blob.size.width) / 2.0
|
||||||
let toOrigin = CGAffineTransform(translationX: -bounds.size.width / 2.0 + offset, y: -bounds.size.height / 2.0 + offset)
|
let toOrigin = CGAffineTransform(translationX: -bounds.size.width / 2.0 + offset, y: -bounds.size.height / 2.0 + offset)
|
||||||
let fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
let fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
|
||||||
|
|
||||||
uiPath.apply(toOrigin)
|
uiPath.apply(toOrigin)
|
||||||
uiPath.apply(CGAffineTransform(scaleX: blob.currentScale * appearanceProgress, y: blob.currentScale * appearanceProgress))
|
uiPath.apply(CGAffineTransform(scaleX: blob.scale * appearanceProgress, y: blob.scale * appearanceProgress))
|
||||||
uiPath.apply(fromOrigin)
|
uiPath.apply(fromOrigin)
|
||||||
|
|
||||||
context.addPath(uiPath.cgPath)
|
context.addPath(uiPath.cgPath)
|
||||||
@ -604,7 +687,6 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
context.restoreGState()
|
context.restoreGState()
|
||||||
|
|
||||||
context.setFillColor(greyColor.cgColor)
|
context.setFillColor(greyColor.cgColor)
|
||||||
@ -614,7 +696,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
|
|
||||||
var drawGradient = false
|
var drawGradient = false
|
||||||
let lineWidth = 3.0 + UIScreenPixel
|
let lineWidth = 3.0 + UIScreenPixel
|
||||||
if parameters.state is VoiceChatActionButtonBackgroundNodeConnectingState || parameters.transition?.previousState is VoiceChatActionButtonBackgroundNodeConnectingState {
|
if parameters.state is VoiceChatActionButtonBackgroundNodeConnectingState || parameters.transition?.previousState == .connecting {
|
||||||
var globalAngle: CGFloat = CGFloat(parameters.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0))
|
var globalAngle: CGFloat = CGFloat(parameters.timestamp.truncatingRemainder(dividingBy: Double.pi * 2.0))
|
||||||
globalAngle *= 4.0
|
globalAngle *= 4.0
|
||||||
globalAngle = CGFloat(globalAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
|
globalAngle = CGFloat(globalAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
|
||||||
@ -627,7 +709,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
var skip = false
|
var skip = false
|
||||||
var progress = CGFloat(1.0 + timestamp.remainder(dividingBy: 2.0))
|
var progress = CGFloat(1.0 + timestamp.remainder(dividingBy: 2.0))
|
||||||
if let transition = parameters.transition {
|
if let transition = parameters.transition {
|
||||||
var transitionProgress = transition.progress(time: parameters.timestamp)
|
var transitionProgress = transition.transition
|
||||||
if parameters.state is VoiceChatActionButtonBackgroundNodeBlobState {
|
if parameters.state is VoiceChatActionButtonBackgroundNodeBlobState {
|
||||||
transitionProgress = min(1.0, transitionProgress / 0.5)
|
transitionProgress = min(1.0, transitionProgress / 0.5)
|
||||||
progress = progress + (2.0 - progress) * transitionProgress
|
progress = progress + (2.0 - progress) * transitionProgress
|
||||||
@ -635,7 +717,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
skip = true
|
skip = true
|
||||||
}
|
}
|
||||||
} else if parameters.state is VoiceChatActionButtonBackgroundNodeDisabledState {
|
} else if parameters.state is VoiceChatActionButtonBackgroundNodeDisabledState {
|
||||||
progress = progress + (1.0 - progress) * transition.progress(time: parameters.timestamp)
|
progress = progress + (1.0 - progress) * transition.transition
|
||||||
if transitionProgress >= 1.0 {
|
if transitionProgress >= 1.0 {
|
||||||
skip = true
|
skip = true
|
||||||
}
|
}
|
||||||
@ -668,8 +750,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
|
|||||||
path.addEllipse(in: buttonRect.insetBy(dx: -lineWidth / 2.0, dy: -lineWidth / 2.0))
|
path.addEllipse(in: buttonRect.insetBy(dx: -lineWidth / 2.0, dy: -lineWidth / 2.0))
|
||||||
context.addPath(path)
|
context.addPath(path)
|
||||||
context.clip()
|
context.clip()
|
||||||
if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState || transition.previousState is VoiceChatActionButtonBackgroundNodeDisabledState, transition.progress(time: parameters.timestamp) > 0.5 {
|
if let transition = parameters.transition, transition.previousState == .connecting || transition.previousState == .disabled, transition.transition > 0.5 {
|
||||||
let progress = (transition.progress(time: parameters.timestamp) - 0.5) / 0.5
|
let progress = (transition.transition - 0.5) / 0.5
|
||||||
clearInside = progress
|
clearInside = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -798,21 +880,21 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
|
|
||||||
var iconMuted = true
|
var iconMuted = true
|
||||||
var iconColor: UIColor = .white
|
var iconColor: UIColor = .white
|
||||||
var backgroundState: VoiceChatActionButtonBackgroundNodeState
|
var backgroundState: VoiceChatActionButtonBackgroundNodeContext
|
||||||
switch state {
|
switch state {
|
||||||
case let .active(state):
|
case let .active(state):
|
||||||
switch state {
|
switch state {
|
||||||
case .on:
|
case .on:
|
||||||
iconMuted = false
|
iconMuted = false
|
||||||
backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: blobSize, active: true, blueGradient: self.blueGradient, greenGradient: self.greenGradient)
|
backgroundState = VoiceChatActionButtonBackgroundNodeBlobContext(size: blobSize, active: true, blueGradient: self.blueGradient, greenGradient: self.greenGradient)
|
||||||
case .muted:
|
case .muted:
|
||||||
backgroundState = VoiceChatActionButtonBackgroundNodeBlobState(size: blobSize, active: false, blueGradient: self.blueGradient, greenGradient: self.greenGradient)
|
backgroundState = VoiceChatActionButtonBackgroundNodeBlobContext(size: blobSize, active: false, blueGradient: self.blueGradient, greenGradient: self.greenGradient)
|
||||||
case .cantSpeak:
|
case .cantSpeak:
|
||||||
iconColor = UIColor(rgb: 0xff3b30)
|
iconColor = UIColor(rgb: 0xff3b30)
|
||||||
backgroundState = VoiceChatActionButtonBackgroundNodeDisabledState()
|
backgroundState = VoiceChatActionButtonBackgroundNodeDisabledContext()
|
||||||
}
|
}
|
||||||
case .connecting:
|
case .connecting:
|
||||||
backgroundState = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: self.blueGradient)
|
backgroundState = VoiceChatActionButtonBackgroundNodeConnectingContext(blueGradient: self.blueGradient)
|
||||||
}
|
}
|
||||||
self.backgroundNode.update(state: backgroundState, simplified: simplified, animated: true)
|
self.backgroundNode.update(state: backgroundState, simplified: simplified, animated: true)
|
||||||
|
|
||||||
@ -871,7 +953,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension UIBezierPath {
|
extension UIBezierPath {
|
||||||
static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat) -> UIBezierPath {
|
static func smoothCurve(through points: [CGPoint], length: CGFloat, smoothness: CGFloat) -> UIBezierPath {
|
||||||
var smoothPoints = [SmoothPoint]()
|
var smoothPoints = [SmoothPoint]()
|
||||||
for index in (0 ..< points.count) {
|
for index in (0 ..< points.count) {
|
||||||
|
@ -80,6 +80,10 @@ public enum PresentationResourceKey: Int32 {
|
|||||||
case chatListRecentStatusOnlineHighlightedIcon
|
case chatListRecentStatusOnlineHighlightedIcon
|
||||||
case chatListRecentStatusOnlinePinnedIcon
|
case chatListRecentStatusOnlinePinnedIcon
|
||||||
case chatListRecentStatusOnlinePanelIcon
|
case chatListRecentStatusOnlinePanelIcon
|
||||||
|
case chatListRecentStatusVoiceChatIcon
|
||||||
|
case chatListRecentStatusVoiceChatHighlightedIcon
|
||||||
|
case chatListRecentStatusVoiceChatPinnedIcon
|
||||||
|
case chatListRecentStatusVoiceChatPanelIcon
|
||||||
|
|
||||||
case chatTitleLockIcon
|
case chatTitleLockIcon
|
||||||
case chatTitleMuteIcon
|
case chatTitleMuteIcon
|
||||||
|
@ -135,20 +135,21 @@ public struct PresentationResourcesChatList {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func recentStatusOnlineIcon(_ theme: PresentationTheme, state: RecentStatusOnlineIconState) -> UIImage? {
|
public static func recentStatusOnlineIcon(_ theme: PresentationTheme, state: RecentStatusOnlineIconState, voiceChat: Bool = false) -> UIImage? {
|
||||||
let key: PresentationResourceKey
|
let key: PresentationResourceKey
|
||||||
switch state {
|
switch state {
|
||||||
case .regular:
|
case .regular:
|
||||||
key = PresentationResourceKey.chatListRecentStatusOnlineIcon
|
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatIcon : PresentationResourceKey.chatListRecentStatusOnlineIcon
|
||||||
case .highlighted:
|
case .highlighted:
|
||||||
key = PresentationResourceKey.chatListRecentStatusOnlineHighlightedIcon
|
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatHighlightedIcon : PresentationResourceKey.chatListRecentStatusOnlineHighlightedIcon
|
||||||
case .pinned:
|
case .pinned:
|
||||||
key = PresentationResourceKey.chatListRecentStatusOnlinePinnedIcon
|
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatPinnedIcon : PresentationResourceKey.chatListRecentStatusOnlinePinnedIcon
|
||||||
case .panel:
|
case .panel:
|
||||||
key = PresentationResourceKey.chatListRecentStatusOnlinePanelIcon
|
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatPanelIcon : PresentationResourceKey.chatListRecentStatusOnlinePanelIcon
|
||||||
}
|
}
|
||||||
return theme.image(key.rawValue, { theme in
|
return theme.image(key.rawValue, { theme in
|
||||||
return generateImage(CGSize(width: 14.0, height: 14.0), rotatedContext: { size, context in
|
let size: CGFloat = voiceChat ? 22.0 : 14.0
|
||||||
|
return generateImage(CGSize(width: size, height: size), rotatedContext: { size, context in
|
||||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||||
context.clear(bounds)
|
context.clear(bounds)
|
||||||
switch state {
|
switch state {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user