jsdw_ios/QuickLocation/Component/PhotoPicker/PhotoPickerView.swift

390 lines
13 KiB
Swift

//
// PhotoPickerView.swift
// QuickLocation
//
import UIKit
import Photos
final class PhotoPickerView: UIView {
var onClose: (() -> Void)?
var onToggleAlbum: (() -> Void)?
var onPreview: (() -> Void)?
var onConfirm: (() -> Void)?
var onSelectAsset: ((Int) -> Void)?
var onPickAlbum: ((Int) -> Void)?
var isAlbumOpen = false {
didSet { applyAlbumOpen() }
}
private let store: PhotoPickerStore
private let maxCount: Int
private var selectedIds: [String] = []
private var currentAlbumIndex = 0
private var lastGridWidth: CGFloat = 0
init(store: PhotoPickerStore, maxCount: Int) {
self.store = store
self.maxCount = maxCount
super.init(frame: .zero)
backgroundColor = UIColor(hexStr: "#2C2C2E")
setupUI()
applyAlbumOpen()
updateBottomBar(selectedCount: 0)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func layoutSubviews() {
super.layoutSubviews()
let width = collectionView.bounds.width
if width > 1, abs(width - lastGridWidth) > 0.5 {
lastGridWidth = width
collectionView.collectionViewLayout.invalidateLayout()
}
}
func reloadGrid() {
collectionView.reloadData()
}
func bindAlbumStore(_ store: PhotoPickerStore) {
albumListView.bindStore(store)
}
func reloadAlbums(currentIndex: Int) {
currentAlbumIndex = currentIndex
albumListView.reload(albums: store.albums, selectedIndex: currentIndex)
let title = store.albums[safe: currentIndex]?.title ?? "最近项目"
albumTitleLab.text = title
}
func applySelection(_ ids: [String]) {
selectedIds = ids
updateBottomBar(selectedCount: ids.count)
collectionView.reloadData()
}
func showLimitToast() {
limitBanner.isHidden = false
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(hideLimitToast), object: nil)
perform(#selector(hideLimitToast), with: nil, afterDelay: 1.8)
}
func thumbnailSize() -> CGSize {
let side = collectionView.bounds.width > 0
? (collectionView.bounds.width - 3) / 4
: (UIScreen.main.bounds.width - 3) / 4
return CGSize(width: side, height: side)
}
// MARK: - UI
private lazy var closeBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setImage(UIImage(named: "PhotoPicker/btn_close"), for: .normal)
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
btn.addTarget(self, action: #selector(tapClose), for: .touchUpInside)
return btn
}()
private lazy var albumTitleBtn: UIControl = {
let control = UIControl()
control.backgroundColor = UIColor.white.withAlphaComponent(0.12)
control.cornerRadius = 16
control.addTarget(self, action: #selector(tapAlbum), for: .touchUpInside)
return control
}()
private lazy var albumTitleLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 15, weight: .medium)
label.textColor = .white
label.text = "最近项目"
return label
}()
private lazy var albumArrowView: UIImageView = {
let iv = UIImageView(image: UIImage(named: "PhotoPicker/btn_album_arrow"))
iv.contentMode = .scaleAspectFit
return iv
}()
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.minimumInteritemSpacing = 1
layout.minimumLineSpacing = 1
layout.sectionInset = .zero
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = UIColor(hexStr: "#2C2C2E")
cv.register(PhotoPickerCell.self, forCellWithReuseIdentifier: "cell")
cv.dataSource = self
cv.delegate = self
cv.alwaysBounceVertical = true
return cv
}()
private lazy var dimView: UIControl = {
let view = UIControl()
view.backgroundColor = UIColor.black.withAlphaComponent(0.45)
view.addTarget(self, action: #selector(tapAlbum), for: .touchUpInside)
return view
}()
private lazy var albumListView: PhotoPickerAlbumView = {
let view = PhotoPickerAlbumView()
view.onSelect = { [weak self] index in
self?.onPickAlbum?(index)
}
return view
}()
private lazy var bottomBar: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#1C1C1E")
return view
}()
private lazy var previewBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle("预览", for: .normal)
btn.setTitleColor(UIColor.white.withAlphaComponent(0.35), for: .disabled)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = .systemFont(ofSize: 16)
btn.contentHorizontalAlignment = .left
btn.addTarget(self, action: #selector(tapPreview), 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 = .systemFont(ofSize: 16, weight: .medium)
btn.backgroundColor = UIColor(hexStr: "#007AFF")
btn.cornerRadius = 8
btn.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside)
return btn
}()
private lazy var limitBanner: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexStr: "#3A3A3C")
view.isHidden = true
let icon = UILabel()
icon.text = "!"
icon.textColor = .white
icon.font = .systemFont(ofSize: 12, weight: .bold)
icon.textAlignment = .center
icon.backgroundColor = .clear
icon.layer.borderColor = UIColor.white.cgColor
icon.layer.borderWidth = 1.2
icon.layer.cornerRadius = 8
icon.clipsToBounds = true
let label = UILabel()
label.text = "最多选取\(maxCount)张图片哦~"
label.textColor = .white
label.font = .systemFont(ofSize: 14)
view.addSubview(icon)
view.addSubview(label)
icon.layoutChain.left(24).centerY().width(16).height(16)
label.layoutChain.leftToRightOfView(icon, offset: 8).centerY()
return view
}()
private var albumListHeight: NSLayoutConstraint?
private func setupUI() {
addSubview(closeBtn)
addSubview(albumTitleBtn)
albumTitleBtn.addSubview(albumTitleLab)
albumTitleBtn.addSubview(albumArrowView)
addSubview(collectionView)
addSubview(dimView)
addSubview(albumListView)
addSubview(limitBanner)
addSubview(bottomBar)
bottomBar.addSubview(previewBtn)
bottomBar.addSubview(confirmBtn)
closeBtn.layoutChain
.left(16)
.width(32)
.height(32)
closeBtn.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 6).isActive = true
albumTitleBtn.layoutChain
.centerX()
.centerY(closeBtn)
.height(32)
albumTitleLab.layoutChain
.left(12)
.centerY()
albumArrowView.layoutChain
.leftToRightOfView(albumTitleLab, offset: 6)
.right(8)
.centerY()
.width(20)
.height(20)
collectionView.layoutChain
.topToBottomOfView(closeBtn, offset: 10)
.left()
.right()
bottomBar.layoutChain
.left()
.right()
.bottom()
.topToBottomOfView(collectionView)
let bottomH = 56 + kSafeBottomMargin
bottomBar.layoutChain.height(bottomH)
previewBtn.layoutChain
.left(20)
.top(16)
.height(24)
confirmBtn.layoutChain
.right(16)
.top(10)
.width(88)
.height(36)
limitBanner.layoutChain
.left()
.right()
.bottomToTopOfView(bottomBar)
.height(44)
dimView.layoutChain
.topToBottomOfView(closeBtn, offset: 10)
.left()
.right()
.bottomToTopOfView(bottomBar)
albumListView.layoutChain
.topToView(dimView)
.left()
.right()
albumListHeight = albumListView.heightAnchor.constraint(equalToConstant: 0)
albumListHeight?.isActive = true
}
private func applyAlbumOpen() {
dimView.isHidden = !isAlbumOpen
albumListView.isHidden = !isAlbumOpen
let count = store.albums.count
let rowH: CGFloat = 64
let maxH = min(CGFloat(count) * rowH, UIScreen.main.bounds.height * 0.42)
albumListHeight?.constant = isAlbumOpen ? maxH : 0
UIView.animate(withDuration: 0.22) {
self.albumArrowView.transform = self.isAlbumOpen
? CGAffineTransform(rotationAngle: .pi)
: .identity
self.layoutIfNeeded()
}
}
private func updateBottomBar(selectedCount: Int) {
previewBtn.isEnabled = selectedCount > 0
if selectedCount > 0 {
previewBtn.setTitle("预览 (\(selectedCount))", for: .normal)
} else {
previewBtn.setTitle("预览", for: .normal)
}
confirmBtn.alpha = selectedCount > 0 ? 1 : 0.55
}
@objc private func tapClose() { onClose?() }
@objc private func tapAlbum() { onToggleAlbum?() }
@objc private func tapPreview() { onPreview?() }
@objc private func tapConfirm() { onConfirm?() }
@objc private func hideLimitToast() { limitBanner.isHidden = true }
}
extension PhotoPickerView: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
store.assets.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! PhotoPickerCell
let asset = store.assets[indexPath.item]
let index = selectedIds.firstIndex(of: asset.localIdentifier)
cell.configure(asset: asset, selectedIndex: index, store: store, size: thumbnailSize())
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
onSelectAsset?(indexPath.item)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
thumbnailSize()
}
}
private final class PhotoPickerCell: UICollectionViewCell {
private let imageView = UIImageView()
private let dimView = UIView()
private let badge = UILabel()
private var requestId: PHImageRequestID = PHInvalidImageRequestID
private var assetId: String = ""
override init(frame: CGRect) {
super.init(frame: frame)
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
dimView.backgroundColor = UIColor.black.withAlphaComponent(0.28)
dimView.isHidden = true
badge.textAlignment = .center
badge.font = .systemFont(ofSize: 13, weight: .semibold)
badge.layer.cornerRadius = 11
badge.clipsToBounds = true
contentView.addSubview(imageView)
contentView.addSubview(dimView)
contentView.addSubview(badge)
imageView.layoutChain.edges()
dimView.layoutChain.edges()
badge.layoutChain.top(5).right(5).width(22).height(22)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func configure(asset: PHAsset, selectedIndex: Int?, store: PhotoPickerStore, size: CGSize) {
if requestId != PHInvalidImageRequestID {
store.cancelRequest(requestId)
}
assetId = asset.localIdentifier
imageView.image = nil
let selected = selectedIndex != nil
dimView.isHidden = !selected
if let selectedIndex {
badge.text = "\(selectedIndex + 1)"
badge.textColor = .white
badge.backgroundColor = UIColor(hexStr: "#007AFF")
badge.layer.borderWidth = 0
} else {
badge.text = ""
badge.backgroundColor = UIColor.black.withAlphaComponent(0.18)
badge.layer.borderWidth = 1.5
badge.layer.borderColor = UIColor.white.cgColor
}
requestId = store.requestThumbnail(for: asset, size: size) { [weak self] image in
guard let self, self.assetId == asset.localIdentifier else { return }
self.imageView.image = image
}
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
assetId = ""
}
}