jsdw_ios/QuickLocation/Section/Home/AddEmojiPopView.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
}
}
}
}