// // 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 } } } }