mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-24 07:05:35 +00:00
[WIP] Stories
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import AppBundle
|
||||
import ObjCRuntimeUtils
|
||||
|
||||
private let innerCircleDiameter: CGFloat = 110.0
|
||||
private let outerCircleDiameter = innerCircleDiameter + 50.0
|
||||
private let outerCircleMinScale = innerCircleDiameter / outerCircleDiameter
|
||||
private let innerCircleImage = generateFilledCircleImage(diameter: innerCircleDiameter, color: UIColor(rgb: 0x007aff))
|
||||
private let outerCircleImage = generateFilledCircleImage(diameter: outerCircleDiameter, color: UIColor(rgb: 0x007aff, alpha: 0.2))
|
||||
private let micIcon = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Text/IconMicrophone"), color: .white)!
|
||||
|
||||
private final class ChatTextInputAudioRecordingOverlayDisplayLinkTarget: NSObject {
|
||||
private let f: () -> Void
|
||||
|
||||
init(_ f: @escaping () -> Void) {
|
||||
self.f = f
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc func displayLinkEvent() {
|
||||
self.f()
|
||||
}
|
||||
}
|
||||
|
||||
final class ChatTextInputAudioRecordingOverlay {
|
||||
private weak var anchorView: UIView?
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private let circleContainerNode: ASDisplayNode
|
||||
private let innerCircleNode: ASImageNode
|
||||
private let outerCircleNode: ASImageNode
|
||||
private let iconNode: ASImageNode
|
||||
|
||||
var animationStartTime: Double?
|
||||
var displayLink: CADisplayLink?
|
||||
var currentLevel: CGFloat = 0.0
|
||||
var inputLevel: CGFloat = 0.0
|
||||
var animatedIn = false
|
||||
|
||||
var dismissFactor: CGFloat = 1.0 {
|
||||
didSet {
|
||||
let scale = max(0.3, min(self.dismissFactor, 1.0))
|
||||
self.circleContainerNode.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
init(anchorView: UIView) {
|
||||
self.anchorView = anchorView
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.isLayerBacked = true
|
||||
|
||||
self.circleContainerNode = ASDisplayNode()
|
||||
self.circleContainerNode.isLayerBacked = true
|
||||
|
||||
self.outerCircleNode = ASImageNode()
|
||||
self.outerCircleNode.displayWithoutProcessing = true
|
||||
self.outerCircleNode.displaysAsynchronously = false
|
||||
self.outerCircleNode.isLayerBacked = true
|
||||
self.outerCircleNode.image = outerCircleImage
|
||||
self.outerCircleNode.frame = CGRect(origin: CGPoint(x: -outerCircleDiameter / 2.0, y: -outerCircleDiameter / 2.0), size: CGSize(width: outerCircleDiameter, height: outerCircleDiameter))
|
||||
|
||||
self.innerCircleNode = ASImageNode()
|
||||
self.innerCircleNode.displayWithoutProcessing = true
|
||||
self.innerCircleNode.displaysAsynchronously = false
|
||||
self.innerCircleNode.isLayerBacked = true
|
||||
self.innerCircleNode.image = innerCircleImage
|
||||
self.innerCircleNode.frame = CGRect(origin: CGPoint(x: -innerCircleDiameter / 2.0, y: -innerCircleDiameter / 2.0), size: CGSize(width: innerCircleDiameter, height: innerCircleDiameter))
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.isLayerBacked = true
|
||||
self.iconNode.image = micIcon
|
||||
self.iconNode.frame = CGRect(origin: CGPoint(x: -micIcon.size.width / 2.0, y: -micIcon.size.height / 2.0), size: micIcon.size)
|
||||
|
||||
self.circleContainerNode.addSubnode(self.outerCircleNode)
|
||||
self.circleContainerNode.addSubnode(self.innerCircleNode)
|
||||
self.containerNode.addSubnode(self.circleContainerNode)
|
||||
self.containerNode.addSubnode(self.iconNode)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.displayLink?.invalidate()
|
||||
}
|
||||
|
||||
func present(in window: UIWindow) {
|
||||
if let anchorView = self.anchorView, let anchorSuperview = anchorView.superview {
|
||||
if let displayLink = self.displayLink {
|
||||
displayLink.invalidate()
|
||||
}
|
||||
self.displayLink = CADisplayLink(target: ChatTextInputAudioRecordingOverlayDisplayLinkTarget({ [weak self] in
|
||||
self?.displayLinkEvent()
|
||||
}), selector: #selector(ChatTextInputAudioRecordingOverlayDisplayLinkTarget.displayLinkEvent))
|
||||
|
||||
let convertedCenter = anchorSuperview.convert(anchorView.center, to: window)
|
||||
self.containerNode.position = CGPoint(x: convertedCenter.x, y: convertedCenter.y)
|
||||
window.addSubnode(self.containerNode)
|
||||
|
||||
self.innerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
self.outerCircleNode.layer.transform = CATransform3DMakeScale(outerCircleMinScale, outerCircleMinScale, 1.0)
|
||||
self.outerCircleNode.layer.animateSpring(from: 0.2 as NSNumber, to: outerCircleMinScale as NSNumber, keyPath: "transform.scale", duration: 0.5)
|
||||
self.innerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15)
|
||||
self.outerCircleNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15)
|
||||
self.iconNode.layer.animateAlpha(from: 0.2, to: 1.0, duration: 0.15)
|
||||
|
||||
self.animatedIn = true
|
||||
self.animationStartTime = CACurrentMediaTime()
|
||||
self.displayLink?.add(to: RunLoop.main, forMode: .common)
|
||||
self.displayLink?.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
self.displayLink?.invalidate()
|
||||
self.displayLink = nil
|
||||
|
||||
var innerCompleted = false
|
||||
var outerCompleted = false
|
||||
var iconCompleted = false
|
||||
|
||||
var containerNodeRef: ASDisplayNode? = self.containerNode
|
||||
|
||||
let completion: () -> Void = {
|
||||
if let containerNode = containerNodeRef, innerCompleted, outerCompleted, iconCompleted {
|
||||
containerNode.removeFromSupernode()
|
||||
containerNodeRef = nil
|
||||
}
|
||||
}
|
||||
|
||||
self.innerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
|
||||
self.innerCircleNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in
|
||||
innerCompleted = true
|
||||
completion()
|
||||
})
|
||||
|
||||
var currentScaleValue: CGFloat = outerCircleMinScale
|
||||
if let currentScale = self.outerCircleNode.layer.floatValue(forKeyPath: "transform.scale") {
|
||||
currentScaleValue = CGFloat(currentScale.floatValue)
|
||||
}
|
||||
|
||||
self.outerCircleNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
|
||||
self.outerCircleNode.layer.animateScale(from: currentScaleValue, to: 0.2, duration: 0.18, removeOnCompletion: false, completion: { _ in
|
||||
outerCompleted = true
|
||||
completion()
|
||||
})
|
||||
|
||||
self.iconNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { _ in
|
||||
iconCompleted = true
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
private func displayLinkEvent() {
|
||||
let t = CACurrentMediaTime()
|
||||
if let animationStartTime = self.animationStartTime {
|
||||
if t > animationStartTime + 0.5 {
|
||||
self.currentLevel = self.currentLevel * 0.8 + self.inputLevel * 0.2
|
||||
|
||||
let scale = outerCircleMinScale + self.currentLevel * (1.0 - outerCircleMinScale)
|
||||
self.outerCircleNode.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addImmediateMicLevel(_ level: CGFloat) {
|
||||
self.inputLevel = level
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import LegacyComponents
|
||||
import AccountContext
|
||||
import ChatInterfaceState
|
||||
import AudioBlob
|
||||
import ChatPresentationInterfaceState
|
||||
import ComponentFlow
|
||||
import LottieAnimationComponent
|
||||
import LottieComponent
|
||||
import LegacyInstantVideoController
|
||||
|
||||
private let offsetThreshold: CGFloat = 10.0
|
||||
private let dismissOffsetThreshold: CGFloat = 70.0
|
||||
|
||||
private func findTargetView(_ view: UIView, point: CGPoint) -> UIView? {
|
||||
if view.bounds.contains(point) && view.tag == 0x01f2bca {
|
||||
return view
|
||||
}
|
||||
for subview in view.subviews {
|
||||
let frame = subview.frame
|
||||
if let result = findTargetView(subview, point: point.offsetBy(dx: -frame.minX, dy: -frame.minY)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenterContainer: UIView {
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let result = findTargetView(self, point: point) {
|
||||
return result
|
||||
}
|
||||
for subview in self.subviews {
|
||||
if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenterController: ViewController {
|
||||
private var controllerNode: ChatTextInputMediaRecordingButtonPresenterControllerNode {
|
||||
return self.displayNode as! ChatTextInputMediaRecordingButtonPresenterControllerNode
|
||||
}
|
||||
|
||||
var containerView: UIView? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.controllerNode.containerView = self.containerView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
self.displayNode = ChatTextInputMediaRecordingButtonPresenterControllerNode()
|
||||
if let containerView = self.containerView {
|
||||
self.controllerNode.containerView = containerView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenterControllerNode: ViewControllerTracingNode {
|
||||
var containerView: UIView? {
|
||||
didSet {
|
||||
if self.containerView !== oldValue {
|
||||
if self.isNodeLoaded, let containerView = oldValue, containerView.superview === self.view {
|
||||
containerView.removeFromSuperview()
|
||||
}
|
||||
if self.isNodeLoaded, let containerView = self.containerView {
|
||||
self.view.addSubview(containerView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
if let containerView = self.containerView {
|
||||
self.view.addSubview(containerView)
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let containerView = self.containerView {
|
||||
if let result = containerView.hitTest(point, with: event), result !== containerView {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatTextInputMediaRecordingButtonPresenter : NSObject, TGModernConversationInputMicButtonPresentation {
|
||||
private let statusBarHost: StatusBarHost?
|
||||
private let presentController: (ViewController) -> Void
|
||||
let container: ChatTextInputMediaRecordingButtonPresenterContainer
|
||||
private var presentationController: ChatTextInputMediaRecordingButtonPresenterController?
|
||||
private var timer: SwiftSignalKit.Timer?
|
||||
fileprivate weak var button: ChatTextInputMediaRecordingButton?
|
||||
|
||||
init(statusBarHost: StatusBarHost?, presentController: @escaping (ViewController) -> Void) {
|
||||
self.statusBarHost = statusBarHost
|
||||
self.presentController = presentController
|
||||
self.container = ChatTextInputMediaRecordingButtonPresenterContainer()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.container.removeFromSuperview()
|
||||
if let presentationController = self.presentationController {
|
||||
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
|
||||
self.presentationController = nil
|
||||
}
|
||||
self.timer?.invalidate()
|
||||
}
|
||||
|
||||
func view() -> UIView! {
|
||||
return self.container
|
||||
}
|
||||
|
||||
func setUserInteractionEnabled(_ enabled: Bool) {
|
||||
self.container.isUserInteractionEnabled = enabled
|
||||
}
|
||||
|
||||
func present() {
|
||||
let windowIsVisible: (UIWindow) -> Bool = { window in
|
||||
return !window.frame.height.isZero
|
||||
}
|
||||
|
||||
if let statusBarHost = self.statusBarHost, let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) {
|
||||
keyboardWindow.addSubview(self.container)
|
||||
|
||||
self.timer = SwiftSignalKit.Timer(timeout: 0.05, repeat: true, completion: { [weak self] in
|
||||
if let keyboardWindow = LegacyComponentsGlobals.provider().applicationKeyboardWindow(), windowIsVisible(keyboardWindow) {
|
||||
} else {
|
||||
self?.present()
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.timer?.start()
|
||||
} else {
|
||||
var presentNow = false
|
||||
if self.presentationController == nil {
|
||||
let presentationController = ChatTextInputMediaRecordingButtonPresenterController(navigationBarPresentationData: nil)
|
||||
presentationController.statusBar.statusBarStyle = .Ignore
|
||||
self.presentationController = presentationController
|
||||
presentNow = true
|
||||
}
|
||||
|
||||
self.presentationController?.containerView = self.container
|
||||
if let presentationController = self.presentationController, presentNow {
|
||||
self.presentController(presentationController)
|
||||
}
|
||||
|
||||
if let timer = self.timer {
|
||||
self.button?.reset()
|
||||
timer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
self.timer?.invalidate()
|
||||
self.container.removeFromSuperview()
|
||||
if let presentationController = self.presentationController {
|
||||
presentationController.presentingViewController?.dismiss(animated: false, completion: {})
|
||||
self.presentationController = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final class ChatTextInputMediaRecordingButton: TGModernConversationInputMicButton, TGModernConversationInputMicButtonDelegate {
|
||||
private let context: AccountContext
|
||||
private var theme: PresentationTheme
|
||||
private let strings: PresentationStrings
|
||||
|
||||
public var mode: ChatTextInputMediaRecordingButtonMode = .audio
|
||||
public var statusBarHost: StatusBarHost?
|
||||
public let presentController: (ViewController) -> Void
|
||||
public var recordingDisabled: () -> Void = { }
|
||||
public var beginRecording: () -> Void = { }
|
||||
public var endRecording: (Bool) -> Void = { _ in }
|
||||
public var stopRecording: () -> Void = { }
|
||||
public var offsetRecordingControls: () -> Void = { }
|
||||
public var switchMode: () -> Void = { }
|
||||
public var updateLocked: (Bool) -> Void = { _ in }
|
||||
public var updateCancelTranslation: () -> Void = { }
|
||||
|
||||
private var modeTimeoutTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private let animationView: ComponentView<Empty>
|
||||
|
||||
private var recordingOverlay: ChatTextInputAudioRecordingOverlay?
|
||||
private var startTouchLocation: CGPoint?
|
||||
fileprivate var controlsOffset: CGFloat = 0.0
|
||||
public private(set) var cancelTranslation: CGFloat = 0.0
|
||||
|
||||
private var micLevelDisposable: MetaDisposable?
|
||||
|
||||
private weak var currentPresenter: UIView?
|
||||
|
||||
public var contentContainer: (UIView, CGRect)? {
|
||||
if let _ = self.currentPresenter {
|
||||
return (self.micDecoration, self.micDecoration.bounds)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var audioRecorder: ManagedAudioRecorder? {
|
||||
didSet {
|
||||
if self.audioRecorder !== oldValue {
|
||||
if self.micLevelDisposable == nil {
|
||||
micLevelDisposable = MetaDisposable()
|
||||
}
|
||||
if let audioRecorder = self.audioRecorder {
|
||||
self.micLevelDisposable?.set(audioRecorder.micLevel.start(next: { [weak self] level in
|
||||
Queue.mainQueue().async {
|
||||
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
|
||||
self?.addMicLevel(CGFloat(level))
|
||||
}
|
||||
}))
|
||||
} else if self.videoRecordingStatus == nil {
|
||||
self.micLevelDisposable?.set(nil)
|
||||
}
|
||||
|
||||
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var videoRecordingStatus: InstantVideoControllerRecordingStatus? {
|
||||
didSet {
|
||||
if self.videoRecordingStatus !== oldValue {
|
||||
if self.micLevelDisposable == nil {
|
||||
micLevelDisposable = MetaDisposable()
|
||||
}
|
||||
|
||||
if let videoRecordingStatus = self.videoRecordingStatus {
|
||||
self.micLevelDisposable?.set(videoRecordingStatus.micLevel.start(next: { [weak self] level in
|
||||
Queue.mainQueue().async {
|
||||
//self?.recordingOverlay?.addImmediateMicLevel(CGFloat(level))
|
||||
self?.addMicLevel(CGFloat(level))
|
||||
}
|
||||
}))
|
||||
} else if self.audioRecorder == nil {
|
||||
self.micLevelDisposable?.set(nil)
|
||||
}
|
||||
|
||||
self.hasRecorder = self.audioRecorder != nil || self.videoRecordingStatus != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var hasRecorder: Bool = false {
|
||||
didSet {
|
||||
if self.hasRecorder != oldValue {
|
||||
if self.hasRecorder {
|
||||
self.animateIn()
|
||||
} else {
|
||||
self.animateOut(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var micDecorationValue: VoiceBlobView?
|
||||
private var micDecoration: (UIView & TGModernConversationInputMicButtonDecoration) {
|
||||
if let micDecorationValue = self.micDecorationValue {
|
||||
return micDecorationValue
|
||||
} else {
|
||||
let blobView = VoiceBlobView(
|
||||
frame: CGRect(origin: CGPoint(), size: CGSize(width: 220.0, height: 220.0)),
|
||||
maxLevel: 4,
|
||||
smallBlobRange: (0.45, 0.55),
|
||||
mediumBlobRange: (0.52, 0.87),
|
||||
bigBlobRange: (0.57, 1.00)
|
||||
)
|
||||
blobView.setColor(self.theme.chat.inputPanel.actionControlFillColor)
|
||||
self.micDecorationValue = blobView
|
||||
return blobView
|
||||
}
|
||||
}
|
||||
|
||||
private var micLockValue: (UIView & TGModernConversationInputMicButtonLock)?
|
||||
private var micLock: UIView & TGModernConversationInputMicButtonLock {
|
||||
if let current = self.micLockValue {
|
||||
return current
|
||||
} else {
|
||||
let lockView = LockView(frame: CGRect(origin: CGPoint(), size: CGSize(width: 40.0, height: 60.0)), theme: self.theme, strings: self.strings)
|
||||
lockView.addTarget(self, action: #selector(handleStopTap), for: .touchUpInside)
|
||||
self.micLockValue = lockView
|
||||
return lockView
|
||||
}
|
||||
}
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, presentController: @escaping (ViewController) -> Void) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.animationView = ComponentView<Empty>()
|
||||
self.presentController = presentController
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
self.pallete = legacyInputMicPalette(from: theme)
|
||||
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
self.updateMode(mode: self.mode, animated: false, force: true)
|
||||
|
||||
self.delegate = self
|
||||
self.isExclusiveTouch = false;
|
||||
|
||||
self.centerOffset = CGPoint(x: 0.0, y: -1.0 + UIScreenPixel)
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let micLevelDisposable = self.micLevelDisposable {
|
||||
micLevelDisposable.dispose()
|
||||
}
|
||||
if let recordingOverlay = self.recordingOverlay {
|
||||
recordingOverlay.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
public func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool) {
|
||||
self.updateMode(mode: mode, animated: animated, force: false)
|
||||
}
|
||||
|
||||
private func updateMode(mode: ChatTextInputMediaRecordingButtonMode, animated: Bool, force: Bool) {
|
||||
let previousMode = self.mode
|
||||
if mode != self.mode || force {
|
||||
self.mode = mode
|
||||
|
||||
self.updateAnimation(previousMode: previousMode)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAnimation(previousMode: ChatTextInputMediaRecordingButtonMode) {
|
||||
let image: UIImage?
|
||||
switch self.mode {
|
||||
case .audio:
|
||||
self.icon = PresentationResourcesChat.chatInputPanelVoiceActiveButtonImage(self.theme)
|
||||
image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
|
||||
case .video:
|
||||
self.icon = PresentationResourcesChat.chatInputPanelVideoActiveButtonImage(self.theme)
|
||||
image = PresentationResourcesChat.chatInputPanelVoiceButtonImage(self.theme)
|
||||
}
|
||||
|
||||
let size = self.bounds.size
|
||||
let iconSize: CGSize
|
||||
if let image = image {
|
||||
iconSize = image.size
|
||||
} else {
|
||||
iconSize = size
|
||||
}
|
||||
|
||||
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
|
||||
let animationName: String
|
||||
switch self.mode {
|
||||
case .audio:
|
||||
animationName = "anim_videoToMic"
|
||||
case .video:
|
||||
animationName = "anim_micToVideo"
|
||||
}
|
||||
|
||||
let _ = animationView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(name: animationName),
|
||||
color: self.theme.chat.inputPanel.panelControlColor.blitOver(self.theme.chat.inputPanel.inputBackgroundColor, alpha: 1.0)
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: animationFrame.size
|
||||
)
|
||||
|
||||
if let view = animationView.view as? LottieComponent.View {
|
||||
view.isUserInteractionEnabled = false
|
||||
if view.superview == nil {
|
||||
self.insertSubview(view, at: 0)
|
||||
}
|
||||
view.frame = animationFrame
|
||||
|
||||
if previousMode != mode {
|
||||
view.playOnce()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTheme(theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.updateAnimation(previousMode: self.mode)
|
||||
|
||||
self.pallete = legacyInputMicPalette(from: theme)
|
||||
self.micDecorationValue?.setColor(self.theme.chat.inputPanel.actionControlFillColor)
|
||||
(self.micLockValue as? LockView)?.updateTheme(theme)
|
||||
}
|
||||
|
||||
public func cancelRecording() {
|
||||
self.isEnabled = false
|
||||
self.isEnabled = true
|
||||
}
|
||||
|
||||
public func micButtonInteractionBegan() {
|
||||
if self.fadeDisabled {
|
||||
self.recordingDisabled()
|
||||
} else {
|
||||
//print("\(CFAbsoluteTimeGetCurrent()) began")
|
||||
self.modeTimeoutTimer?.invalidate()
|
||||
let modeTimeoutTimer = SwiftSignalKit.Timer(timeout: 0.19, repeat: false, completion: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.modeTimeoutTimer = nil
|
||||
strongSelf.beginRecording()
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
self.modeTimeoutTimer = modeTimeoutTimer
|
||||
modeTimeoutTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
public func micButtonInteractionCancelled(_ velocity: CGPoint) {
|
||||
//print("\(CFAbsoluteTimeGetCurrent()) cancelled")
|
||||
self.modeTimeoutTimer?.invalidate()
|
||||
self.endRecording(false)
|
||||
}
|
||||
|
||||
public func micButtonInteractionCompleted(_ velocity: CGPoint) {
|
||||
//print("\(CFAbsoluteTimeGetCurrent()) completed")
|
||||
if let modeTimeoutTimer = self.modeTimeoutTimer {
|
||||
//print("\(CFAbsoluteTimeGetCurrent()) switch")
|
||||
modeTimeoutTimer.invalidate()
|
||||
self.modeTimeoutTimer = nil
|
||||
self.switchMode()
|
||||
}
|
||||
self.endRecording(true)
|
||||
}
|
||||
|
||||
public func micButtonInteractionUpdate(_ offset: CGPoint) {
|
||||
self.controlsOffset = offset.x
|
||||
self.offsetRecordingControls()
|
||||
}
|
||||
|
||||
public func micButtonInteractionUpdateCancelTranslation(_ translation: CGFloat) {
|
||||
self.cancelTranslation = translation
|
||||
self.updateCancelTranslation()
|
||||
}
|
||||
|
||||
public func micButtonInteractionLocked() {
|
||||
self.updateLocked(true)
|
||||
}
|
||||
|
||||
public func micButtonInteractionRequestedLockedAction() {
|
||||
}
|
||||
|
||||
public func micButtonInteractionStopped() {
|
||||
self.stopRecording()
|
||||
}
|
||||
|
||||
public func micButtonShouldLock() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public func micButtonPresenter() -> TGModernConversationInputMicButtonPresentation! {
|
||||
let presenter = ChatTextInputMediaRecordingButtonPresenter(statusBarHost: self.statusBarHost, presentController: self.presentController)
|
||||
presenter.button = self
|
||||
self.currentPresenter = presenter.view()
|
||||
return presenter
|
||||
}
|
||||
|
||||
public func micButtonDecoration() -> (UIView & TGModernConversationInputMicButtonDecoration)! {
|
||||
return micDecoration
|
||||
}
|
||||
|
||||
public func micButtonLock() -> (UIView & TGModernConversationInputMicButtonLock)! {
|
||||
return micLock
|
||||
}
|
||||
|
||||
@objc private func handleStopTap() {
|
||||
micButtonInteractionStopped()
|
||||
}
|
||||
|
||||
override public func animateIn() {
|
||||
super.animateIn()
|
||||
|
||||
if self.context.sharedContext.energyUsageSettings.fullTranslucency {
|
||||
micDecoration.isHidden = false
|
||||
micDecoration.startAnimating()
|
||||
}
|
||||
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut)
|
||||
if let layer = self.animationView.view?.layer {
|
||||
transition.updateAlpha(layer: layer, alpha: 0.0)
|
||||
transition.updateTransformScale(layer: layer, scale: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
override public func animateOut(_ toSmallSize: Bool) {
|
||||
super.animateOut(toSmallSize)
|
||||
|
||||
micDecoration.stopAnimating()
|
||||
|
||||
if toSmallSize {
|
||||
micDecoration.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.03, delay: 0.15, removeOnCompletion: false)
|
||||
} else {
|
||||
micDecoration.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false)
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.15, curve: .easeInOut)
|
||||
if let layer = self.animationView.view?.layer {
|
||||
transition.updateAlpha(layer: layer, alpha: 1.0)
|
||||
transition.updateTransformScale(layer: layer, scale: 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var previousSize = CGSize()
|
||||
public func layoutItems() {
|
||||
let size = self.bounds.size
|
||||
if size != self.previousSize {
|
||||
self.previousSize = size
|
||||
if let view = self.animationView.view {
|
||||
let iconSize = view.bounds.size
|
||||
view.frame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: floor((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import UIKit
|
||||
import LegacyComponents
|
||||
import AppBundle
|
||||
import Lottie
|
||||
import TelegramPresentationData
|
||||
|
||||
final class LockView: UIButton, TGModernConversationInputMicButtonLock {
|
||||
//private var colorCallbacks = [LOTValueDelegate]()
|
||||
|
||||
private let idleView: AnimationView = {
|
||||
guard let url = getAppBundle().url(forResource: "LockWait", withExtension: "json"), let animation = Animation.filepath(url.path)
|
||||
else { return AnimationView() }
|
||||
|
||||
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
|
||||
view.loopMode = .autoReverse
|
||||
view.backgroundColor = .clear
|
||||
view.isOpaque = false
|
||||
return view
|
||||
}()
|
||||
|
||||
private let lockingView: AnimationView = {
|
||||
guard let url = getAppBundle().url(forResource: "Lock", withExtension: "json"), let animation = Animation.filepath(url.path)
|
||||
else { return AnimationView() }
|
||||
|
||||
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
|
||||
view.backgroundColor = .clear
|
||||
view.isOpaque = false
|
||||
return view
|
||||
}()
|
||||
|
||||
init(frame: CGRect, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
super.init(frame: frame)
|
||||
|
||||
accessibilityLabel = strings.VoiceOver_Recording_StopAndPreview
|
||||
|
||||
addSubview(idleView)
|
||||
idleView.frame = bounds
|
||||
|
||||
addSubview(lockingView)
|
||||
lockingView.frame = bounds
|
||||
|
||||
updateTheme(theme)
|
||||
updateLockness(0)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func updateLockness(_ lockness: CGFloat) {
|
||||
idleView.isHidden = lockness > 0
|
||||
if lockness > 0 && idleView.isAnimationPlaying {
|
||||
idleView.stop()
|
||||
} else if lockness == 0 && !idleView.isAnimationPlaying {
|
||||
idleView.play()
|
||||
}
|
||||
lockingView.isHidden = !idleView.isHidden
|
||||
|
||||
lockingView.currentProgress = lockness
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
//colorCallbacks.removeAll()
|
||||
|
||||
[
|
||||
"Rectangle.Заливка 1": theme.chat.inputPanel.panelBackgroundColor,
|
||||
"Rectangle.Rectangle.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
|
||||
"Path.Path.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
|
||||
"Path 4.Path 4.Обводка 1": theme.chat.inputPanel.panelControlAccentColor
|
||||
].forEach { key, value in
|
||||
idleView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
|
||||
/*let colorCallback = LOTColorValueCallback(color: value.cgColor)
|
||||
self.colorCallbacks.append(colorCallback)
|
||||
idleView.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))*/
|
||||
}
|
||||
|
||||
[
|
||||
"Path.Path.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
|
||||
"Path.Path.Заливка 1": theme.chat.inputPanel.panelBackgroundColor,
|
||||
"Rectangle.Rectangle.Обводка 1": theme.chat.inputPanel.panelControlAccentColor,
|
||||
"Rectangle.Заливка 1": theme.chat.inputPanel.panelControlAccentColor,
|
||||
"Path 4.Path 4.Обводка 1": theme.chat.inputPanel.panelControlAccentColor
|
||||
].forEach { key, value in
|
||||
lockingView.setValueProvider(ColorValueProvider(value.lottieColorValue), keypath: AnimationKeypath(keypath: "\(key).Color"))
|
||||
/*let colorCallback = LOTColorValueCallback(color: value.cgColor)
|
||||
self.colorCallbacks.append(colorCallback)
|
||||
lockingView.setValueDelegate(colorCallback, for: LOTKeypath(string: "\(key).Color"))*/
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let superTest = super.hitTest(point, with: event)
|
||||
if superTest === lockingView {
|
||||
return self
|
||||
}
|
||||
return superTest
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user