提交
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user