// // ChatKeyboardView.swift // yinmeng-ios // // Created by MaiMang on 2024/2/29. // import UIKit import NIMSDK protocol ChatKeyboardViewDelegate: NSObjectProtocol { /// 录音完成 func keyboard(_ keyboard: ChatKeyboardView, voiceDidFinish path: String) /// 输入完消息 func keyboard(_ keyboard: ChatKeyboardView, DidFinish content: String) /// 键盘收起/弹出 func keyboard(_ keyboard: ChatKeyboardView, DidBecome isBecome: Bool) /// 键盘的y值 func keyboard(_ keyboard: ChatKeyboardView, DidObserver offsetY: CGFloat) /// 菜单栏点击 func keyboard(_ keyboard: ChatKeyboardView, DidMoreMenu type: ChatMoreMenuType) } extension ChatKeyboardViewDelegate { func keyboard(_ keyboard: ChatKeyboardView, DidFinish content: String) {} func keyboard(_ keyboard: ChatKeyboardView, DidBecome isBecome: Bool) {} func keyboard(_ keyboard: ChatKeyboardView, DidObserver offsetY: CGFloat) {} func keyboard(_ keyboard: ChatKeyboardView, DidMoreMenu type: ChatMoreMenuType) {} } class ChatKeyboardView: UIView { private let kLeft: CGFloat = 12.0 private let kSpace: CGFloat = 12.0 private let kViewWH: CGFloat = 26.0 private let kLineHeight: CGFloat = 0.75 // MARK: - var lazy weak var delegate: ChatKeyboardViewDelegate? fileprivate var toolBarHeight: CGFloat = (52.0 + SafeAraeBottomHeight) fileprivate var lastTextHeight: CGFloat = 34.0 fileprivate var keyboardHeight: CGFloat = 0.0 /// 底部菜单容器高度 fileprivate var contentHeight: CGFloat = 0.0 fileprivate var isShowMore = false /// 是否弹出了系统键盘 fileprivate var isShowKeyboard = false private lazy var sendVoiceBtn: UIButton = { let button = UIButton(type: .custom) let w: CGFloat = ScreenWidth - self.kViewWH * 2 - self.kSpace * 3 - self.kSpace button.frame = CGRect(x: self.kSpace + self.kLeft + self.kViewWH, y: self.kSpace, width: w, height: 36) button.setTitle("按住说话", for: .normal) button.setTitle("松开结束", for: .normal) button.backgroundColor = ThemeColor(hexStr: "F1F1FA") button.setTitleColor(ThemeColor(hexStr: "#282828"), for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .medium) button.layer.masksToBounds = true button.layer.cornerRadius = 4 button.isHidden = true return button }() /// 语音文字 切换按钮 fileprivate lazy var changeButton : UIButton = { let button = UIButton(type: .custom) let x: CGFloat = self.kLeft button.frame = CGRect(x: x, y: self.kSpace + 5, width: self.kViewWH, height: self.kViewWH) button.setImage(UIImage(named: "chat_input_text"), for: .normal) button.setImage(UIImage(named: "chat_voice"), for: .highlighted) button.addTarget(self, action: #selector(changeDidAction(_:)), for: .touchUpInside) return button }() /// 更多按钮 fileprivate lazy var moreButton : UIButton = { let button = UIButton(type: .custom) let x: CGFloat = ScreenWidth - self.kViewWH - self.kSpace button.frame = CGRect(x: x, y: self.kSpace + 5, width: self.kViewWH, height: self.kViewWH) button.setImage(UIImage(named: "chat_more"), for: .normal) button.setImage(UIImage(named: "chat_more"), for: .highlighted) button.addTarget(self, action: #selector(moreDidAction(_:)), for: .touchUpInside) return button }() /// 文本输入框 fileprivate lazy var chatTextView: ChatGrowingTextView = { let w: CGFloat = ScreenWidth - self.kViewWH * 2 - self.kSpace * 3 - self.kSpace let textView = ChatGrowingTextView(frame: CGRect(x: self.kSpace + self.kLeft + self.kViewWH, y: self.kSpace, width: w, height: 36)) textView.placeholder = "请输入..." textView.textColor = ThemeColor(hexStr: "#000000") textView.maxNumberOfLines = 5 textView.delegate = self textView.backgroundColor = ThemeColor(hexStr: "#F1F1FA") textView.layer.cornerRadius = 4 textView.layer.masksToBounds = true textView.didTextChangedHeightClosure = { [weak self] height in self?.changeKeyboardHeight(height: height) } return textView }() fileprivate lazy var toolBarView: UIView = { let view = UIView(frame: CGRect(x: 0, y: 0, width: ScreenWidth, height: self.toolBarHeight)) view.backgroundColor = .clear return view }() /// 底部背景容器 fileprivate lazy var contentView: UIView = { let y = self.toolBarView.maxY let view = UIView(frame: CGRect(x: 0, y: y, width: ScreenWidth, height: self.contentHeight)) view.backgroundColor = .white return view }() /// 更多菜单 fileprivate lazy var moreMenuView: ChatMoreMenuView = { let view = ChatMoreMenuView(frame: self.contentView.bounds) view.isHidden = true view.delegate = self return view }() private lazy var voiceView: ChatSendVoiceView = { let view = ChatSendVoiceView() view.backgroundColor = .clear return view }() // MARK: - life cycle override init(frame: CGRect) { super.init(frame: frame) NIMSDK.shared().mediaManager.add(self) setupKeyboardView() registerNotification() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupKeyboardView() registerNotification() } func setupKeyboardView() { self.backgroundColor = .clear self.isUserInteractionEnabled = true addSendVoiceAction() addSubview(toolBarView) toolBarView.addSubview(moreButton) toolBarView.addSubview(changeButton) toolBarView.addSubview(chatTextView) toolBarView.addSubview(sendVoiceBtn) addSubview(contentView) contentView.addSubview(moreMenuView) } private func addSendVoiceAction() { sendVoiceBtn.addTarget(self, action: #selector(audioTouchDownAction), for: .touchDown) sendVoiceBtn.addTarget(self, action: #selector(audioTouchUpOutsideAction), for: .touchUpOutside) sendVoiceBtn.addTarget(self, action: #selector(audioTouchUpInsideAction), for: .touchUpInside) sendVoiceBtn.addTarget(self, action: #selector(audioTouchDragEnterAction), for: .touchDragEnter) sendVoiceBtn.addTarget(self, action: #selector(audioTouchDragExitAction), for: .touchDragExit) } // MARK: - 监听键盘通知 private func registerNotification() { // 监听键盘弹出通知 NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name:UIResponder.keyboardWillShowNotification,object: nil) // 监听键盘隐藏通知 NotificationCenter.default.addObserver(self,selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) // 主要是为了获取点击空白处回收键盘的处理 NotificationCenter.default.addObserver(self,selector: #selector(keyboardNeedHide), name: .kChatTextKeyboardNeedHide, object: nil) // 添加KVO监听输入键盘y值 addObserver(self, forKeyPath: "frame", options: [.new, .old], context: nil) } // MARK: - KVO监听 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "frame" && ((object as? UIView) != nil) { if let changeObject = change.value { if let newFrame = changeObject[.newKey] as? CGRect { delegate?.keyboard(self, DidObserver: newFrame.origin.y) print("y值发生改变\(newFrame.origin.y)") } } }else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } deinit { NIMSDK.shared().mediaManager.remove(self) self.removeObserver(self, forKeyPath: "frame") } } extension ChatKeyboardView: NIMMediaManagerDelegate { func recordAudioInterruptionBegin() { voiceView.cancelAudioRecord() } func recordAudioProgress(_ currentTime: TimeInterval) { voiceView.updateAudioRecordProgress(recordTime: currentTime) } func recordAudio(_ filePath: String?, didCompletedWithError error: Error?) { if let path = filePath { delegate?.keyboard(self, voiceDidFinish: path) } } } // MARK: - ChatMoreMenuViewDelegate extension ChatKeyboardView: ChatMoreMenuViewDelegate { func menu(_ view: ChatMoreMenuView, DidSelected type: ChatMoreMenuType) { delegate?.keyboard(self, DidMoreMenu: type) } } // MARK: - UITextViewDelegate extension ChatKeyboardView: UITextViewDelegate { func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { // 发送键&回车键处理 if (text == "\n") { if (isShowKeyboard) { isShowKeyboard = true } sendChatMessage() return false } return true } /// 发送消息内容 private func sendChatMessage() { delegate?.keyboard(self, DidFinish: self.chatTextView.text ?? "") changeKeyboardHeight(height: lastTextHeight) chatTextView.text = "" chatTextView.attributedText = NSAttributedString(string: "") } } // MARK: - KeyBoard Manager extension ChatKeyboardView { /// 键盘将要显示 @objc func keyboardWillShow(_ noti: NSNotification) { guard let userInfo = noti.userInfo else { return } contentHeight = 0 delegate?.keyboard(self, DidBecome: true) let duration = userInfo["UIKeyboardAnimationDurationUserInfoKey"] as! Double let endFrame = (userInfo["UIKeyboardFrameEndUserInfoKey"] as! NSValue).cgRectValue let y = endFrame.origin.y // 获取键盘的高度 keyboardHeight = endFrame.height // 键盘弹出状态 isShowKeyboard = true let option = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as! Int var changedY = y - self.toolBarHeight - contentHeight if (isShowMore) { //显示系统键盘 isShowMore = false self.moreMenuView.isHidden = true self.moreMenuView.hidePageController = true changedY = y - self.toolBarHeight } UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: UIView.AnimationOptions.RawValue(option)), animations: { self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: self.toolBarHeight + self.contentHeight) }, completion: nil) } /// 键盘将要消失 @objc func keyboardWillHide(_ noti: NSNotification) { guard let userInfo = noti.userInfo else { return } delegate?.keyboard(self, DidBecome: false) let duration = userInfo["UIKeyboardAnimationDurationUserInfoKey"] as! Double let endFrame = (userInfo["UIKeyboardFrameEndUserInfoKey"] as! NSValue).cgRectValue //let y = endFrame.origin.y // 获取键盘的高度 keyboardHeight = endFrame.height // 键盘弹出状态 isShowKeyboard = false let option = userInfo["UIKeyboardAnimationCurveUserInfoKey"] as! Int let changedY = ScreenHeight - self.toolBarHeight - self.contentHeight UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: UIView.AnimationOptions.RawValue(option)), animations: { self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: self.toolBarHeight + self.contentHeight) }, completion: nil) } @objc func keyboardNeedHide(_ noti: NSNotification) { chatTextView.resignFirstResponder() moreMenuView.hidePageController = true contentHeight = 0.0 restToolbarContentHeight(true) delegate?.keyboard(self, DidBecome: false) } } // MARK: - Action extension ChatKeyboardView { @objc func audioTouchDownAction() { sendVoiceBtn.isSelected = true // 开始录音 if voiceView.superview == nil { UIApplication.shared.keyWindow?.addSubview(voiceView) voiceView.translatesAutoresizingMaskIntoConstraints = false voiceView.snp.makeConstraints { make in make.center.equalToSuperview() } voiceView.configAudioRecord(imageName: "chat_voice_record_first", title: "手指上滑,取消发送", isAnimation: true) voiceView.beginAudioRecord() } } @objc func audioTouchUpOutsideAction() { sendVoiceBtn.isSelected = false voiceView.cancelAudioRecord() voiceView.removeFromSuperview() } @objc func audioTouchUpInsideAction() { sendVoiceBtn.isSelected = false voiceView.finishAudioRecord() voiceView.removeFromSuperview() } @objc func audioTouchDragEnterAction() { voiceView.configAudioRecord(imageName: "chat_voice_record_first", title: "手指上滑,取消发送", isAnimation: true) } @objc func audioTouchDragExitAction() { voiceView.configAudioRecord(imageName: "chat_voice_record_cancel", title: "松开手指,取消发送", isAnimation: false) } @objc func changeDidAction(_ button: UIButton) { button.isSelected = !button.isSelected isShowMore = false contentView.isHidden = true moreMenuView.isHidden = true contentHeight = 0.0 restToolbarContentHeight(true) if button.isSelected { chatTextView.resignFirstResponder() sendVoiceBtn.isHidden = false chatTextView.isHidden = true } else { chatTextView.becomeFirstResponder() sendVoiceBtn.isHidden = true chatTextView.isHidden = false } delegate?.keyboard(self, DidBecome: false) } /// 更多按钮点击处理 @objc func moreDidAction(_ button: UIButton) { // 如有弹出菜单 if isShowMore { isShowMore = false contentView.isHidden = true moreMenuView.isHidden = true contentHeight = 0.0 restToolbarContentHeight(true) delegate?.keyboard(self, DidBecome: false) return } isShowMore = true contentHeight = 250 contentView.isHidden = false moreMenuView.isHidden = false chatTextView.resignFirstResponder() restToolbarContentHeight() moreMenuView.reloadData() delegate?.keyboard(self, DidBecome: true) } /// 更改容器高度 func restToolbarContentHeight(_ isRest: Bool = false) { var changedY = ScreenHeight - self.toolBarHeight - contentHeight if (isRest) { if (isShowMore) { isShowMore = false } changedY = ScreenHeight - self.toolBarHeight - contentHeight } UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: { self.contentView.frame = CGRect(x: 0, y: self.toolBarView.maxY, width: ScreenWidth, height: self.contentHeight) self.moreMenuView.frame = self.contentView.bounds self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: self.toolBarHeight + self.contentHeight) }, completion: nil) self.layoutIfNeeded() } } // MARK: - 改变输入框高度位置 extension ChatKeyboardView { func changeKeyboardHeight(_ isClear: Bool = false, height: CGFloat) { let textHeight = height toolBarHeight = textHeight + kSpace * 2 toolBarView.frame = CGRect(x: toolBarView.x, y: 0, width: toolBarView.width, height: toolBarHeight) let spaceY = toolBarView.height - kSpace - kViewWH chatTextView.frame = CGRect(x: chatTextView.x, y: chatTextView.x, width: chatTextView.width, height: textHeight) moreButton.frame = CGRect(x: moreButton.x, y: spaceY, width: moreButton.width, height: moreButton.height) contentView.frame = CGRect(x: contentView.x, y: toolBarView.maxY, width: contentView.width, height: contentHeight) if (isShowKeyboard) { if (isShowMore) { isShowMore = false } let changedY = ScreenHeight - keyboardHeight - toolBarHeight self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: toolBarView.height + contentView.height) }else { let changedY = ScreenHeight - (toolBarView.height + contentView.height) self.frame = CGRect(x: 0, y: changedY, width: ScreenWidth, height: toolBarView.height + contentView.height) } self.setNeedsLayout() print("self y === \(self.frame.origin.y)") } }