400 lines
13 KiB
Swift
400 lines
13 KiB
Swift
//
|
|
// AddEmojiPopView.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
import Lottie
|
|
|
|
final class AddEmojiPopView: UIView {
|
|
|
|
private static let shared = AddEmojiPopView(frame: CGRect(origin: .zero, size: kScreenSize))
|
|
private static let primaryBlue = UIColor(hexStr: "#16B3FF")
|
|
|
|
private let names = InteractionEmojiCell.normalFileNames
|
|
private var selectedName: String?
|
|
private var previewName: String?
|
|
private var lastLayoutWidth: CGFloat = 0
|
|
private var completion: ((String?) -> Void)?
|
|
|
|
private let collectionLayout = CollectionHFlowLayout()
|
|
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionLayout)
|
|
|
|
private let bgView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .black.withAlphaComponent(0.5)
|
|
return view
|
|
}()
|
|
|
|
private let previewView: LottieAnimationView = {
|
|
let view = LottieAnimationView()
|
|
view.contentMode = .scaleAspectFit
|
|
view.loopMode = .loop
|
|
view.backgroundBehavior = .pauseAndRestore
|
|
view.isUserInteractionEnabled = false
|
|
return view
|
|
}()
|
|
|
|
private let infoView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = .white
|
|
view.layer.cornerRadius = 28
|
|
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
view.clipsToBounds = true
|
|
return view
|
|
}()
|
|
|
|
private let titleLab: UILabel = {
|
|
let label = UILabel()
|
|
label.text = "实时表情"
|
|
label.textColor = UIColor(hexStr: "#293445")
|
|
label.font = .systemFont(ofSize: 18, weight: .bold)
|
|
label.textAlignment = .center
|
|
return label
|
|
}()
|
|
|
|
private let pageControl: UIPageControl = {
|
|
let control = UIPageControl()
|
|
control.isUserInteractionEnabled = false
|
|
control.currentPageIndicatorTintColor = UIColor(hexStr: "#16B3FF")
|
|
control.pageIndicatorTintColor = UIColor(hexStr: "#7AD6FF", alpha: 0.4)
|
|
return control
|
|
}()
|
|
|
|
private lazy var cancelBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.setTitle("取消", for: .normal)
|
|
btn.setTitleColor(Self.primaryBlue, for: .normal)
|
|
btn.titleLabel?.font = FontManager.boboBold(18)
|
|
btn.backgroundColor = .white
|
|
btn.cornerRadius = 16
|
|
btn.layer.borderWidth = 1
|
|
btn.layer.borderColor = Self.primaryBlue.cgColor
|
|
btn.addTarget(self, action: #selector(cancelAction), for: .touchUpInside)
|
|
return btn
|
|
}()
|
|
|
|
private lazy var confirmBtn: UIButton = {
|
|
let btn = UIButton(type: .custom)
|
|
btn.setTitle("确定", for: .normal)
|
|
btn.setTitleColor(.white, for: .normal)
|
|
btn.titleLabel?.font = FontManager.boboBold(18)
|
|
btn.backgroundColor = Self.primaryBlue
|
|
btn.cornerRadius = 16
|
|
btn.addTarget(self, action: #selector(confirmAction), for: .touchUpInside)
|
|
return btn
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = .clear
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
updateGridLayout()
|
|
if collectionView.bounds.width > 1, collectionView.contentSize.width < 1, !names.isEmpty {
|
|
collectionLayout.invalidateLayout()
|
|
collectionView.reloadData()
|
|
}
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(bgView)
|
|
addSubview(previewView)
|
|
addSubview(infoView)
|
|
bgView.layoutChain.edges()
|
|
|
|
infoView.addSubview(titleLab)
|
|
infoView.addSubview(collectionView)
|
|
infoView.addSubview(pageControl)
|
|
infoView.addSubview(cancelBtn)
|
|
infoView.addSubview(confirmBtn)
|
|
|
|
infoView.layoutChain
|
|
.bottom()
|
|
.edgesHorzontal()
|
|
|
|
titleLab.layoutChain
|
|
.top(22)
|
|
.edgesHorzontal(20)
|
|
.height(24)
|
|
|
|
cancelBtn.layoutChain
|
|
.left(20)
|
|
.bottom(20 + kSafeBottomMargin)
|
|
.height(44)
|
|
.widthToView(confirmBtn)
|
|
|
|
confirmBtn.layoutChain
|
|
.leftToRightOfView(cancelBtn, offset: 12)
|
|
.right(20)
|
|
.bottomToView(cancelBtn)
|
|
.height(44)
|
|
|
|
pageControl.layoutChain
|
|
.centerX()
|
|
.bottomToTopOfView(cancelBtn, offset: -10)
|
|
.height(16)
|
|
|
|
collectionLayout.rows = 3
|
|
collectionLayout.colums = 4
|
|
applyGridLayout(for: kScreenWidth - 32)
|
|
collectionView.backgroundColor = .clear
|
|
collectionView.isPagingEnabled = true
|
|
collectionView.bounces = false
|
|
collectionView.showsHorizontalScrollIndicator = false
|
|
collectionView.dataSource = self
|
|
collectionView.delegate = self
|
|
collectionView.register(AddEmojiCell.self)
|
|
collectionView.layoutChain
|
|
.topToBottomOfView(titleLab, offset: 16)
|
|
.edgesHorzontal(16)
|
|
.height(236)
|
|
.bottomToTopOfView(pageControl, offset: -4)
|
|
|
|
previewView.layoutChain
|
|
.centerX()
|
|
.bottomToTopOfView(infoView, offset: -36)
|
|
.width(148)
|
|
.height(148)
|
|
|
|
let tap = UITapGestureRecognizer(target: self, action: #selector(cancelAction))
|
|
tap.delegate = self
|
|
addGestureRecognizer(tap)
|
|
}
|
|
|
|
private func resetSelection() {
|
|
selectedName = names.first
|
|
pageControl.numberOfPages = max((names.count + 11) / 12, 1)
|
|
pageControl.currentPage = 0
|
|
lastLayoutWidth = 0
|
|
reloadGrid()
|
|
updatePreview(with: selectedName)
|
|
}
|
|
|
|
private func reloadGrid() {
|
|
updateGridLayout()
|
|
collectionLayout.invalidateLayout()
|
|
collectionView.reloadData()
|
|
collectionView.layoutIfNeeded()
|
|
collectionView.setContentOffset(.zero, animated: false)
|
|
}
|
|
|
|
private func updateGridLayout() {
|
|
let collectionWidth = collectionView.bounds.width
|
|
let width = collectionWidth > 1 ? collectionWidth : max(bounds.width - 32, kScreenWidth - 32)
|
|
guard width > 1 else { return }
|
|
applyGridLayout(for: width)
|
|
if collectionWidth > 1 {
|
|
lastLayoutWidth = collectionWidth
|
|
}
|
|
}
|
|
|
|
private func applyGridLayout(for width: CGFloat) {
|
|
let columns: CGFloat = 4
|
|
let rows: CGFloat = 3
|
|
let inset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
|
|
let hSpacing: CGFloat = 20
|
|
let vSpacing: CGFloat = 10
|
|
let itemWidth = floor((width - inset.left - inset.right - hSpacing * (columns - 1)) / columns)
|
|
let collectionHeight = collectionView.bounds.height > 1 ? collectionView.bounds.height : 236
|
|
let itemHeight = floor((collectionHeight - inset.top - inset.bottom - vSpacing * (rows - 1)) / rows)
|
|
collectionLayout.itemSize = CGSize(width: max(itemWidth, 1), height: max(itemHeight, 1))
|
|
collectionLayout.hSpacing = hSpacing
|
|
collectionLayout.vSpacing = vSpacing
|
|
collectionLayout.sectionInset = inset
|
|
}
|
|
|
|
private func updatePreview(with name: String?) {
|
|
guard let name else {
|
|
previewName = nil
|
|
previewView.stop()
|
|
previewView.animation = nil
|
|
return
|
|
}
|
|
if previewName == name, previewView.animation != nil {
|
|
if !previewView.isAnimationPlaying {
|
|
previewView.play()
|
|
}
|
|
return
|
|
}
|
|
previewName = name
|
|
previewView.stop()
|
|
previewView.animation = nil
|
|
EmojiPanelCell.loadAnimation(for: name) { [weak self] animation in
|
|
guard let self, self.previewName == name else { return }
|
|
self.previewView.animation = animation
|
|
self.previewView.currentProgress = 0
|
|
self.previewView.play()
|
|
}
|
|
}
|
|
|
|
private func stopPreview() {
|
|
previewName = nil
|
|
previewView.stop()
|
|
previewView.animation = nil
|
|
}
|
|
|
|
private func slideOffTransform() -> CGAffineTransform {
|
|
layoutIfNeeded()
|
|
return CGAffineTransform(translationX: 0, y: max(infoView.bounds.height, kScreenHeight * 0.4))
|
|
}
|
|
|
|
@objc private func cancelAction() {
|
|
completion?(nil)
|
|
}
|
|
|
|
@objc private func confirmAction() {
|
|
completion?(selectedName)
|
|
}
|
|
}
|
|
|
|
extension AddEmojiPopView {
|
|
static func show(completion: @escaping (String?) -> Void) {
|
|
guard let superView = kKeyWindow else { return }
|
|
let pop = AddEmojiPopView.shared
|
|
if pop.superview != nil {
|
|
pop.removeFromSuperview()
|
|
}
|
|
pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
|
pop.completion = { name in
|
|
completion(name)
|
|
AddEmojiPopView.dismiss()
|
|
}
|
|
pop.bgView.alpha = 0
|
|
pop.previewView.alpha = 0
|
|
superView.addSubview(pop)
|
|
superView.bringSubviewToFront(pop)
|
|
pop.setNeedsLayout()
|
|
pop.layoutIfNeeded()
|
|
pop.resetSelection()
|
|
pop.infoView.transform = pop.slideOffTransform()
|
|
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
|
pop.bgView.alpha = 1
|
|
pop.previewView.alpha = 1
|
|
pop.infoView.transform = .identity
|
|
}
|
|
DispatchQueue.main.async {
|
|
pop.reloadGrid()
|
|
}
|
|
}
|
|
|
|
static func dismiss() {
|
|
let pop = AddEmojiPopView.shared
|
|
guard pop.superview != nil else { return }
|
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
|
|
pop.bgView.alpha = 0
|
|
pop.previewView.alpha = 0
|
|
pop.infoView.transform = pop.slideOffTransform()
|
|
} completion: { _ in
|
|
pop.stopPreview()
|
|
pop.infoView.transform = .identity
|
|
pop.removeFromSuperview()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension AddEmojiPopView: UIGestureRecognizerDelegate {
|
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
touch.view == self || touch.view == bgView
|
|
}
|
|
}
|
|
|
|
extension AddEmojiPopView: UICollectionViewDataSource, UICollectionViewDelegate {
|
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
names.count
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
let cell: AddEmojiCell = collectionView.dequeueReusableCell(for: indexPath)
|
|
let name = names[indexPath.item]
|
|
cell.configure(image: InteractionEmojiCell.pngImage(named: name), selected: name == selectedName)
|
|
return cell
|
|
}
|
|
|
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
selectedName = names[indexPath.item]
|
|
collectionView.reloadData()
|
|
updatePreview(with: selectedName)
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
guard scrollView.bounds.width > 0 else { return }
|
|
pageControl.currentPage = Int(round(scrollView.contentOffset.x / scrollView.bounds.width))
|
|
}
|
|
}
|
|
|
|
final class AddEmojiCell: UICollectionViewCell {
|
|
private let imageView = UIImageView()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
contentView.backgroundColor = UIColor(hexStr: "#F7F8FA")
|
|
contentView.layer.cornerRadius = 16
|
|
contentView.layer.borderWidth = 2
|
|
contentView.layer.borderColor = UIColor.clear.cgColor
|
|
contentView.clipsToBounds = true
|
|
imageView.contentMode = .scaleAspectFit
|
|
contentView.addSubview(imageView)
|
|
imageView.layoutChain.edges(all: 6)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func configure(image: UIImage?, selected: Bool) {
|
|
imageView.image = image
|
|
contentView.layer.borderColor = selected ? UIColor(hexStr: "#16B3FF").cgColor : UIColor.clear.cgColor
|
|
contentView.backgroundColor = selected ? UIColor(hexStr: "#F3FBFF") : UIColor(hexStr: "#F7F8FA")
|
|
}
|
|
}
|
|
|
|
|
|
final class ReceivedEmojiPopView {
|
|
|
|
private static let emojiView: LottieAnimationView = {
|
|
let view = LottieAnimationView()
|
|
view.contentMode = .scaleAspectFit
|
|
view.loopMode = .playOnce
|
|
view.backgroundBehavior = .pauseAndRestore
|
|
view.isUserInteractionEnabled = false
|
|
view.isHidden = true
|
|
return view
|
|
}()
|
|
|
|
static func show(name: String) {
|
|
guard let window = kKeyWindow else { return }
|
|
let view = emojiView
|
|
if view.superview != window {
|
|
view.removeFromSuperview()
|
|
window.addSubview(view)
|
|
view.layoutChain
|
|
.centerX()
|
|
.centerY()
|
|
.width(180)
|
|
.height(180)
|
|
}
|
|
window.bringSubviewToFront(view)
|
|
view.stop()
|
|
view.animation = nil
|
|
view.isHidden = false
|
|
EmojiPanelCell.loadAnimation(for: name) { animation in
|
|
guard view.superview != nil else { return }
|
|
view.animation = animation
|
|
view.currentProgress = 0
|
|
view.play { finished in
|
|
guard finished else { return }
|
|
view.isHidden = true
|
|
view.animation = nil
|
|
}
|
|
}
|
|
}
|
|
}
|