增加换肤功能

This commit is contained in:
启星
2025-08-14 10:07:49 +08:00
parent f6964c1e89
commit 4f9318d98e
8789 changed files with 978530 additions and 2 deletions

View File

@@ -0,0 +1,280 @@
//
// FloatChatInputController.swift
// TUIRoomKit
//
// Created by CY zhao on 2024/5/11.
// Copyright © 2024 Tencent. All rights reserved.
//
import UIKit
import SnapKit
import Foundation
import TUICore
import Factory
class FloatChatInputController: UIViewController {
@Injected(\.floatChatService) private var store: FloatChatStoreProvider
@Injected(\.conferenceStore) private var operation
private var textViewBottomConstraint: Constraint?
private var textViewHeightConstraint: Constraint?
private var emojiPanelTopConstraint: Constraint?
private let maxNumberOfLines = 3
private let emojiPanelHeight = 274.0
private let inputBarView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.tui_color(withHex: "#22262E")
return view
}()
private let emojiButton: LargeTapAreaButton = {
let button = LargeTapAreaButton()
let img = UIImage(named: "room_emoji_icon", in: tuiRoomKitBundle(), compatibleWith: nil)
button.setImage(img, for: .normal)
return button
}()
private let inputTextView: UITextView = {
let view = UITextView(frame: .zero)
view.font = UIFont.systemFont(ofSize: 17.5)
view.returnKeyType = UIReturnKeyType.send
view.enablesReturnKeyAutomatically = true
view.textContainer.lineBreakMode = .byCharWrapping
view.textContainerInset = UIEdgeInsets(top: view.textContainerInset.top, left: 10, bottom: view.textContainerInset.bottom, right: 10)
view.textContainer.lineFragmentPadding = 0
view.layer.cornerRadius = view.sizeThatFits(.zero).height / 2
view.layer.masksToBounds = true
view.isHidden = true
view.textColor = UIColor.tui_color(withHex: "#D5F4F2", alpha: 0.6)
view.backgroundColor = UIColor.tui_color(withHex: "#4F586B", alpha: 0.3)
return view
}()
private let sendButton: UIButton = {
let button = UIButton()
button.setTitle(.sendText, for: .normal)
button.layer.cornerRadius = 18
button.backgroundColor = UIColor.tui_color(withHex: "#006CFF")
return button
}()
private let backgroundView: UIView = {
let view = UITextView(frame: .zero)
view.backgroundColor = UIColor.tui_color(withHex: "#22262E")
return view
}()
private lazy var emojiPanel: EmotionBoardView = {
let emotionBoardView = EmotionBoardView()
let emotionHelper = EmotionHelper.shared
emotionBoardView.emotions = emotionHelper.emotions
emotionBoardView.delegate = self
emotionBoardView.backgroundColor = UIColor.tui_color(withHex: "#22262E")
emotionBoardView.isHidden = true
return emotionBoardView
}()
private lazy var maxHeightOfTextView: CGFloat = {
let lineHeight = inputTextView.font?.lineHeight ?? 0
return ceil(lineHeight * CGFloat(maxNumberOfLines) + inputTextView.textContainerInset.top + inputTextView.textContainerInset.bottom)
}()
override func viewDidLoad() {
super.viewDidLoad()
constructViewHierarchy()
activateConstraints()
bindInteraction()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
showInputView()
}
private func constructViewHierarchy() {
inputBarView.addSubview(emojiButton)
inputBarView.addSubview(inputTextView)
inputBarView.addSubview(sendButton)
view.addSubview(backgroundView)
view.addSubview(inputBarView)
view.addSubview(emojiPanel)
}
private func activateConstraints() {
backgroundView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.bottom.equalToSuperview()
make.top.equalTo(inputBarView.snp.top)
}
inputBarView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.height.equalTo(inputTextView).offset(2 * 12)
textViewBottomConstraint = make.bottom.equalTo(view).constraint
}
emojiButton.snp.makeConstraints { make in
make.width.height.equalTo(24)
make.centerY.equalToSuperview()
make.leading.equalTo(view.safeAreaLayoutGuide.snp.leading).offset(10)
}
sendButton.snp.makeConstraints { make in
make.width.equalTo(64)
make.height.equalTo(36)
make.centerY.equalToSuperview()
make.trailing.equalTo(view.safeAreaLayoutGuide.snp.trailing).offset(-10)
}
inputTextView.snp.makeConstraints { make in
make.leading.equalTo(emojiButton.snp.trailing).offset(10)
make.trailing.equalTo(sendButton.snp.leading).offset(-10)
let size = inputTextView.sizeThatFits(.zero)
textViewHeightConstraint = make.height.equalTo(size.height).constraint
make.centerY.equalToSuperview()
}
emojiPanel.snp.makeConstraints { make in
make.leading.trailing.equalTo(view.safeAreaLayoutGuide)
make.height.equalTo(emojiPanelHeight)
emojiPanelTopConstraint = make.top.equalTo(view.snp.bottom).constraint
}
}
private func bindInteraction() {
inputTextView.delegate = self
emojiButton.addTarget(self, action: #selector(onEmojiButtonTapped), for: .touchUpInside)
sendButton.addTarget(self, action: #selector(onSendButtonTapped), for: .touchUpInside)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(hideInputView))
view.addGestureRecognizer(tapGesture)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil)
}
@objc private func keyboardWillShow(notification: NSNotification) {
guard let keyboardRect: CGRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
let curve: UInt = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt,
let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
else {
return
}
let intersection = CGRectIntersection(keyboardRect, self.view.frame)
UIView.animate(withDuration: duration, delay: 0.0, options: UIView.AnimationOptions(rawValue: curve)) { [weak self] in
guard let self = self else { return }
self.textViewBottomConstraint?.update(offset: -CGRectGetHeight(intersection))
}
}
@objc private func onSendButtonTapped(sender: UIButton) {
if inputTextView.normalText.isEmpty {
operation.dispatch(action: ViewActions.showToast(payload: ToastInfo(message: .inputCannotBeEmpty, position: .center)))
} else {
store.dispatch(action: FloatChatActions.sendMessage(payload: inputTextView.normalText))
}
hideInputView()
}
private func showInputView() {
inputTextView.isHidden = false
inputTextView.becomeFirstResponder()
}
@objc private func hideInputView() {
inputBarView.isHidden = true
view.endEditing(true)
store.dispatch(action: FloatViewActions.showFloatInputView(payload: false))
}
@objc private func onEmojiButtonTapped(sender: UIButton) {
sender.isSelected = !sender.isSelected
if sender.isSelected {
showEmojiPanel()
} else {
hideEmojiPanel()
}
}
private func showEmojiPanel() {
inputTextView.resignFirstResponder()
emojiPanel.isHidden = false
UIView.animate(withDuration: 0.2, delay: 0, options: UIView.AnimationOptions.curveEaseInOut) { [weak self] in
guard let self = self else { return }
self.emojiPanelTopConstraint?.update(offset: -self.emojiPanelHeight)
self.textViewBottomConstraint?.update(offset: -self.emojiPanelHeight)
}
}
private func hideEmojiPanel() {
UIView.animate(withDuration: 0.2, delay: 0, options: UIView.AnimationOptions.curveEaseInOut) { [weak self] in
guard let self = self else { return }
self.emojiPanelTopConstraint?.update(offset: self.emojiPanelHeight)
} completion: {[weak self] _ in
guard let self = self else { return }
self.emojiPanel.isHidden = true
self.inputTextView.becomeFirstResponder()
}
}
private func updateTextViewHeight() {
let currentHeight = ceil(inputTextView.sizeThatFits(CGSize(width: inputTextView.bounds.size.width, height: CGFloat.greatestFiniteMagnitude)).height)
inputTextView.isScrollEnabled = currentHeight > maxHeightOfTextView
if currentHeight <= maxHeightOfTextView {
textViewHeightConstraint?.update(offset: currentHeight)
}
}
}
extension FloatChatInputController: UITextViewDelegate {
func textViewDidBeginEditing(_ textView: UITextView) {
inputTextView.becomeFirstResponder()
}
func textViewDidChange(_ textView: UITextView) {
updateTextViewHeight()
}
func textViewDidEndEditing(_ textView: UITextView) {
inputTextView.resignFirstResponder()
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n" {
store.dispatch(action: FloatChatActions.sendMessage(payload: textView.normalText))
hideInputView()
return false
}
return true
}
}
extension FloatChatInputController: EmotionBoardViewDelegate {
func emotionView(emotionBoardView: EmotionBoardView, didSelectEmotion emotion: Emotion, atIndex index: Int) {
let attributedString = EmotionHelper.shared.obtainImageAttributedString(byImageKey: emotion.displayName,
font: inputTextView.font ?? UIFont(), useCache: false)
inputTextView.insertEmotionAttributedString(emotionAttributedString: attributedString)
}
func emotionViewDidSelectDeleteButton(emotionBoardView: EmotionBoardView) {
if !inputTextView.deleteEmotion() {
inputTextView.deleteBackward()
}
}
}
class LargeTapAreaButton: UIButton {
var tapAreaPadding: CGFloat = 20
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let largerBounds = bounds.insetBy(dx: -tapAreaPadding, dy: -tapAreaPadding)
return largerBounds.contains(point)
}
}
private extension String {
static var sendText: String {
localized("Send")
}
static let inputCannotBeEmpty = localized("Input can't be empty!")
}