Voice Chat UI improvements

This commit is contained in:
Ilya Laktyushin 2020-12-09 17:02:22 +04:00
parent 7489524aa5
commit 8af1de7109
7 changed files with 148 additions and 65 deletions

View File

@ -1051,14 +1051,15 @@ public extension ContainedViewLayoutTransition {
#if os(iOS) #if os(iOS)
public extension ContainedViewLayoutTransition { public extension ContainedViewLayoutTransition {
func animateView(_ f: @escaping () -> Void) { func animateView(_ f: @escaping () -> Void, completion: ((Bool) -> Void)? = nil) {
switch self { switch self {
case .immediate: case .immediate:
f() f()
completion?(true)
case let .animated(duration, curve): case let .animated(duration, curve):
UIView.animate(withDuration: duration, delay: 0.0, options: curve.viewAnimationOptions, animations: { UIView.animate(withDuration: duration, delay: 0.0, options: curve.viewAnimationOptions, animations: {
f() f()
}, completion: nil) }, completion: completion)
} }
} }
} }

View File

@ -500,6 +500,8 @@ open class NavigationController: UINavigationController, ContainableController,
var notifyGlobalOverlayControllersUpdated = false var notifyGlobalOverlayControllersUpdated = false
var additionalSideInsets = UIEdgeInsets()
var modalStyleOverlayTransitionFactor: CGFloat = 0.0 var modalStyleOverlayTransitionFactor: CGFloat = 0.0
var previousGlobalOverlayContainer: NavigationOverlayContainer? var previousGlobalOverlayContainer: NavigationOverlayContainer?
for i in (0 ..< self.globalOverlayContainers.count).reversed() { for i in (0 ..< self.globalOverlayContainers.count).reversed() {
@ -527,6 +529,9 @@ open class NavigationController: UINavigationController, ContainableController,
notifyGlobalOverlayControllersUpdated = true notifyGlobalOverlayControllersUpdated = true
} }
let controllerAdditionalSideInsets = overlayContainer.controller.additionalSideInsets
additionalSideInsets = UIEdgeInsets(top: 0.0, left: max(additionalSideInsets.left, controllerAdditionalSideInsets.left), bottom: 0.0, right: max(additionalSideInsets.right, controllerAdditionalSideInsets.right))
if overlayContainer.supernode != nil { if overlayContainer.supernode != nil {
previousGlobalOverlayContainer = overlayContainer previousGlobalOverlayContainer = overlayContainer
let controllerStatusBarStyle = overlayContainer.controller.statusBar.statusBarStyle let controllerStatusBarStyle = overlayContainer.controller.statusBar.statusBarStyle
@ -546,7 +551,6 @@ open class NavigationController: UINavigationController, ContainableController,
} }
} }
var additionalSideInsets = UIEdgeInsets()
var previousOverlayContainer: NavigationOverlayContainer? var previousOverlayContainer: NavigationOverlayContainer?
for i in (0 ..< self.overlayContainers.count).reversed() { for i in (0 ..< self.overlayContainers.count).reversed() {
let overlayContainer = self.overlayContainers[i] let overlayContainer = self.overlayContainers[i]

View File

@ -47,15 +47,13 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
var pressing: Bool = false { var pressing: Bool = false {
didSet { didSet {
var pressing = self.pressing
var snap = false var snap = false
if let (_, _, _, _, _, _, _, snapValue) = self.currentParams, snapValue { if let (_, _, _, _, _, _, _, snapValue) = self.currentParams {
pressing = false snap = snapValue
snap = true
} }
if pressing { if self.pressing {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: self.iconNode, scale: 0.9) transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 0.9)
} else { } else {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 1.0) transition.updateTransformScale(node: self.iconNode, scale: snap ? 0.5 : 1.0)
@ -80,18 +78,15 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
self.containerNode.addSubnode(self.backgroundNode) self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.iconNode) self.containerNode.addSubnode(self.iconNode)
self.highligthedChanged = { [weak self] highlighted in self.highligthedChanged = { [weak self] pressing in
if let strongSelf = self { if let strongSelf = self {
var highlighted = highlighted
var snap = false var snap = false
if let (_, _, _, _, _, _, _, snapValue) = strongSelf.currentParams, snapValue { if let (_, _, _, _, _, _, _, snapValue) = strongSelf.currentParams {
highlighted = false snap = snapValue
snap = true
} }
if pressing {
if highlighted {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: strongSelf.iconNode, scale: 0.9) transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 0.9)
} else if !strongSelf.pressing { } else if !strongSelf.pressing {
let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring) let transition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .spring)
transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0) transition.updateTransformScale(node: strongSelf.iconNode, scale: snap ? 0.5 : 1.0)
@ -173,7 +168,7 @@ final class VoiceChatActionButton: HighlightTrackingButtonNode {
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.2, curve: .easeInOut) : .immediate
if snap { if snap {
transition.updateTransformScale(node: self.backgroundNode, scale: active ? 0.75 : 0.5) transition.updateTransformScale(node: self.backgroundNode, scale: self.pressing ? 0.75 : 0.5)
transition.updateTransformScale(node: self.iconNode, scale: 0.5) transition.updateTransformScale(node: self.iconNode, scale: 0.5)
transition.updateAlpha(node: self.titleLabel, alpha: 0.0) transition.updateAlpha(node: self.titleLabel, alpha: 0.0)
transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0) transition.updateAlpha(node: self.subtitleLabel, alpha: 0.0)
@ -909,8 +904,8 @@ private final class VoiceBlobView: UIView {
pointsCount: 8, pointsCount: 8,
minRandomness: 1, minRandomness: 1,
maxRandomness: 1, maxRandomness: 1,
minSpeed: 0.85, minSpeed: 0.9,
maxSpeed: 7, maxSpeed: 4.0,
minScale: mediumBlobRange.min, minScale: mediumBlobRange.min,
maxScale: mediumBlobRange.max maxScale: mediumBlobRange.max
) )
@ -918,8 +913,8 @@ private final class VoiceBlobView: UIView {
pointsCount: 8, pointsCount: 8,
minRandomness: 1, minRandomness: 1,
maxRandomness: 1, maxRandomness: 1,
minSpeed: 0.85, minSpeed: 1.0,
maxSpeed: 7, maxSpeed: 4.4,
minScale: bigBlobRange.min, minScale: bigBlobRange.min,
maxScale: bigBlobRange.max maxScale: bigBlobRange.max
) )

View File

@ -460,6 +460,7 @@ public final class VoiceChatController: ViewController {
self.dimNode.backgroundColor = dimColor self.dimNode.backgroundColor = dimColor
self.contentContainer = ASDisplayNode() self.contentContainer = ASDisplayNode()
self.contentContainer.isHidden = true
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor self.backgroundNode.backgroundColor = secondaryPanelBackgroundColor
@ -1596,7 +1597,7 @@ public final class VoiceChatController: ViewController {
} }
func animateIn() { func animateIn() {
guard let (layout, _) = self.validLayout else { guard let (layout, navigationHeight) = self.validLayout else {
return return
} }
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
@ -1605,13 +1606,23 @@ public final class VoiceChatController: ViewController {
let initialBounds = self.contentContainer.bounds let initialBounds = self.contentContainer.bounds
self.contentContainer.bounds = initialBounds.offsetBy(dx: 0.0, dy: -(layout.size.height - topPanelFrame.minY)) self.contentContainer.bounds = initialBounds.offsetBy(dx: 0.0, dy: -(layout.size.height - topPanelFrame.minY))
transition.animateView { self.contentContainer.isHidden = false
transition.animateView({
self.contentContainer.view.bounds = initialBounds self.contentContainer.view.bounds = initialBounds
} }, completion: { _ in
self.bottomPanelNode.addSubnode(self.actionButton)
self.containerLayoutUpdated(layout, navigationHeight:navigationHeight, transition: .immediate)
self.controller?.currentOverlayController?.dismiss()
self.controller?.currentOverlayController = nil
})
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
} }
func animateOut(completion: (() -> Void)?) { func animateOut(completion: (() -> Void)?) {
guard let (layout, _) = self.validLayout else {
return
}
var offsetCompleted = false var offsetCompleted = false
let internalCompletion: () -> Void = { [weak self] in let internalCompletion: () -> Void = { [weak self] in
if offsetCompleted { if offsetCompleted {
@ -1627,7 +1638,9 @@ public final class VoiceChatController: ViewController {
} }
} }
self.contentContainer.layer.animateBoundsOriginYAdditive(from: self.contentContainer.bounds.origin.y, to: -self.contentContainer.bounds.size.height, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in let topPanelFrame = self.topPanelNode.view.convert(self.topPanelNode.bounds, to: self.view)
self.contentContainer.layer.animateBoundsOriginYAdditive(from: self.contentContainer.bounds.origin.y, to: -(layout.size.height - topPanelFrame.minY), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
offsetCompleted = true offsetCompleted = true
internalCompletion() internalCompletion()
}) })
@ -1857,6 +1870,7 @@ public final class VoiceChatController: ViewController {
private var didAppearOnce: Bool = false private var didAppearOnce: Bool = false
private var isDismissed: Bool = false private var isDismissed: Bool = false
private var isDisconnected: Bool = false
private var controllerNode: Node { private var controllerNode: Node {
return self.displayNode as! Node return self.displayNode as! Node
@ -1864,7 +1878,7 @@ public final class VoiceChatController: ViewController {
private let idleTimerExtensionDisposable = MetaDisposable() private let idleTimerExtensionDisposable = MetaDisposable()
private var currentOverlayController: VoiceChatOverlayController? private weak var currentOverlayController: VoiceChatOverlayController?
private var validLayout: ContainerViewLayout? private var validLayout: ContainerViewLayout?
@ -1937,40 +1951,42 @@ public final class VoiceChatController: ViewController {
self.idleTimerExtensionDisposable.set(nil) self.idleTimerExtensionDisposable.set(nil)
DispatchQueue.main.async { DispatchQueue.main.async {
self.didAppearOnce = false
self.isDismissed = true
self.detachActionButton()
self.onViewDidDisappear?() self.onViewDidDisappear?()
} }
} }
public func dismiss(closing: Bool) { public func dismiss(closing: Bool) {
if closing { if !closing {
self.dismiss() self.detachActionButton()
} else { } else {
self.isDisconnected = true
}
self.dismiss()
}
private func detachActionButton() {
guard self.currentOverlayController == nil && !self.isDisconnected else {
return
}
let overlayController = VoiceChatOverlayController(actionButton: self.controllerNode.actionButton) let overlayController = VoiceChatOverlayController(actionButton: self.controllerNode.actionButton)
if let navigationController = self.navigationController as? NavigationController { if let navigationController = self.navigationController as? NavigationController {
navigationController.presentOverlay(controller: overlayController, inGlobal: true, blockInteraction: false) navigationController.presentOverlay(controller: overlayController, inGlobal: true, blockInteraction: false)
} }
self.sharedContext.presentGlobalController(overlayController, nil)
self.currentOverlayController = overlayController self.currentOverlayController = overlayController
self.reclaimActionButton = { [weak self, weak overlayController] in self.reclaimActionButton = { [weak self, weak overlayController] in
if let strongSelf = self { if let strongSelf = self {
let actionButton = strongSelf.controllerNode.actionButton let actionButton = strongSelf.controllerNode.actionButton
overlayController?.animateOut(reclaim: true, completion: { [weak self] in overlayController?.animateOut(reclaim: true, completion: {})
if let strongSelf = self {
strongSelf.controllerNode.bottomPanelNode.addSubnode(actionButton)
if let validLayout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(validLayout, transition: .immediate)
}
}
})
strongSelf.currentOverlayController = nil
strongSelf.reclaimActionButton = nil strongSelf.reclaimActionButton = nil
} }
} }
self.dismiss()
}
} }
override public func dismiss(completion: (() -> Void)? = nil) { override public func dismiss(completion: (() -> Void)? = nil) {

View File

@ -15,7 +15,7 @@ import AppBundle
import ContextUI import ContextUI
import PresentationDataUtils import PresentationDataUtils
final class VoiceChatOverlayController: ViewController { public final class VoiceChatOverlayController: ViewController {
private final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: VoiceChatOverlayController? private weak var controller: VoiceChatOverlayController?
@ -25,6 +25,41 @@ final class VoiceChatOverlayController: ViewController {
self.controller = controller self.controller = controller
} }
private var isButtonHidden = false
func update(hidden: Bool, slide: Bool, animated: Bool) {
guard let actionButton = self.controller?.actionButton, actionButton.supernode === self else {
return
}
if self.isButtonHidden == hidden {
return
}
self.isButtonHidden = hidden
if animated {
if hidden {
if slide {
} else {
actionButton.layer.removeAllAnimations()
actionButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak actionButton] _ in
actionButton?.isHidden = true
})
}
} else {
if slide {
} else {
actionButton.layer.removeAllAnimations()
actionButton.isHidden = false
actionButton.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
}
}
} else {
}
}
func animateIn(from: CGRect) { func animateIn(from: CGRect) {
guard let actionButton = self.controller?.actionButton else { guard let actionButton = self.controller?.actionButton else {
return return
@ -53,7 +88,7 @@ final class VoiceChatOverlayController: ViewController {
keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y)))
} }
actionButton.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, completion: { [weak self] _ in actionButton.layer.animateKeyframes(values: keyframes, duration: 0.2, keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { _ in
}) })
} }
@ -88,9 +123,8 @@ final class VoiceChatOverlayController: ViewController {
actionButton.update(snap: false) actionButton.update(snap: false)
actionButton.position = targetPosition actionButton.position = targetPosition
actionButton.layer.animateKeyframes(values: keyframes, duration: 0.4, keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { [weak self] _ in actionButton.layer.animateKeyframes(values: keyframes, duration: 0.4, keyPath: "position", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, completion: { _ in
completion() completion()
self?.controller?.dismiss()
}) })
} else { } else {
actionButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self, weak actionButton] _ in actionButton.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { [weak self, weak actionButton] _ in
@ -101,12 +135,15 @@ final class VoiceChatOverlayController: ViewController {
} }
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let actionButton = self.controller?.actionButton, actionButton.supernode === self, actionButton.frame.contains(point) { if let actionButton = self.controller?.actionButton, actionButton.supernode === self {
let actionButtonSize = CGSize(width: 84.0, height: 84.0)
let actionButtonFrame = CGRect(origin: CGPoint(x: actionButton.position.x - actionButtonSize.width / 2.0, y: actionButton.position.y - actionButtonSize.height / 2.0), size: actionButtonSize)
if actionButtonFrame.contains(point) {
return actionButton.hitTest(self.view.convert(point, to: actionButton.view), with: event) return actionButton.hitTest(self.view.convert(point, to: actionButton.view), with: event)
} else {
return nil
} }
} }
return nil
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
self.validLayout = layout self.validLayout = layout
@ -141,6 +178,10 @@ final class VoiceChatOverlayController: ViewController {
self.additionalSideInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0) self.additionalSideInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 75.0)
} }
deinit {
print("")
}
required init(coder aDecoder: NSCoder) { required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
@ -150,7 +191,7 @@ final class VoiceChatOverlayController: ViewController {
self.displayNodeDidLoad() self.displayNodeDidLoad()
} }
override func dismiss(completion: (() -> Void)? = nil) { public override func dismiss(completion: (() -> Void)? = nil) {
super.dismiss(completion: completion) super.dismiss(completion: completion)
self.presentingViewController?.dismiss(animated: false, completion: nil) self.presentingViewController?.dismiss(animated: false, completion: nil)
completion?() completion?()
@ -160,6 +201,10 @@ final class VoiceChatOverlayController: ViewController {
self.controllerNode.animateOut(reclaim: reclaim, completion: completion) self.controllerNode.animateOut(reclaim: reclaim, completion: completion)
} }
public func update(hidden: Bool, slide: Bool, animated: Bool) {
self.controllerNode.update(hidden: hidden, slide: slide, animated: animated)
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition) super.containerLayoutUpdated(layout, transition: transition)

View File

@ -666,8 +666,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
}, sendMessagesWithSignals: { [weak self] signals, _, _ in }, sendMessagesWithSignals: { [weak self] signals, _, _ in
if let strongSelf = self { if let strongSelf = self {
if canEditMessage(context: strongSelf.context, limitsConfiguration: strongSelf.context.currentLimitsConfiguration.with { $0 }, message: message) {
strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in }) strongSelf.interfaceInteraction?.setupEditMessage(messageId, { _ in })
strongSelf.editMessageMediaWithLegacySignals(signals!) strongSelf.editMessageMediaWithLegacySignals(signals!)
} else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: false)
}
} }
}, present: { [weak self] c, a in }, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a) self?.present(c, in: .window(.root), with: a)
@ -7052,6 +7056,24 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if saveInterfaceState { if saveInterfaceState {
self.saveInterfaceState(includeScrollState: false) self.saveInterfaceState(includeScrollState: false)
} }
if let navigationController = self.navigationController as? NavigationController {
var voiceChatOverlayController: VoiceChatOverlayController?
for controller in navigationController.globalOverlayControllers {
if let controller = controller as? VoiceChatOverlayController {
voiceChatOverlayController = controller
break
}
}
if let controller = voiceChatOverlayController {
if self.presentationInterfaceState.inputMode == .text && self.presentationInterfaceState.interfaceState.composeInputState.inputText.string.count > 0 {
controller.update(hidden: true, slide: false, animated: true)
} else {
controller.update(hidden: false, slide: false, animated: true)
}
}
}
} }
private func updateItemNodesSelectionStates(animated: Bool) { private func updateItemNodesSelectionStates(animated: Bool) {

View File

@ -634,7 +634,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|> deliverOnMainQueue).start(next: { [weak self] call in |> deliverOnMainQueue).start(next: { [weak self] call in
if let strongSelf = self { if let strongSelf = self {
if call !== strongSelf.groupCallController?.call { if call !== strongSelf.groupCallController?.call {
strongSelf.groupCallController?.dismiss() strongSelf.groupCallController?.dismiss(closing: true)
strongSelf.groupCallController = nil strongSelf.groupCallController = nil
strongSelf.hasOngoingCall.set(false) strongSelf.hasOngoingCall.set(false)