Voice Chat UI improvements

This commit is contained in:
Ilya Laktyushin 2020-11-30 03:38:41 +04:00
parent 7f98f8918c
commit 6b75d36548
8 changed files with 470 additions and 82 deletions

View File

@ -442,6 +442,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
private var isHighlighted: Bool = false
private var skipFadeout: Bool = false
private var onlineIsVoiceChat: Bool = false
override var canBeSelected: Bool {
if self.selectableControlNode != nil || self.item?.editing == true {
return false
@ -695,7 +697,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
transition.updateAlpha(layer: self.highlightedBackgroundNode.layer, alpha: highlightProgress)
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 {
if self.highlightedBackgroundNode.supernode != nil {
@ -711,11 +713,11 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
if let item = self.item {
let onlineIcon: UIImage?
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 {
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.cachedChatListText = chatListText
strongSelf.cachedChatListSearchResult = chatListSearchResult
strongSelf.onlineIsVoiceChat = onlineIsVoiceChat
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))
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)
let onlineIcon: UIImage?
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 {
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned)
onlineIcon = PresentationResourcesChatList.recentStatusOnlineIcon(item.presentationData.theme, state: .pinned, voiceChat: onlineIsVoiceChat)
} 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 _ = dateApply()

View File

@ -209,7 +209,7 @@ public final class HorizontalPeerItemNode: ListViewItemNode {
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)
let _ = badgeApply()

View File

@ -3,8 +3,106 @@ import UIKit
import AsyncDisplayKit
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 {
private let iconNode: ASImageNode
private var animationNode: VoiceChatIndicatorNode?
private var color: UIColor = UIColor(rgb: 0xffffff) {
didSet {
self.animationNode?.color = self.color
}
}
override public init() {
self.iconNode = ASImageNode()
@ -20,16 +118,31 @@ public final class PeerOnlineMarkerNode: ASDisplayNode {
self.addSubnode(self.iconNode)
}
public func setImage(_ image: UIImage?) {
public func setImage(_ image: UIImage?, color: UIColor?) {
self.iconNode.image = image
if let color = color {
self.color = color
}
}
public func asyncLayout() -> (Bool, Bool) -> (CGSize, (Bool) -> Void) {
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 {
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 {
let initialScale: CGFloat = strongSelf.iconNode.isHidden ? 0.0 : CGFloat((strongSelf.iconNode.value(forKeyPath: "layer.presentationLayer.transform.scale.x") as? NSNumber)?.floatValue ?? 1.0)
let targetScale: CGFloat = online ? 1.0 : 0.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
if let strongSelf = self, finished {
strongSelf.iconNode.isHidden = !online
if let animationNode = strongSelf.animationNode, !isVoiceChat {
animationNode.removeFromSupernode()
}
}
})
} else {
strongSelf.iconNode.isHidden = !online
if let animationNode = strongSelf.animationNode, !isVoiceChat {
animationNode.removeFromSupernode()
}
}
}
})

View File

@ -166,7 +166,7 @@ public final class SelectablePeerNode: ASDisplayNode {
let (onlineSize, onlineApply) = onlineLayout(online, 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.setNeedsLayout()

View File

@ -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 {
var muted = true

View File

@ -40,19 +40,26 @@ private enum VoiceChatActionButtonBackgroundNodeType {
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 {
var blueGradient: 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 greenGradient: UIImage?
init(blueGradient: UIImage?) {
self.blueGradient = blueGradient
}
var isAnimating: Bool {
return true
@ -69,15 +76,21 @@ private final class VoiceChatActionButtonBackgroundNodeConnectingState: NSObject
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?) {
self.blueGradient = blueGradient
}
}
private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var blueGradient: UIImage?
var greenGradient: UIImage?
private final class VoiceChatActionButtonBackgroundNodeDisabledContext: VoiceChatActionButtonBackgroundNodeContext {
var isAnimating: Bool {
return false
}
@ -92,6 +105,15 @@ private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject,
func updateAnimations() {
}
func drawingState() -> VoiceChatActionButtonBackgroundNodeState {
return VoiceChatActionButtonBackgroundNodeDisabledState()
}
}
private final class VoiceChatActionButtonBackgroundNodeDisabledState: NSObject, VoiceChatActionButtonBackgroundNodeState {
var blueGradient: UIImage?
var greenGradient: UIImage?
}
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 greenGradient: UIImage?
@ -321,13 +343,15 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
return .blob
}
typealias BlobRange = (min: CGFloat, max: CGFloat)
let blobs: [Blob]
let size: CGSize
var active: Bool
var activeTransitionArguments: (startTime: Double, duration: Double)?
typealias BlobRange = (min: CGFloat, max: CGFloat)
let blobs: [Blob]
init(size: CGSize, active: Bool, blueGradient: UIImage, greenGradient: UIImage) {
self.size = size
self.active = active
self.blueGradient = blueGradient
self.greenGradient = greenGradient
@ -340,8 +364,8 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
self.blobs = [largeBlob, mediumBlob]
}
func update(with state: VoiceChatActionButtonBackgroundNodeBlobState) {
func update(with state: VoiceChatActionButtonBackgroundNodeBlobContext) {
if self.active != state.active {
self.active = state.active
@ -364,14 +388,69 @@ private final class VoiceChatActionButtonBackgroundNodeBlobState: NSObject, Voic
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 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.duration = duration
self.previousState = previousState
@ -384,15 +463,20 @@ private final class VoiceChatActionButtonBackgroundNodeTransition {
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 {
let timestamp: Double
let state: VoiceChatActionButtonBackgroundNodeState
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.state = state
self.simplified = simplified
@ -401,14 +485,14 @@ private class VoiceChatActionButtonBackgroundNodeDrawingState: NSObject {
}
private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
private var state: VoiceChatActionButtonBackgroundNodeState
private var state: VoiceChatActionButtonBackgroundNodeContext
private var hasState = false
private var transition: VoiceChatActionButtonBackgroundNodeTransition?
private var transition: VoiceChatActionButtonBackgroundNodeTransitionContext?
private var simplified = false
var audioLevel: CGFloat = 0.0 {
didSet {
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobState {
if let blobsState = self.state as? VoiceChatActionButtonBackgroundNodeBlobContext {
for blob in blobsState.blobs {
blob.loop = audioLevel.isZero
blob.updateSpeedLevel(to: self.audioLevel)
@ -421,7 +505,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
private var animator: ConstantDisplayLinkAnimator?
override init() {
self.state = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: nil)
self.state = VoiceChatActionButtonBackgroundNodeConnectingContext(blueGradient: nil)
super.init()
@ -429,7 +513,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
self.displaysAsynchronously = true
}
func update(state: VoiceChatActionButtonBackgroundNodeState, simplified: Bool, animated: Bool) {
func update(state: VoiceChatActionButtonBackgroundNodeContext, simplified: Bool, animated: Bool) {
var animated = animated
var hadState = true
if !self.hasState {
@ -442,10 +526,10 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
if state.type != self.state.type || !hadState {
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
} 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)
}
@ -457,7 +541,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
let timestamp = CACurrentMediaTime()
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 {
blob.level = self.presentationAudioLevel
}
@ -496,14 +580,13 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
}
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) {
let context = UIGraphicsGetCurrentContext()!
// let drawStart = CACurrentMediaTime()
if !isRasterizing {
context.setBlendMode(.copy)
context.setFillColor(UIColor.clear.cgColor)
@ -529,8 +612,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
var appearanceProgress: CGFloat = 1.0
var glowScale: CGFloat = 0.75
if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState {
appearanceProgress = transition.progress(time: parameters.timestamp)
if let transition = parameters.transition, transition.previousState == .connecting {
appearanceProgress = transition.transition
}
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
@ -582,26 +665,25 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
if let blobsState = parameters.state as? VoiceChatActionButtonBackgroundNodeBlobState {
for blob in blobsState.blobs {
if let path = blob.currentShape, let uiPath = path.copy() as? UIBezierPath {
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 fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
let uiPath = blob.path
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 fromOrigin = CGAffineTransform(translationX: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
uiPath.apply(toOrigin)
uiPath.apply(CGAffineTransform(scaleX: blob.currentScale * appearanceProgress, y: blob.currentScale * appearanceProgress))
uiPath.apply(fromOrigin)
uiPath.apply(toOrigin)
uiPath.apply(CGAffineTransform(scaleX: blob.scale * appearanceProgress, y: blob.scale * appearanceProgress))
uiPath.apply(fromOrigin)
context.addPath(uiPath.cgPath)
context.clip()
context.addPath(uiPath.cgPath)
context.clip()
context.setAlpha(blob.alpha)
context.setAlpha(blob.alpha)
if parameters.simplified {
context.setFillColor(simpleColor.cgColor)
context.fill(bounds)
} else if let gradient = gradientImage?.cgImage {
context.draw(gradient, in: CGRect(origin: CGPoint(x: gradientCenter.x - gradientSize / 2.0, y: gradientCenter.y - gradientSize / 2.0), size: CGSize(width: gradientSize, height: gradientSize)))
}
if parameters.simplified {
context.setFillColor(simpleColor.cgColor)
context.fill(bounds)
} else if let gradient = gradientImage?.cgImage {
context.draw(gradient, in: CGRect(origin: CGPoint(x: gradientCenter.x - gradientSize / 2.0, y: gradientCenter.y - gradientSize / 2.0), size: CGSize(width: gradientSize, height: gradientSize)))
}
}
}
@ -614,7 +696,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
var drawGradient = false
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))
globalAngle *= 4.0
globalAngle = CGFloat(globalAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
@ -627,7 +709,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
var skip = false
var progress = CGFloat(1.0 + timestamp.remainder(dividingBy: 2.0))
if let transition = parameters.transition {
var transitionProgress = transition.progress(time: parameters.timestamp)
var transitionProgress = transition.transition
if parameters.state is VoiceChatActionButtonBackgroundNodeBlobState {
transitionProgress = min(1.0, transitionProgress / 0.5)
progress = progress + (2.0 - progress) * transitionProgress
@ -635,7 +717,7 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
skip = true
}
} 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 {
skip = true
}
@ -668,8 +750,8 @@ private class VoiceChatActionButtonBackgroundNode: ASDisplayNode {
path.addEllipse(in: buttonRect.insetBy(dx: -lineWidth / 2.0, dy: -lineWidth / 2.0))
context.addPath(path)
context.clip()
if let transition = parameters.transition, transition.previousState is VoiceChatActionButtonBackgroundNodeConnectingState || transition.previousState is VoiceChatActionButtonBackgroundNodeDisabledState, transition.progress(time: parameters.timestamp) > 0.5 {
let progress = (transition.progress(time: parameters.timestamp) - 0.5) / 0.5
if let transition = parameters.transition, transition.previousState == .connecting || transition.previousState == .disabled, transition.transition > 0.5 {
let progress = (transition.transition - 0.5) / 0.5
clearInside = progress
}
@ -798,21 +880,21 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
var iconMuted = true
var iconColor: UIColor = .white
var backgroundState: VoiceChatActionButtonBackgroundNodeState
var backgroundState: VoiceChatActionButtonBackgroundNodeContext
switch state {
case let .active(state):
switch state {
case .on:
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:
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:
iconColor = UIColor(rgb: 0xff3b30)
backgroundState = VoiceChatActionButtonBackgroundNodeDisabledState()
backgroundState = VoiceChatActionButtonBackgroundNodeDisabledContext()
}
case .connecting:
backgroundState = VoiceChatActionButtonBackgroundNodeConnectingState(blueGradient: self.blueGradient)
backgroundState = VoiceChatActionButtonBackgroundNodeConnectingContext(blueGradient: self.blueGradient)
}
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 {
var smoothPoints = [SmoothPoint]()
for index in (0 ..< points.count) {

View File

@ -80,6 +80,10 @@ public enum PresentationResourceKey: Int32 {
case chatListRecentStatusOnlineHighlightedIcon
case chatListRecentStatusOnlinePinnedIcon
case chatListRecentStatusOnlinePanelIcon
case chatListRecentStatusVoiceChatIcon
case chatListRecentStatusVoiceChatHighlightedIcon
case chatListRecentStatusVoiceChatPinnedIcon
case chatListRecentStatusVoiceChatPanelIcon
case chatTitleLockIcon
case chatTitleMuteIcon

View File

@ -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
switch state {
case .regular:
key = PresentationResourceKey.chatListRecentStatusOnlineIcon
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatIcon : PresentationResourceKey.chatListRecentStatusOnlineIcon
case .highlighted:
key = PresentationResourceKey.chatListRecentStatusOnlineHighlightedIcon
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatHighlightedIcon : PresentationResourceKey.chatListRecentStatusOnlineHighlightedIcon
case .pinned:
key = PresentationResourceKey.chatListRecentStatusOnlinePinnedIcon
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatPinnedIcon : PresentationResourceKey.chatListRecentStatusOnlinePinnedIcon
case .panel:
key = PresentationResourceKey.chatListRecentStatusOnlinePanelIcon
key = voiceChat ? PresentationResourceKey.chatListRecentStatusVoiceChatPanelIcon : PresentationResourceKey.chatListRecentStatusOnlinePanelIcon
}
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)
context.clear(bounds)
switch state {