jsdw_ios/QuickLocation/Section/Home/Bubble/CreateBubbleSetupView.swift

483 lines
18 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.

//
// CreateBubbleSetupView.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import SwiftDate
/// picker + sheet
final class CreateBubbleSetupView: UIView {
var disposeBag = DisposeBag()
let selectedHour = BehaviorRelay<Int>(value: 1)
var messageText: String = ""
var onConfirmCreate: (() -> Void)?
var onSheetVisibilityChanged: ((Bool) -> Void)?
let heroView = BubbleHeroView()
let confirmBtn = UIButton(type: .custom)
let sheetConfirmBtn = UIButton(type: .custom)
private let subtitleLab = UILabel()
private let titleLab = UILabel()
private let hourPicker = BubbleHourPickerView()
private let sheetOverlay = UIView()
private let sheetPanel = UIView()
private let headerBgImage = UIImageView()
private let megaphoneImage = UIImageView()
private let sheetTitleLab = UILabel()
private let messageView = UIView()
private let messageLab = UILabel()
private let previewAvatar = UIImageView()
private let tipStack = UIStackView()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setupUI()
setupSheet()
hourPicker.onHourChanged = { [weak self] hour in
self?.selectedHour.accept(hour)
}
selectedHour.accept(1)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func showNotifySheet(message: String) {
messageText = message
messageLab.attributedText = notificationMessage(from: message)
sheetOverlay.isHidden = false
sheetOverlay.alpha = 0
sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
megaphoneImage.transform = CGAffineTransform(translationX: 0, y: 40)
onSheetVisibilityChanged?(true)
UIView.animate(withDuration: 0.28, delay: 0, options: .curveEaseOut) {
self.sheetOverlay.alpha = 1
self.sheetPanel.transform = .identity
self.megaphoneImage.transform = .identity
}
}
func dismissNotifySheet() {
onSheetVisibilityChanged?(false)
UIView.animate(withDuration: 0.22, delay: 0, options: .curveEaseIn) {
self.sheetOverlay.alpha = 0
self.sheetPanel.transform = CGAffineTransform(translationX: 0, y: 40)
self.megaphoneImage.transform = CGAffineTransform(translationX: 0, y: 40)
} completion: { _ in
self.sheetOverlay.isHidden = true
}
}
var isSheetVisible: Bool { !sheetOverlay.isHidden && sheetOverlay.alpha > 0.01 }
private func setupUI() {
addSubview(heroView)
addSubview(titleLab)
addSubview(subtitleLab)
addSubview(hourPicker)
addSubview(confirmBtn)
heroView.showsOuterDecorBubbles = false
heroView.layoutChain
.top(33)
.leading(30)
.trailing(30)
.heightToWidth(1)
titleLab.text = "您会在气泡待多久?"
titleLab.font = FontManager.boboBold(30)
titleLab.textColor = UIColor(hexStr: "#293445")
titleLab.textAlignment = .center
titleLab.layoutChain.topToBottomOfView(heroView, offset: 25).centerX()
subtitleLab.text = "设置气泡时间"
subtitleLab.font = .systemFont(ofSize: 18, weight: .bold)
subtitleLab.textColor = UIColor(hexStr: "#9CA3AF")
subtitleLab.layoutChain.topToBottomOfView(titleLab, offset: 19).centerX()
hourPicker.layoutChain
.topToBottomOfView(subtitleLab, offset: 29)
.edgesHorzontal()
.height(96)
confirmBtn.setTitle("确定", for: .normal)
confirmBtn.setTitleColor(.white, for: .normal)
confirmBtn.titleLabel?.font = FontManager.boboBold(18)
confirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
confirmBtn.cornerRadius = 20
confirmBtn.layoutChain.left(30).right(30).bottom(34).height(56)
}
private func setupSheet() {
sheetOverlay.backgroundColor = UIColor.black.withAlphaComponent(0.45)
sheetOverlay.isHidden = true
addSubview(sheetOverlay)
sheetOverlay.layoutChain.edges()
sheetPanel.backgroundColor = .white
sheetPanel.layer.cornerRadius = 40
sheetPanel.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
sheetPanel.clipsToBounds = true
sheetOverlay.addSubview(sheetPanel)
sheetPanel.layoutChain.edgesHorzontal().bottom()
headerBgImage.image = UIImage(named: "Common/popup_header_bg")
headerBgImage.contentMode = .scaleToFill
sheetPanel.addSubview(headerBgImage)
headerBgImage.layoutChain
.top()
.edgesHorzontal()
.heightToWidth(239.0 / 750.0)
megaphoneImage.image = UIImage(named: "Bubble/notification_megaphone")
megaphoneImage.contentMode = .scaleAspectFit
sheetOverlay.addSubview(megaphoneImage)
megaphoneImage.layoutChain
.left(34)
.topToView(sheetPanel, offset: -44)
.width(130)
.height(90)
sheetTitleLab.text = "通知您的圈子"
sheetTitleLab.font = FontManager.boboBold(26)
sheetTitleLab.textColor = UIColor(hexStr: "#173A5E")
sheetTitleLab.textAlignment = .center
sheetPanel.addSubview(sheetTitleLab)
sheetTitleLab.layoutChain.top(52).centerX()
messageView.backgroundColor = UIColor(hexStr: "#E1F4FF")
messageView.layer.cornerRadius = 16
messageView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
sheetPanel.addSubview(messageView)
messageView.addSubview(messageLab)
messageLab.font = .systemFont(ofSize: 16, weight: .medium)
messageLab.textColor = UIColor(hexStr: "#293445")
messageLab.numberOfLines = 0
messageLab.lineBreakMode = .byWordWrapping
messageLab.layoutChain.edgesHorzontal(16).edgesVertical(12)
previewAvatar.contentMode = .scaleAspectFill
previewAvatar.clipsToBounds = true
previewAvatar.layer.cornerRadius = 20
previewAvatar.layer.borderWidth = 2
previewAvatar.layer.borderColor = UIColor.white.cgColor
let avatar = AppContextManager.shared.avaterIcon
previewAvatar.image = avatar.size.width > 0 ? avatar : UIImage(named: "Common/default_avatar")
sheetPanel.addSubview(previewAvatar)
messageView.layoutChain
.topToBottomOfView(sheetTitleLab, offset: 42)
.left(35)
.right(75)
previewAvatar.layoutChain
.right(24)
.topToView(messageView, offset: -20)
.width(40)
.height(40)
tipStack.axis = .vertical
tipStack.spacing = 16
sheetPanel.addSubview(tipStack)
tipStack.layoutChain
.topToBottomOfView(messageView, offset: 22)
.edgesHorzontal(35)
[
"您的圈子成员只会看到您在气泡中。",
"当您在气泡中,将不会共享您的确切位置。"
].forEach { tipStack.addArrangedSubview(makeTipRow($0)) }
sheetConfirmBtn.setTitle("确定", for: .normal)
sheetConfirmBtn.setTitleColor(.white, for: .normal)
sheetConfirmBtn.titleLabel?.font = FontManager.boboBold(18)
sheetConfirmBtn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
sheetConfirmBtn.layer.cornerRadius = 20
sheetConfirmBtn.clipsToBounds = true
sheetPanel.addSubview(sheetConfirmBtn)
sheetConfirmBtn.layoutChain
.topToBottomOfView(tipStack, offset: 32)
.edgesHorzontal(30)
.height(56)
.bottom(12 + kSafeBottomMargin)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask))
sheetOverlay.addGestureRecognizer(tap)
}
private func makeTipRow(_ text: String) -> UIView {
let row = UIView()
let star = UIImageView(image: UIImage(named: "Bubble/notification_sparkle"))
star.contentMode = .scaleAspectFit
let lab = UILabel()
lab.text = text
lab.font = .systemFont(ofSize: 14, weight: .medium)
lab.textColor = UIColor(hexStr: "#7C7F87")
lab.numberOfLines = 0
row.addSubview(star)
row.addSubview(lab)
star.layoutChain.left().top(4).width(11).height(11)
lab.layoutChain.leftToRightOfView(star, offset: 10).right().top().bottom()
return row
}
private func notificationMessage(from message: String) -> NSAttributedString {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 5
let attributed = NSMutableAttributedString(
string: message,
attributes: [
.font: UIFont.systemFont(ofSize: 16, weight: .medium),
.foregroundColor: UIColor(hexStr: "#293445"),
.paragraphStyle: paragraphStyle
]
)
if let separatorRange = message.range(of: "之前") {
let timePrefix = String(message[..<separatorRange.lowerBound])
attributed.addAttribute(
.foregroundColor,
value: UIColor(hexStr: "#16B3FF"),
range: NSRange(location: 0, length: (timePrefix as NSString).length)
)
}
return attributed
}
@objc private func tapMask(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: sheetPanel)
if sheetPanel.bounds.contains(point) { return }
let megaphonePoint = gesture.location(in: megaphoneImage)
if megaphoneImage.bounds.contains(megaphonePoint) { return }
dismissNotifySheet()
}
func buildMessage(for hours: Int) -> String {
let endDate = Calendar.current.date(byAdding: .hour, value: hours, to: Date()) ?? Date()
let timeStr = endDate.toFormat("HH:mm")
return "\(timeStr)之前,我会一直在这个区域。如果需要我帮忙,请给我发消息。"
}
}
/// 16 N
private final class BubbleHourPickerView: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
var onHourChanged: ((Int) -> Void)?
private let hours = Array(1...6)
private let loopCount = 200
private let itemWidth: CGFloat = 52
private let itemSpacing: CGFloat = 12
private let accent = UIColor(hexStr: "#FF5CB8")
private var currentHour = 1
private var didInitialScroll = false
private var lastLayoutWidth: CGFloat = 0
private lazy var layout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: itemWidth, height: 70)
layout.minimumLineSpacing = itemSpacing
layout.minimumInteritemSpacing = 0
return layout
}()
private lazy var collectionView: UICollectionView = {
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .clear
view.showsHorizontalScrollIndicator = false
view.decelerationRate = .fast
view.dataSource = self
view.delegate = self
view.register(HourCell.self, forCellWithReuseIdentifier: HourCell.reuseId)
return view
}()
private let capsule = UIView()
private let capsuleLab = UILabel()
private let pointerLayer = CAShapeLayer()
private let centerBar = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = false
addSubview(collectionView)
capsule.backgroundColor = accent
capsule.layer.cornerRadius = 15
capsule.clipsToBounds = false
capsule.isUserInteractionEnabled = false
addSubview(capsule)
capsuleLab.text = "1小时"
capsuleLab.font = .systemFont(ofSize: 16, weight: .bold)
capsuleLab.textColor = .white
capsuleLab.textAlignment = .center
capsule.addSubview(capsuleLab)
pointerLayer.fillColor = accent.cgColor
capsule.layer.addSublayer(pointerLayer)
centerBar.backgroundColor = accent
centerBar.layer.cornerRadius = 4
centerBar.isUserInteractionEnabled = false
addSubview(centerBar)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func layoutSubviews() {
super.layoutSubviews()
guard bounds.width > 0 else { return }
if abs(bounds.width - lastLayoutWidth) > 0.5 {
lastLayoutWidth = bounds.width
let inset = max(0, (bounds.width - itemWidth) / 2)
layout.sectionInset = UIEdgeInsets(top: 0, left: inset, bottom: 0, right: inset)
layout.invalidateLayout()
}
collectionView.frame = CGRect(x: 0, y: 18, width: bounds.width, height: bounds.height - 18)
let capsuleSize = CGSize(width: 78, height: 30)
capsule.frame = CGRect(
x: (bounds.width - capsuleSize.width) / 2,
y: 0,
width: capsuleSize.width,
height: capsuleSize.height
)
capsuleLab.frame = capsule.bounds
let pointerWidth: CGFloat = 10
let pointerHeight: CGFloat = 6
let path = UIBezierPath()
let px = capsule.bounds.midX
let py = capsule.bounds.maxY - 1
path.move(to: CGPoint(x: px - pointerWidth / 2, y: py))
path.addLine(to: CGPoint(x: px + pointerWidth / 2, y: py))
path.addLine(to: CGPoint(x: px, y: py + pointerHeight))
path.close()
pointerLayer.path = path.cgPath
centerBar.frame = CGRect(x: (bounds.width - 8) / 2, y: 46, width: 8, height: 36)
if !didInitialScroll {
didInitialScroll = true
collectionView.layoutIfNeeded()
scrollToHour(currentHour, animated: false)
updateVisibleCells()
}
}
private var itemSpan: CGFloat { itemWidth + itemSpacing }
private var totalItems: Int { hours.count * loopCount }
private func hour(at item: Int) -> Int {
hours[((item % hours.count) + hours.count) % hours.count]
}
private func centeredItem() -> Int {
let index = Int((collectionView.contentOffset.x / itemSpan).rounded())
return min(max(0, index), totalItems - 1)
}
private func scrollToHour(_ hour: Int, animated: Bool) {
let hourIndex = hours.firstIndex(of: hour) ?? 0
let item = (loopCount / 2) * hours.count + hourIndex
collectionView.setContentOffset(CGPoint(x: CGFloat(item) * itemSpan, y: 0), animated: animated)
}
private func commitCenteredHour() {
let hour = self.hour(at: centeredItem())
guard hour != currentHour else { return }
currentHour = hour
capsuleLab.text = "\(hour)小时"
onHourChanged?(hour)
}
private func updateVisibleCells() {
for cell in collectionView.visibleCells {
guard let hourCell = cell as? HourCell else { continue }
let converted = collectionView.convert(hourCell.center, to: self)
let distance = abs(converted.x - bounds.midX)
let isCenter = distance < itemWidth * 0.45
hourCell.numberLab.alpha = isCenter ? 0 : max(0.28, 1 - distance / (itemWidth * 3.2))
hourCell.tick.alpha = isCenter ? 0 : max(0.35, 1 - distance / (itemWidth * 2.6))
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
totalItems
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HourCell.reuseId, for: indexPath) as! HourCell
cell.numberLab.text = "\(hour(at: indexPath.item))"
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
updateVisibleCells()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateVisibleCells()
commitCenteredHour()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
commitCenteredHour()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate { commitCenteredHour() }
}
func scrollViewWillEndDragging(
_ scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
let raw = targetContentOffset.pointee.x / itemSpan
let index = CGFloat(Int(raw.rounded()))
let clamped = min(max(0, index), CGFloat(totalItems - 1))
targetContentOffset.pointee.x = clamped * itemSpan
}
}
private final class HourCell: UICollectionViewCell {
static let reuseId = "HourCell"
let numberLab = UILabel()
let tick = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
numberLab.textAlignment = .center
numberLab.font = .systemFont(ofSize: 22, weight: .medium)
numberLab.textColor = UIColor(hexStr: "#B0B8C1")
tick.backgroundColor = UIColor(hexStr: "#FFD2E8")
tick.layer.cornerRadius = 5
contentView.addSubview(numberLab)
contentView.addSubview(tick)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func layoutSubviews() {
super.layoutSubviews()
numberLab.frame = CGRect(x: 0, y: 6, width: bounds.width, height: 28)
tick.frame = CGRect(x: (bounds.width - 10) / 2, y: 40, width: 10, height: 16)
}
}