jsdw_ios/QuickLocation/Section/Home/InteractionView.swift

656 lines
20 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// InteractionView.swift
// QuickLocation
//
// Created by on 2026/6/15.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import Lottie
class InteractionView: UIView {
var disposeBag = DisposeBag()
private static let emojiCols = 4
private static let emojiRows = 3
private static let emojiPerPage = emojiCols * emojiRows
func configure(member: CircleMember) {
self.currentMember = member
avaterImgView.image = UIImage(named: "UserIcon/\(member.avatar)")
nameLab.text = member.name
locationLab.text = member.address
//
if AppContextManager.shared.vip > 1 {
batteryInfoView.isHidden = member.battery.int == 0
//
let batteryInt = Int(member.battery) ?? 0
let batteryPercent = min(CGFloat(batteryInt), 100)
batteryView.layoutChain.width(CGFloat(16 - 1) * batteryPercent / 100.0)
batteryLab.text = "\(member.battery)%"
//
lockView.isHidden = true
}
else {
batteryInfoView.isHidden = true
lockView.isHidden = false
}
}
/// normal / fun
private let emojiRelay = BehaviorRelay<[String]>(value: UIView.emojiFileNames)
private var emojiPlaybackGeneration = 0
private var isShowingFunEmoji = false
private func setupRx() {
//
emojiNormalBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.switchEmojiCategory(isFun: false)
})
.disposed(by: disposeBag)
emojiFunBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.switchEmojiCategory(isFun: true)
})
.disposed(by: disposeBag)
navigateBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.onNavigate?()
})
.disposed(by: disposeBag)
shareBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.onShare?()
})
.disposed(by: disposeBag)
// collectionView
emojiRelay
.map { [SectionModel(model: "", items: $0)] }
.bind(to: emojiCollectionView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
//
emojiCollectionView.rx.modelSelected(String.self)
.subscribe(onNext: { [weak self] name in
guard let self = self else { return }
// index: normal_5 15, fun_2 22
let prefix: String
let numStr: String
if name.hasPrefix("normal_") {
prefix = "1"
numStr = name.replacingOccurrences(of: "normal_", with: "")
} else if name.hasPrefix("fun_") {
prefix = "2"
numStr = name.replacingOccurrences(of: "fun_", with: "")
} else { return }
guard let num = Int(numStr) else { return }
let indexStr = "\(prefix)\(num)"
self.onSendEmote?(indexStr.integer)
})
.disposed(by: disposeBag)
// pageControl
emojiRelay
.subscribe(onNext: { [weak self] items in
guard let self = self else { return }
self.stopVisibleEmojiAnimations()
let pages = (items.count + InteractionView.emojiPerPage - 1) / InteractionView.emojiPerPage
self.emojiPageControl.numberOfPages = max(pages, 1)
self.emojiPageControl.currentPage = 0
self.emojiCollectionView.setContentOffset(.zero, animated: false)
})
.disposed(by: disposeBag)
}
private func switchEmojiCategory(isFun: Bool) {
guard isShowingFunEmoji != isFun else { return }
isShowingFunEmoji = isFun
stopVisibleEmojiAnimations()
normalBg.isHidden = !isFun
interestBg.isHidden = isFun
emojiRelay.accept(isFun ? UIView.funEmojiFileNames : UIView.emojiFileNames)
}
private lazy var dataSource: RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> = {
RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>> { _, collectionView, indexPath, name in
let cell: EmojiPanelCell = collectionView.dequeueReusableCell(for: indexPath)
cell.configure(name: name)
return cell
}
}()
private func stopVisibleEmojiAnimations() {
emojiPlaybackGeneration += 1
emojiCollectionView.visibleCells.forEach { cell in
(cell as? EmojiPanelCell)?.stopAnimation()
}
}
private func setupUI() {
addSubview(infoView)
infoView.addSubview(headerBgView)
infoView.addSubview(lineView)
infoView.addSubview(avaterImgView)
infoView.addSubview(batteryInfoView)
batteryInfoView.addSubview(cornerView)
cornerView.addSubview(batteryView)
cornerView.addSubview(batteryIcon)
cornerView.addSubview(batteryLab)
infoView.addSubview(nameLab)
infoView.addSubview(locationLab)
infoView.addSubview(navigateBtn)
infoView.addSubview(shareBtn)
infoView.addSubview(emojiView)
emojiView.addSubview(emojiBgView)
emojiView.addSubview(segmentView)
emojiView.addSubview(emojiCollectionView)
emojiView.addSubview(emojiPageControl)
infoView.layoutChain.edges()
headerBgView.layoutChain
.edges(excludingEdge: .bottom)
.height(134)
lineView.layoutChain
.top(13)
.width(36)
.height(4)
.centerX()
avaterImgView.layoutChain
.top(25)
.left(25)
.width(50)
.height(50)
batteryInfoView.layoutChain
.leftToView(avaterImgView)
.rightToView(avaterImgView)
.bottomToView(avaterImgView)
.height(12)
cornerView.layoutChain.edges()
batteryIcon.layoutChain
.left(7)
.centerY()
.width(16)
.height(8)
batteryView.layoutChain
.topToView(batteryIcon)
.leftToView(batteryIcon, offset: -1)
.bottomToView(batteryIcon)
batteryLab.layoutChain
.leftToRightOfView(batteryIcon, offset: 4)
.right(5)
.centerY()
nameLab.layoutChain
.topToView(avaterImgView, offset: 8)
.leftToRightOfView(avaterImgView, offset: 15)
locationLab.layoutChain
.topToBottomOfView(nameLab)
.leftToView(nameLab)
shareBtn.layoutChain
.right(15)
.centerY(avaterImgView)
.width(30)
.height(30)
navigateBtn.layoutChain
.rightToLeftOfView(shareBtn, offset: -10)
.centerY(avaterImgView)
.width(30)
.height(30)
emojiView.layoutChain
.topToBottomOfView(batteryInfoView, offset: 21)
.edgesHorzontal(15)
.height(276)
emojiBgView.layoutChain.edges()
segmentView.layoutChain
.edges(excludingEdge: .bottom)
.height(56)
emojiPageControl.layoutChain
.centerX()
.height(28)
.bottom(13)
emojiCollectionView.layoutChain
.top(51)
.edgesHorzontal()
.height(180)
}
lazy var infoView: UIView = {
let view = UIView()
view.backgroundColor = .white
return view
}()
lazy var headerBgView: UIImageView = {
let view = UIImageView(image: UIImage(named: "Home/interaction_header"))
return view
}()
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 2
return view
}()
lazy var avaterImgView: UIImageView = {
let view = UIImageView()
view.backgroundColor = .lightGray
view.contentMode = .scaleAspectFill
view.cornerRadius = 25
return view
}()
lazy var batteryInfoView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.1).cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowOpacity = 1
view.layer.shadowRadius = 6
view.isHidden = true
return view
}()
lazy var cornerView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 6
return view
}()
lazy var batteryView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#75E582")
return view
}()
lazy var batteryIcon: UIImageView = {
let view = UIImageView()
view.backgroundColor = .clear
view.image = UIImage(named: "Home/battery")
return view
}()
lazy var batteryLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#D4D4D4")
label.font = .systemFont(ofSize: 6, weight: .medium)
return label
}()
lazy var nameLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#0F2846")
label.font = .systemFont(ofSize: 14, weight: .semibold)
return label
}()
lazy var locationLab: UILabel = {
let label = UILabel()
label.textColor = UIColor(hexStr: "#8D8D8D")
label.font = .systemFont(ofSize: 10, weight: .regular)
return label
}()
lazy var navigateBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = .white
btn.setImage(UIImage(named: "Home/navigate"), for: .normal)
btn.cornerRadius = 15
btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 30, bottom: 30, right: 0)
return btn
}()
lazy var shareBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.backgroundColor = .white
btn.setImage(UIImage(named: "Home/share"), for: .normal)
btn.cornerRadius = 15
btn.extendEdgeInsets = UIEdgeInsets(top: 30, left: 0, bottom: 30, right: 15)
return btn
}()
lazy var emojiView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#E3F7FE")//.clear
view.cornerRadius = 15
return view
}()
lazy var emojiBgView: UIView = {
let view = UIImageView()//UIImageView(image: UIImage(named: "Home/emoji_bg"))
view.contentMode = .scaleAspectFill
return view
}()
lazy var segmentView: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#E3F7FE")
view.addSubview(normalBg)
view.addSubview(interestBg)
normalBg.layoutChain
.top().left()
.height(56)
.widthToHeight(220/56)
interestBg.layoutChain
.top().right()
.height(56)
.widthToHeight(220/56)
let normalLab = UILabel()
normalLab.text = "常规表情"
normalLab.font = .systemFont(ofSize: 12, weight: .semibold)
normalLab.textColor = .black
normalLab.textAlignment = .center
view.addSubview(normalLab)
normalLab.layoutChain
.top(11)
.left().rightToCenterXOfView(view)
let interesView = UIView()
interesView.backgroundColor = .clear
view.addSubview(interesView)
interesView.layoutChain
.top(11)
.right().leftToCenterXOfView(view)
.height(18)
let interestLab = UILabel()
interestLab.text = "趣味表情"
interestLab.font = .systemFont(ofSize: 12, weight: .semibold)
interestLab.textColor = .black
interestLab.textAlignment = .center
interesView.addSubview(interestLab)
interestLab.layoutChain
.top()
.centerX()
interesView.addSubview(lockView)
lockView.layoutChain
.leftToRightOfView(interestLab, offset: 4)
.centerY(interestLab)
.width(18)
.height(18)
view.addSubview(emojiNormalBtn)
emojiNormalBtn.layoutChain
.top()
.left()
.rightToCenterXOfView(view)
.height(40)
view.addSubview(emojiFunBtn)
emojiFunBtn.layoutChain
.top()
.right()
.leftToCenterXOfView(view)
.height(40)
return view
}()
lazy var normalBg: UIImageView = {
let view = UIImageView(image: UIImage(named: "Home/emoji_normal"))
view.contentMode = .scaleAspectFill
view.isHidden = true
return view
}()
lazy var interestBg: UIImageView = {
let view = UIImageView(image: UIImage(named: "Home/emoji_interest"))
view.contentMode = .scaleAspectFill
return view
}()
lazy var emojiNormalBtn: UIButton = {
let btn = UIButton()
btn.backgroundColor = .clear
return btn
}()
lazy var emojiFunBtn: UIButton = {
let btn = UIButton()
btn.backgroundColor = .clear
return btn
}()
lazy var lockView: UIView = {
let view = UIView()
view.backgroundColor = .white
view.cornerRadius = 9
let icon = UIImageView(image: UIImage(named: "Home/lock"))
view.addSubview(icon)
icon.layoutChain
.centerX()
.centerY()
return view
}()
lazy var emojiCollectionView: UICollectionView = {
let layout = CollectionHFlowLayout()
let hSpacing: CGFloat = (kScreenWidth - 30 - CGFloat(InteractionView.emojiCols) * 50) / CGFloat(InteractionView.emojiCols + 1)
let vSpacing: CGFloat = (180 - CGFloat(InteractionView.emojiRows) * 50) / CGFloat(InteractionView.emojiRows + 1)
layout.rows = InteractionView.emojiRows
layout.colums = InteractionView.emojiCols
layout.itemSize = CGSize(width: 50, height: 50)
layout.hSpacing = hSpacing
layout.vSpacing = vSpacing
layout.sectionInset = UIEdgeInsets(top: 0, left: hSpacing, bottom: 0, right: hSpacing)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.isPagingEnabled = true
cv.bounces = false
cv.showsHorizontalScrollIndicator = false
cv.register(EmojiPanelCell.self)
cv.delegate = self
return cv
}()
var onNavigate: (() -> Void)?
var onSendEmote: ((Int) -> Void)?
var onShare: (() -> Void)?
var currentMember: CircleMember?
lazy var emojiPageControl: UIPageControl = {
let pc = UIPageControl()
pc.numberOfPages = (UIView.emojiFileNames.count + InteractionView.emojiPerPage - 1) / InteractionView.emojiPerPage
pc.currentPageIndicatorTintColor = UIColor(hexStr: "#16B3FF")
pc.pageIndicatorTintColor = UIColor(hexStr: "#7AD6FF", alpha: 0.4)
return pc
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
setupUI()
setupRx()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
infoView.layoutIfNeeded()
infoView.setCornerRadius(corners: [.topLeft ,.topRight], withCornerRadii: CGSize(width: 20, height: 20))
}
}
// MARK: - UICollectionViewDelegate (page control)
extension InteractionView: UICollectionViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
guard scrollView == emojiCollectionView else { return }
let page = Int(scrollView.contentOffset.x / scrollView.bounds.width)
emojiPageControl.currentPage = page
}
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard collectionView == emojiCollectionView else { return }
(cell as? EmojiPanelCell)?.stopAnimation()
}
}
// MARK: - EmojiPanelCell
final class EmojiPanelCell: UICollectionViewCell {
private static var animationCache: [String: LottieAnimation] = [:]
private static var loadingCompletions: [String: [(LottieAnimation?) -> Void]] = [:]
private static let animationQueue = DispatchQueue(label: "com.quicklocation.emoji.animation", qos: .userInitiated)
static func loadAnimation(for name: String, completion: @escaping (LottieAnimation?) -> Void) {
animationQueue.async {
if let cached = animationCache[name] {
DispatchQueue.main.async { completion(cached) }
return
}
if loadingCompletions[name] != nil {
loadingCompletions[name]?.append(completion)
return
}
loadingCompletions[name] = [completion]
let animation: LottieAnimation?
if let path = Bundle.main.path(forResource: name, ofType: "json") {
animation = LottieAnimation.filepath(path)
} else {
animation = nil
}
animationCache[name] = animation
let completions = loadingCompletions.removeValue(forKey: name) ?? []
DispatchQueue.main.async {
completions.forEach { $0(animation) }
}
}
}
private var animationName: String?
private var shouldPlayAnimation = false
private let lottieView: LottieAnimationView = {
let v = LottieAnimationView()
v.contentMode = .scaleAspectFit
v.loopMode = .loop
return v
}()
lazy var lockView: UIView = {
let view = UIView()
view.backgroundColor = .clear
view.isHidden = true
let icon = UIImageView(image: UIImage(named: "Home/emoji_lock"))
view.addSubview(icon)
icon.layoutChain
.edges()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(lottieView)
contentView.addSubview(lockView)
lottieView.layoutChain.edges()
lockView.layoutChain
.right()
.bottom()
.width(14)
.height(14)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(name: String) {
animationName = name
shouldPlayAnimation = false
lottieView.stop()
lottieView.animation = nil
lottieView.currentProgress = 0
Self.loadAnimation(for: name) { [weak self] animation in
guard let self = self, self.animationName == name else { return }
self.lottieView.animation = animation
self.lottieView.currentProgress = 0
if self.shouldPlayAnimation {
self.lottieView.play()
}
}
lockView.isHidden = AppContextManager.shared.vip > 1
}
func playAnimation() {
shouldPlayAnimation = true
lottieView.play()
}
func stopAnimation() {
shouldPlayAnimation = false
lottieView.stop()
}
override func prepareForReuse() {
super.prepareForReuse()
animationName = nil
shouldPlayAnimation = false
lottieView.stop()
lottieView.animation = nil
}
}
// MARK: -
extension UIView {
static var emojiFileNames: [String] = {
let paths = Bundle.main.paths(forResourcesOfType: "json", inDirectory: nil)
return paths
.compactMap { $0.components(separatedBy: "/").last }
.filter { $0.hasPrefix("normal_") && $0.hasSuffix(".json") }
.map { $0.replacingOccurrences(of: ".json", with: "") }
.sorted()
}()
static var funEmojiFileNames: [String] = {
let paths = Bundle.main.paths(forResourcesOfType: "json", inDirectory: nil)
return paths
.compactMap { $0.components(separatedBy: "/").last }
.filter { $0.hasPrefix("fun_") && $0.hasSuffix(".json") }
.map { $0.replacingOccurrences(of: ".json", with: "") }
.sorted()
}()
}