| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- //
- // EmotionBoardView.swift
- // TUILiveKit
- //
- // Created by krabyu on 2024/4/7.
- //
- import Foundation
- import UIKit
- protocol EmotionBoardViewDelegate: AnyObject {
- func emotionView(emotionBoardView: EmotionBoardView, didSelectEmotion emotion: Emotion, atIndex index: Int)
- func emotionViewDidSelectDeleteButton(emotionBoardView: EmotionBoardView)
- }
- protocol EmotionPageViewDelegate: AnyObject {
- func emotionPageView(emotionPageView: EmotionPageView, didSelectEmotion emotion: Emotion, atIndex index: Int)
- func emotionPageViewDidLayoutEmotions(emotionPageView: EmotionPageView)
- }
- class EmotionPageView: UIView {
- private let buttonWidth = 35
- private let buttonHeight = 30
- weak var delegate: EmotionPageViewDelegate?
- let emotionSelectedBackgroundView: UIView = {
- let view = UIView()
- view.isUserInteractionEnabled = false
- view.backgroundColor = UIColor(red: 0 / 255.0, green: 0 / 255.0, blue: 0 / 255.0, alpha: 0.16)
- view.layer.cornerRadius = 3
- view.alpha = 0
- return view
- }()
- var deleteButton: UIButton = UIButton()
- var deleteButtonOffset: CGPoint = CGPointZero
- var emotionLayers: [CALayer] = []
- var emotions: [Emotion] = []
- // Record the rect of the clickable area of all emoticons in the current pageView, updated in drawRect: and used in tap events
- var emotionHittingRects: [NSValue] = []
- var padding = UIEdgeInsets()
- var numberOfRows: Int = 4
- // The size of the drawing area for each expression
- var emotionSize = CGSize(width: 30, height: 30)
- var emotionSelectedBackgroundExtension = UIEdgeInsets()
- var minimumEmotionHorizontalSpacing: CGFloat = 16
- var needsLayoutEmotions: Bool = true
- var previousLayoutFrame = CGRect()
- override init(frame: CGRect) {
- super.init(frame: frame)
- backgroundColor = UIColor(red: 34 / 255.0, green: 38 / 255.0, blue: 46 / 255.0, alpha: 1)
- addSubview(emotionSelectedBackgroundView)
- let tap = UITapGestureRecognizer(target: self, action: #selector(handleTapGestureRecognizer))
- addGestureRecognizer(tap)
- }
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- private func frameForDeleteButton(deleteButton: UIView) -> CGRect {
- var rect = deleteButton.frame
- let x = CGRectGetWidth(bounds) - padding.right - CGRectGetWidth(deleteButton.frame) -
- (emotionSize.width - CGRectGetWidth(deleteButton.frame)) / 2.0 + deleteButtonOffset.x
- let y = CGRectGetHeight(bounds) - padding.bottom - CGRectGetHeight(deleteButton.frame) -
- (emotionSize.height - CGRectGetHeight(deleteButton.frame)) / 2.0 + deleteButtonOffset.y
- rect.origin = CGPoint(x: x, y: y)
- return rect
- }
- override func layoutSubviews() {
- super.layoutSubviews()
- if deleteButton.superview == self {
- // The Delete button must be placed in the position of the last expression,
- // and is centered left and right above and below the expression
- deleteButton.frame = frameForDeleteButton(deleteButton: deleteButton)
- }
- let isSizeChanged = !CGSizeEqualToSize(previousLayoutFrame.size, frame.size)
- previousLayoutFrame = frame
- if isSizeChanged {
- setNeedsLayoutEmotions()
- }
- layoutEmotionsIfNeeded()
- }
- func setNeedsLayoutEmotions() {
- needsLayoutEmotions = true
- }
- func setEmotions(emotions: [Emotion]) {
- if self.emotions == emotions { return }
- self.emotions = emotions
- setNeedsLayoutEmotions()
- setNeedsLayout()
- }
- func layoutEmotionsIfNeeded() {
- if !needsLayoutEmotions { return }
- needsLayoutEmotions = false
- emotionHittingRects.removeAll()
- let contentSize = bounds.inset(by: padding).size
- let emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing)
- let emotionHorizontalSpacing = (contentSize.width - emotionCountPerRow * emotionSize.width) / (emotionCountPerRow - 1)
- let emotionVerticalSpacing = Int(contentSize.height - CGFloat(numberOfRows) * emotionSize.height) / Int(numberOfRows - 1)
- emotionSelectedBackgroundExtension = UIEdgeInsets(top: CGFloat(-emotionVerticalSpacing) / 2,
- left: -emotionHorizontalSpacing / 2,
- bottom: CGFloat(-emotionVerticalSpacing) / 2,
- right: -emotionHorizontalSpacing / 2)
- var emotionOrigin = CGPointZero
- let emotionCount = emotions.count
- for i in stride(from: 0, to: emotionCount, by: 1) {
- var emotionLayer: CALayer
- if i < emotionLayers.count {
- emotionLayer = emotionLayers[i]
- } else {
- emotionLayer = CALayer()
- emotionLayer.contentsScale = UIScreen.main.scale
- emotionLayers.append(emotionLayer)
- layer.addSublayer(emotionLayer)
- }
- emotionLayer.contents = emotions[i].image.cgImage
- let row = i / Int(emotionCountPerRow)
- emotionOrigin.x = padding.left + (emotionSize.width + emotionHorizontalSpacing) * CGFloat(i % Int(emotionCountPerRow))
- emotionOrigin.y = padding.top + (emotionSize.height + CGFloat(emotionVerticalSpacing)) * CGFloat(row)
- let emotionRect = CGRect(x: emotionOrigin.x, y: emotionOrigin.y, width: emotionSize.width, height: emotionSize.height)
- let emotionHittingRect = emotionRect.inset(by: emotionSelectedBackgroundExtension)
- emotionHittingRects.append(NSValue(cgRect: emotionHittingRect))
- emotionLayer.frame = emotionRect
- emotionLayer.isHidden = false
- }
- if emotionLayers.count > emotionCount {
- for i in emotionLayers.count - emotionCount ..< emotionLayers.count {
- emotionLayers[i].isHidden = true
- }
- }
- delegate?.emotionPageViewDidLayoutEmotions(emotionPageView: self)
- }
- @objc func handleTapGestureRecognizer(_ gestureRecognizer: UITapGestureRecognizer) {
- let location = gestureRecognizer.location(in: self)
- for i in 0 ..< emotionHittingRects.count {
- let rect = emotionHittingRects[i].cgRectValue
- if rect.contains(location) {
- let layer = emotionLayers[i]
- if layer.opacity < 0.2 { return }
- let emotion = emotions[i]
- emotionSelectedBackgroundView.frame = rect
- UIView.animate(withDuration: 0.08, animations: { [weak self] in
- guard let self = self else { return }
- self.emotionSelectedBackgroundView.alpha = 1
- }, completion: { [weak self] _ in
- guard let self = self else { return }
- UIView.animate(withDuration: 0.08, animations: {
- self.emotionSelectedBackgroundView.alpha = 0
- }, completion: nil)
- })
- delegate?.emotionPageView(emotionPageView: self, didSelectEmotion: emotion, atIndex: i)
- return
- }
- }
- }
- func verticalSizeThatFits(size: CGSize, emotionVerticalSpacing: CGFloat) -> CGSize {
- let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
- let contentSize = rect.inset(by: padding).size
- let emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing)
- let row = ceil(CGFloat(emotions.count) / (emotionCountPerRow * 1.0))
- let height = (emotionSize.height + emotionVerticalSpacing) * row - emotionVerticalSpacing + (padding.top + padding.bottom)
- return CGSize(width: size.width, height: height)
- }
- func updateDeleteButton(deleteButton: UIButton) {
- self.deleteButton = deleteButton
- addSubview(deleteButton)
- }
- func setDeleteButtonOffset(deleteButtonOffset: CGPoint) {
- self.deleteButtonOffset = deleteButtonOffset
- setNeedsLayout()
- }
- }
- class EmotionVerticalScrollView: UIScrollView {
- let pageView: EmotionPageView = {
- let pageView = EmotionPageView()
- pageView.deleteButton.isHidden = true
- return pageView
- }()
- override init(frame: CGRect) {
- super.init(frame: frame)
- addSubview(pageView)
- }
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
- func setEmotions(emotions: [Emotion],
- emotionSize: CGSize,
- minimumEmotionHorizontalSpacing: CGFloat,
- emotionVerticalSpacing: CGFloat,
- emotionSelectedBackgroundExtension: UIEdgeInsets,
- paddingInPage: UIEdgeInsets) {
- let pageView = self.pageView
- pageView.emotions = emotions
- pageView.padding = paddingInPage
- let contentSize = CGSize(width: bounds.size.width - edgeInsetsGetHorizontalValue(insets: paddingInPage),
- height: bounds.size.height - edgeInsetsGetVerticalValue(insets: paddingInPage))
- let emotionCountPerRow = (contentSize.width + minimumEmotionHorizontalSpacing) / (emotionSize.width + minimumEmotionHorizontalSpacing)
- pageView.numberOfRows = Int(ceil(CGFloat(emotions.count) / emotionCountPerRow))
- pageView.emotionSize = emotionSize
- pageView.emotionSelectedBackgroundExtension = emotionSelectedBackgroundExtension
- pageView.minimumEmotionHorizontalSpacing = minimumEmotionHorizontalSpacing
- pageView.setNeedsLayout()
- let size = pageView.verticalSizeThatFits(size: bounds.size, emotionVerticalSpacing: emotionVerticalSpacing)
- self.pageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
- self.contentSize = size
- }
- func adjustEmotionsAlpha(withFloatingRect floatingRect: CGRect) {
- let contentSize = CGSize(width: contentSize.width - edgeInsetsGetHorizontalValue(insets: pageView.padding),
- height: contentSize.height - edgeInsetsGetVerticalValue(insets: pageView.padding))
- let emotionCountPerRow = (contentSize.width + pageView.minimumEmotionHorizontalSpacing) /
- ((pageView.emotionSize.width) + (pageView.minimumEmotionHorizontalSpacing))
- let emotionVerticalSpacing = Int(contentSize.height - CGFloat(pageView.numberOfRows) *
- (pageView.emotionSize.height)) / Int(CGFloat(pageView.numberOfRows) - 1)
- let emotionHorizontalSpacing = (contentSize.width - emotionCountPerRow * (pageView.emotionSize.width)) / (emotionCountPerRow - 1)
- let columnIndexLeft = ceil((floatingRect.origin.x - (pageView.padding.left)) / ((pageView.emotionSize.width) +
- emotionHorizontalSpacing)) - 1
- let columnIndexRight = emotionCountPerRow - 1
- let rowIndexTop = ((floatingRect.origin.y - (pageView.padding.top)) / ((pageView.emotionSize.height) +
- CGFloat(emotionVerticalSpacing))) - 1
- for i in 0 ..< pageView.emotionLayers.count {
- let row = i / Int(emotionCountPerRow)
- let column = i % Int(emotionCountPerRow)
- CATransaction.begin()
- CATransaction.setDisableActions(true)
- if column >= Int(columnIndexLeft) && column <= Int(columnIndexRight) && row > Int(rowIndexTop) {
- if row == Int(ceil(rowIndexTop)) {
- let intersectAreaHeight = floatingRect.origin.y - pageView.emotionLayers[i].frame.origin.y
- let percent = intersectAreaHeight / pageView.emotionSize.height
- pageView.emotionLayers[i].opacity = Float(percent * percent)
- } else {
- pageView.emotionLayers[i].opacity = 0
- }
- } else {
- pageView.emotionLayers[i].opacity = 1.0
- }
- CATransaction.commit()
- }
- }
- func edgeInsetsGetVerticalValue(insets: UIEdgeInsets) -> CGFloat {
- return insets.top + insets.bottom
- }
- func edgeInsetsGetHorizontalValue(insets: UIEdgeInsets) -> CGFloat {
- return insets.left + insets.right
- }
- }
- class EmotionBoardView: UIView {
- private let buttonWidth = 35
- private let buttonHeight = 30
- var emotions: [Emotion] = []
- weak var delegate: EmotionBoardViewDelegate?
- var deleteButtonMargins = UIEdgeInsets(top: 0, left: 0, bottom: 18, right: 18)
- var pagedEmotions: [Emotion] = []
- let emotionVerticalSpacing = 16
- let paddingInPage = UIEdgeInsets(top: 18, left: 18, bottom: 65, right: 18)
- let numberOfRowsPerPage: Int = 4
- let emotionSize = CGSize(width: 34, height: 34)
- let emotionSelectedBackgroundExtension = UIEdgeInsets(top: -3, left: -3, bottom: -3, right: 03)
- let minimumEmotionHorizontalSpacing: CGFloat = 16
- let deleteButtonOffset: CGPoint = CGPointZero
- let pageControlMarginBottom: CGFloat = 22
- lazy var verticalScrollView: EmotionVerticalScrollView = {
- let scrollView = EmotionVerticalScrollView()
- scrollView.contentInsetAdjustmentBehavior = .never
- scrollView.delegate = self
- return scrollView
- }()
- lazy var deleteButton: UIButton = {
- let button = UIButton()
- button.setImage(UIImage(named: "room_floatchat_delete", in: tuiRoomKitBundle(), compatibleWith: nil), for: .normal)
- button.addTarget(self, action: #selector(didSelectDeleteButton), for: .touchUpInside)
- button.layer.cornerRadius = 4
- button.backgroundColor = .white
- return button
- }()
- lazy var topLineView: UIView = {
- let view = UIView()
- view.frame = CGRect(x: 0, y: 0, width: CGRectGetWidth(bounds), height: 1 / UIScreen.main.scale)
- view.backgroundColor = UIColor(red: 34 / 255.0, green: 38 / 255.0, blue: 46 / 255.0, alpha: 1)
- return view
- }()
- override init(frame: CGRect) {
- super.init(frame: frame)
- didInitialized(withFrame: frame)
- }
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- didInitialized(withFrame: CGRectZero)
- }
- func didInitialized(withFrame frame: CGRect) {
- addSubview(verticalScrollView)
- addSubview(deleteButton)
- addSubview(topLineView)
- }
- func setEmotions(emotions: [Emotion]) {
- self.emotions = emotions
- setNeedsLayout()
- }
- override func layoutSubviews() {
- super.layoutSubviews()
- deleteButton.frame = CGRect(x: Int(Double(Int(bounds.width) - Int(deleteButtonMargins.right) - buttonWidth)),
- y: Int(Double(bounds.height - safeAreaInsets.bottom) - deleteButtonMargins.bottom) - buttonHeight,
- width: buttonWidth, height: buttonHeight)
- var paddingInPage = paddingInPage
- paddingInPage.bottom = paddingInPage.bottom + safeAreaInsets.bottom
- let verticalScrollViewFrame = bounds.inset(by: .zero)
- verticalScrollView.frame = verticalScrollViewFrame
- verticalScrollView.setEmotions(emotions: emotions,
- emotionSize: emotionSize,
- minimumEmotionHorizontalSpacing: minimumEmotionHorizontalSpacing,
- emotionVerticalSpacing: CGFloat(emotionVerticalSpacing),
- emotionSelectedBackgroundExtension: emotionSelectedBackgroundExtension,
- paddingInPage: paddingInPage)
- verticalScrollView.pageView.delegate = self
- topLineView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 1 / UIScreen.main.scale)
- }
- func adjustEmotionsAlpha() {
- let x = deleteButton.frame.origin.x
- let y = deleteButton.frame.origin.y
- let width = deleteButton.frame.maxX
- let height = deleteButton.frame.maxY - deleteButton.frame.minY
- let buttonGroupRect = CGRect(x: x, y: y, width: width, height: height)
- let floatingRect = verticalScrollView.convert(buttonGroupRect, from: self)
- verticalScrollView.adjustEmotionsAlpha(withFloatingRect: floatingRect)
- }
- @objc func didSelectDeleteButton() {
- delegate?.emotionViewDidSelectDeleteButton(emotionBoardView: self)
- }
- }
- extension EmotionBoardView: EmotionPageViewDelegate {
- func emotionPageView(emotionPageView: EmotionPageView, didSelectEmotion emotion: Emotion, atIndex index: Int) {
- let index = emotions.firstIndex(of: emotion) ?? -1
- delegate?.emotionView(emotionBoardView: self, didSelectEmotion: emotion, atIndex: index)
- }
- func emotionPageViewDidLayoutEmotions(emotionPageView: EmotionPageView) {
- adjustEmotionsAlpha()
- }
- }
- extension EmotionBoardView: UIScrollViewDelegate {
- func scrollViewDidScroll(_ scrollView: UIScrollView) {
- if scrollView == verticalScrollView {
- adjustEmotionsAlpha()
- }
- }
- }
|