472 lines
15 KiB
Swift
472 lines
15 KiB
Swift
//
|
|
// 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)")
|
|
}
|
|
}
|
|
|
|
|