From 01d517e0fa407a610e2b400e47561d37172d23f8 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 11 Jun 2021 14:08:21 +0300 Subject: [PATCH 1/2] Video Chat Improvements --- .../Sources/VoiceChatMainStageNode.swift | 115 ++++++---- .../Sources/VoiceChatPinNode.swift | 201 ++++++++++++++++++ 2 files changed, 278 insertions(+), 38 deletions(-) create mode 100644 submodules/TelegramCallsUI/Sources/VoiceChatPinNode.swift diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift index 4334b3449b..d77126829d 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -26,6 +26,77 @@ private let fadeColor = UIColor(rgb: 0x000000, alpha: 0.5) private let fadeHeight: CGFloat = 50.0 private let destructiveColor: UIColor = UIColor(rgb: 0xff3b30) +private class VoiceChatPinButtonNode: HighlightTrackingButtonNode { + private let pinButtonIconNode: VoiceChatPinNode + private let pinButtonClippingnode: ASDisplayNode + private let pinButtonTitleNode: ImmediateTextNode + + init(presentationData: PresentationData) { + self.pinButtonIconNode = VoiceChatPinNode() + self.pinButtonClippingnode = ASDisplayNode() + self.pinButtonClippingnode.clipsToBounds = true + + self.pinButtonTitleNode = ImmediateTextNode() + self.pinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_Unpin, font: Font.regular(17.0), textColor: .white) + self.pinButtonTitleNode.alpha = 0.0 + + super.init() + + self.addSubnode(self.pinButtonClippingnode) + self.addSubnode(self.pinButtonIconNode) + self.pinButtonClippingnode.addSubnode(self.pinButtonTitleNode) + + self.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.pinButtonClippingnode.layer.removeAnimation(forKey: "opacity") + strongSelf.pinButtonIconNode.layer.removeAnimation(forKey: "opacity") + strongSelf.pinButtonClippingnode.alpha = 0.4 + strongSelf.pinButtonIconNode.alpha = 0.4 + } else { + strongSelf.pinButtonClippingnode.alpha = 1.0 + strongSelf.pinButtonIconNode.alpha = 1.0 + strongSelf.pinButtonClippingnode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + strongSelf.pinButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) + } + } + } + } + + private var isPinned = false + func update(pinned: Bool, animated: Bool) { + let wasPinned = self.isPinned + self.pinButtonIconNode.update(state: .init(pinned: pinned, color: .white), animated: true) + self.isPinned = pinned + + if animated { + if wasPinned { + self.pinButtonTitleNode.alpha = 0.0 + self.pinButtonTitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + self.pinButtonTitleNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.pinButtonTitleNode.frame.width, y: 0.0), duration: 0.2, additive: true) + } else { + self.pinButtonTitleNode.alpha = 1.0 + self.pinButtonTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.pinButtonTitleNode.layer.animatePosition(from: CGPoint(x: self.pinButtonTitleNode.frame.width, y: 0.0), to: CGPoint(), duration: 0.2, additive: true) + } + } else { + self.pinButtonTitleNode.alpha = isPinned ? 1.0 : 0.0 + } + } + + func update(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { + let unpinSize = self.pinButtonTitleNode.updateLayout(size) + let pinIconSize = CGSize(width: 48.0, height: 48.0) + let totalSize = CGSize(width: unpinSize.width + pinIconSize.width, height: 44.0) + + transition.updateFrame(node: self.pinButtonIconNode, frame: CGRect(origin: CGPoint(x: totalSize.width - pinIconSize.width, y: 0.0), size: pinIconSize)) + transition.updateFrame(node: self.pinButtonTitleNode, frame: CGRect(origin: CGPoint(x: 4.0, y: 12.0), size: unpinSize)) + transition.updateFrame(node: self.pinButtonClippingnode, frame: CGRect(x: 0.0, y: 0.0, width: totalSize.width - pinIconSize.width * 0.6667, height: 44.0)) + + return totalSize + } +} + final class VoiceChatMainStageNode: ASDisplayNode { private let context: AccountContext private let call: PresentationGroupCall @@ -44,9 +115,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { private let headerNode: ASDisplayNode private let backButtonNode: HighlightableButtonNode private let backButtonArrowNode: ASImageNode - private let pinButtonNode: HighlightTrackingButtonNode - private let pinButtonIconNode: ASImageNode - private let pinButtonTitleNode: ImmediateTextNode + private let pinButtonNode: VoiceChatPinButtonNode private let audioLevelNode: VoiceChatBlobNode private let audioLevelDisposable = MetaDisposable() private let speakingPeerDisposable = MetaDisposable() @@ -136,14 +205,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.pinButtonIconNode = ASImageNode() - self.pinButtonIconNode.displayWithoutProcessing = true - self.pinButtonIconNode.displaysAsynchronously = false - self.pinButtonIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) - self.pinButtonTitleNode = ImmediateTextNode() - self.pinButtonTitleNode.isHidden = true - self.pinButtonTitleNode.attributedText = NSAttributedString(string: presentationData.strings.VoiceChat_Unpin, font: Font.regular(17.0), textColor: .white) - self.pinButtonNode = HighlightableButtonNode() + self.pinButtonNode = VoiceChatPinButtonNode(presentationData: presentationData) self.backdropAvatarNode = ImageNode() self.backdropAvatarNode.contentMode = .scaleAspectFill @@ -212,8 +274,6 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.addSubnode(self.headerNode) self.headerNode.addSubnode(self.backButtonNode) self.headerNode.addSubnode(self.backButtonArrowNode) - self.headerNode.addSubnode(self.pinButtonIconNode) - self.headerNode.addSubnode(self.pinButtonTitleNode) self.headerNode.addSubnode(self.pinButtonNode) self.addSubnode(self.placeholderIconNode) @@ -257,22 +317,6 @@ final class VoiceChatMainStageNode: ASDisplayNode { } } self.backButtonNode.addTarget(self, action: #selector(self.backPressed), forControlEvents: .touchUpInside) - - self.pinButtonNode.highligthedChanged = { [weak self] highlighted in - if let strongSelf = self { - if highlighted { - strongSelf.pinButtonTitleNode.layer.removeAnimation(forKey: "opacity") - strongSelf.pinButtonIconNode.layer.removeAnimation(forKey: "opacity") - strongSelf.pinButtonTitleNode.alpha = 0.4 - strongSelf.pinButtonIconNode.alpha = 0.4 - } else { - strongSelf.pinButtonTitleNode.alpha = 1.0 - strongSelf.pinButtonIconNode.alpha = 1.0 - strongSelf.pinButtonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - strongSelf.pinButtonIconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) - } - } - } self.pinButtonNode.addTarget(self, action: #selector(self.pinPressed), forControlEvents: .touchUpInside) } @@ -727,8 +771,7 @@ final class VoiceChatMainStageNode: ASDisplayNode { self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, isLandscape: isLandscape, isTablet: isTablet, transition: .immediate) } - self.pinButtonTitleNode.isHidden = !pinned - self.pinButtonIconNode.image = !pinned ? generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) : generateTintedImage(image: UIImage(bundleImageName: "Call/Unpin"), color: .white) + self.pinButtonNode.update(pinned: pinned, animated: true) self.audioLevelNode.startAnimating(immediately: true) @@ -1091,13 +1134,9 @@ final class VoiceChatMainStageNode: ASDisplayNode { } transition.updateFrame(node: self.backButtonNode, frame: CGRect(origin: CGPoint(x: sideInset + 27.0, y: 12.0), size: backSize)) - let unpinSize = self.pinButtonTitleNode.updateLayout(size) - if let image = self.pinButtonIconNode.image { - let offset: CGFloat = sideInset.isZero ? 0.0 : initialBottomInset + 8.0 - transition.updateFrame(node: self.pinButtonIconNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width - offset, y: 0.0), size: image.size)) - transition.updateFrame(node: self.pinButtonTitleNode, frame: CGRect(origin: CGPoint(x: size.width - image.size.width - unpinSize.width + 4.0 - offset, y: 12.0), size: unpinSize)) - transition.updateFrame(node: self.pinButtonNode, frame: CGRect(x: size.width - image.size.width - unpinSize.width - offset, y: 0.0, width: unpinSize.width + image.size.width, height: 44.0)) - } + let offset: CGFloat = sideInset.isZero ? 0.0 : initialBottomInset + 8.0 + let pinButtonSize = self.pinButtonNode.update(size: size, transition: transition) + transition.updateFrame(node: self.pinButtonNode, frame: CGRect(origin: CGPoint(x: size.width - pinButtonSize.width - offset, y: 0.0), size: pinButtonSize)) transition.updateFrame(node: self.headerNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: 64.0))) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatPinNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatPinNode.swift new file mode 100644 index 0000000000..613dd8c150 --- /dev/null +++ b/submodules/TelegramCallsUI/Sources/VoiceChatPinNode.swift @@ -0,0 +1,201 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display + +private let iconImage = generateTintedImage(image: UIImage(bundleImageName: "Call/Pin"), color: .white) + +private final class VoiceChatPinNodeDrawingState: NSObject { + let color: UIColor + let transition: CGFloat + let reverse: Bool + + init(color: UIColor, transition: CGFloat, reverse: Bool) { + self.color = color + self.transition = transition + self.reverse = reverse + + super.init() + } +} + +final class VoiceChatPinNode: ASDisplayNode { + class State: Equatable { + let pinned: Bool + let color: UIColor + + init(pinned: Bool, color: UIColor) { + self.pinned = pinned + self.color = color + } + + static func ==(lhs: State, rhs: State) -> Bool { + if lhs.pinned != rhs.pinned { + return false + } + if lhs.color.argb != rhs.color.argb { + return false + } + return true + } + } + + private class TransitionContext { + let startTime: Double + let duration: Double + let previousState: State + + init(startTime: Double, duration: Double, previousState: State) { + self.startTime = startTime + self.duration = duration + self.previousState = previousState + } + } + + private var animator: ConstantDisplayLinkAnimator? + + private var hasState = false + private var state: State = State(pinned: false, color: .black) + private var transitionContext: TransitionContext? + + override init() { + super.init() + + self.isOpaque = false + } + + func update(state: State, animated: Bool) { + var animated = animated + if !self.hasState { + self.hasState = true + animated = false + } + + if self.state != state { + let previousState = self.state + self.state = state + + if animated { + self.transitionContext = TransitionContext(startTime: CACurrentMediaTime(), duration: 0.18, previousState: previousState) + } + + self.updateAnimations() + self.setNeedsDisplay() + } + } + + private func updateAnimations() { + var animate = false + let timestamp = CACurrentMediaTime() + + if let transitionContext = self.transitionContext { + if transitionContext.startTime + transitionContext.duration < timestamp { + self.transitionContext = nil + } else { + animate = true + } + } + + if animate { + let animator: ConstantDisplayLinkAnimator + if let current = self.animator { + animator = current + } else { + animator = ConstantDisplayLinkAnimator(update: { [weak self] in + self?.updateAnimations() + }) + self.animator = animator + } + animator.isPaused = false + } else { + self.animator?.isPaused = true + } + + self.setNeedsDisplay() + } + + override public func drawParameters(forAsyncLayer layer: _ASDisplayLayer) -> NSObjectProtocol? { + var transitionFraction: CGFloat = self.state.pinned ? 1.0 : 0.0 + var color = self.state.color + + var reverse = false + if let transitionContext = self.transitionContext { + let timestamp = CACurrentMediaTime() + var t = CGFloat((timestamp - transitionContext.startTime) / transitionContext.duration) + t = min(1.0, max(0.0, t)) + + if transitionContext.previousState.pinned != self.state.pinned { + transitionFraction = self.state.pinned ? t : 1.0 - t + + reverse = transitionContext.previousState.pinned + } + + if transitionContext.previousState.color.rgb != color.rgb { + color = transitionContext.previousState.color.interpolateTo(color, fraction: t)! + } + } + + return VoiceChatPinNodeDrawingState(color: color, transition: transitionFraction, reverse: reverse) + } + + @objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) { + let context = UIGraphicsGetCurrentContext()! + + if !isRasterizing { + context.setBlendMode(.copy) + context.setFillColor(UIColor.clear.cgColor) + context.fill(bounds) + } + + guard let parameters = parameters as? VoiceChatPinNodeDrawingState else { + return + } + + context.setFillColor(parameters.color.cgColor) + + let clearLineWidth: CGFloat = 2.0 + let lineWidth: CGFloat = 1.0 + UIScreenPixel + if let iconImage = iconImage?.cgImage { + context.saveGState() + context.translateBy(x: bounds.midX, y: bounds.midY) + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: -bounds.midX, y: -bounds.midY) + context.draw(iconImage, in: CGRect(origin: CGPoint(), size: CGSize(width: 48.0, height: 48.0))) + context.restoreGState() + } + + if parameters.transition > 0.0 { + let startPoint: CGPoint + let endPoint: CGPoint + + let origin = CGPoint(x: 14.0, y: 16.0 - UIScreenPixel) + let length: CGFloat = 17.0 + + if parameters.reverse { + startPoint = CGPoint(x: origin.x + length * (1.0 - parameters.transition), y: origin.y + length * (1.0 - parameters.transition)).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + endPoint = CGPoint(x: origin.x + length, y: origin.y + length).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + } else { + startPoint = origin.offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + endPoint = CGPoint(x: origin.x + length * parameters.transition, y: origin.y + length * parameters.transition).offsetBy(dx: UIScreenPixel, dy: -UIScreenPixel) + } + + + context.setBlendMode(.clear) + context.setLineWidth(clearLineWidth) + + context.move(to: startPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel)) + context.addLine(to: endPoint.offsetBy(dx: 0.0, dy: 1.0 + UIScreenPixel)) + context.strokePath() + + context.setBlendMode(.normal) + context.setStrokeColor(parameters.color.cgColor) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + context.setLineJoin(.round) + + context.move(to: startPoint) + context.addLine(to: endPoint) + context.strokePath() + } + } +} From 2e71f2f0df493936c728f08cd8eb6c6815109e45 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 11 Jun 2021 14:14:31 +0300 Subject: [PATCH 2/2] Video Chat Improvements --- .../TelegramCallsUI/Sources/VoiceChatMainStageNode.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift index d77126829d..1a5a557931 100644 --- a/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift +++ b/submodules/TelegramCallsUI/Sources/VoiceChatMainStageNode.swift @@ -69,18 +69,15 @@ private class VoiceChatPinButtonNode: HighlightTrackingButtonNode { self.pinButtonIconNode.update(state: .init(pinned: pinned, color: .white), animated: true) self.isPinned = pinned + self.pinButtonTitleNode.alpha = self.isPinned ? 1.0 : 0.0 if animated { if wasPinned { - self.pinButtonTitleNode.alpha = 0.0 self.pinButtonTitleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) self.pinButtonTitleNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.pinButtonTitleNode.frame.width, y: 0.0), duration: 0.2, additive: true) } else { - self.pinButtonTitleNode.alpha = 1.0 self.pinButtonTitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.pinButtonTitleNode.layer.animatePosition(from: CGPoint(x: self.pinButtonTitleNode.frame.width, y: 0.0), to: CGPoint(), duration: 0.2, additive: true) } - } else { - self.pinButtonTitleNode.alpha = isPinned ? 1.0 : 0.0 } }