jsdw_ios/QuickLocation/Section/Mine/MinePhotoWallView.swift

887 lines
30 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.

//
// MinePhotoWallView.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
/// 线 + +
final class MinePhotoWallView: UIView {
var onCenteredMember: ((GroupMemberModel) -> Void)?
///
var onTapMember: ((GroupMemberModel) -> Void)?
private var members: [GroupMemberModel] = []
/// members
private var clipColorIndices: [Int] = []
private var ownerUserId: String = ""
private var centeredIndex: Int = 0
private var isDragging = false
private var lastLayoutWidth: CGFloat = 0
private var isPairMode = false
/// cell 70×90 +
private let itemSize = CGSize(width: 72, height: 110)
private let itemSpacing: CGFloat = 18
///
private let edgeFadeWidth: CGFloat = 56
///
private let ropeEndY: CGFloat = 30
///
private let ropeSag: CGFloat = 28
private var clipAttachInCell: CGFloat { MinePolaroidCell.clipAttachOffset }
private let selectedSize = MinePolaroidCell.selectedFrameSize
private let unselectedSize = MinePolaroidCell.unselectedFrameSize
/// t[-1,1]
private func ropeT(atX x: CGFloat) -> CGFloat {
let w = max(bounds.width, 1)
let t = (x - w * 0.5) / (w * 0.5)
return max(-1, min(1, t))
}
/// x y
private func ropeY(atX x: CGFloat) -> CGFloat {
let t = ropeT(atX: x)
return ropeEndY + ropeSag * (1 - t * t)
}
/// 线UIKit 使
private func ropeAngle(atX x: CGFloat) -> CGFloat {
let w = max(bounds.width, 1)
let t = ropeT(atX: x)
// y = end + sag*(1-t²), t=(x-w/2)/(w/2) dy/dx = -4*sag*t/w
return atan(-4 * ropeSag * t / w)
}
private var ropeCenterY: CGFloat { ropeEndY + ropeSag }
// MARK: - Public
func reload(
members: [GroupMemberModel],
ownerUserId: String,
preferUserId: String,
memberLimit: Int = -1
) {
isPairMode = memberLimit == 2
self.members = isPairMode ? Array(members.prefix(2)) : members
self.ownerUserId = ownerUserId
self.clipColorIndices = Self.makeClipColorIndices(for: self.members)
collectionView.isScrollEnabled = !isPairMode
daysBadgeImg.image = UIImage(named: isPairMode ? "Mine/circle_days_pair" : "Mine/circle_days")
collectionView.reloadData()
applyPairInsetsIfNeeded()
guard !self.members.isEmpty else {
updateInfo(nil)
return
}
if isPairMode {
centeredIndex = 0
collectionView.setContentOffset(.zero, animated: false)
collectionView.layoutIfNeeded()
applyArcAndFocus()
DispatchQueue.main.async { [weak self] in
self?.applyArcAndFocus()
}
updateInfo(self.members[0])
onCenteredMember?(self.members[0])
return
}
let idx: Int
if let i = self.members.firstIndex(where: { $0.user_id == preferUserId }) {
idx = i
} else if let i = self.members.firstIndex(where: { $0.user_id == AppContextManager.shared.userId }) {
idx = i
} else {
idx = 0
}
centeredIndex = idx
collectionView.layoutIfNeeded()
scrollToIndex(idx, animated: false)
collectionView.layoutIfNeeded()
applyArcAndFocus()
DispatchQueue.main.async { [weak self] in
self?.applyArcAndFocus()
}
updateInfo(self.members[idx])
onCenteredMember?(self.members[idx])
}
///
private static func makeClipColorIndices(for members: [GroupMemberModel]) -> [Int] {
let colorCount = MinePolaroidCell.clipAssetNames.count
guard colorCount > 1 else { return Array(repeating: 0, count: members.count) }
var result: [Int] = []
var prev = -1
for (i, m) in members.enumerated() {
var options = Array(0..<colorCount).filter { $0 != prev }
// >2
if i == members.count - 1, members.count > 2, let first = result.first {
options = options.filter { $0 != first }
if options.isEmpty { options = Array(0..<colorCount).filter { $0 != prev } }
}
let hash = abs(m.user_id.hashValue)
let pick = options[hash % options.count]
result.append(pick)
prev = pick
}
return result
}
// MARK: - UI
private lazy var ropeShadowLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.strokeColor = UIColor(hexStr: "#C8D0D8").withAlphaComponent(0.55).cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 1.5
layer.lineCap = .round
return layer
}()
private lazy var ropeLayer: CAShapeLayer = {
let layer = CAShapeLayer()
layer.strokeColor = UIColor.white.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 1
layer.lineCap = .round
return layer
}()
private lazy var collectionView: UICollectionView = {
let layout = MinePhotoWallFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = itemSize
layout.minimumLineSpacing = itemSpacing
let side = (UIScreen.main.bounds.width - itemSize.width) / 2
let topInset = ropeCenterY - MinePolaroidCell.clipAttachOffset
layout.sectionInset = UIEdgeInsets(top: max(0, topInset), left: side, bottom: 0, right: side)
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .clear
cv.showsHorizontalScrollIndicator = false
cv.decelerationRate = .fast
cv.clipsToBounds = false
cv.dataSource = self
cv.delegate = self
cv.register(MinePolaroidCell.self)
return cv
}()
private lazy var titleLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.textColor = UIColor(hexStr: "#293445")
label.textAlignment = .center
label.text = " "
return label
}()
private lazy var pairHeartImg: UIImageView = {
let iv = UIImageView(image: UIImage(named: "Mine/name_heart"))
iv.contentMode = .scaleAspectFit
iv.isHidden = true
return iv
}()
private lazy var pairLeftLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.textColor = UIColor(hexStr: "#293445")
label.textAlignment = .right
label.isHidden = true
return label
}()
private lazy var pairRightLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16, weight: .heavy)
label.textColor = UIColor(hexStr: "#293445")
label.textAlignment = .left
label.isHidden = true
return label
}()
private lazy var daysRow: UIView = {
let v = UIView()
v.addSubview(daysPrefixLab)
v.addSubview(daysBadge)
v.addSubview(daysSuffixLab)
daysPrefixLab.layoutChain
.left()
.centerY()
daysBadge.layoutChain
.leftToRightOfView(daysPrefixLab, offset: 4)
.centerY()
.width(37)
.height(26)
daysSuffixLab.layoutChain
.leftToRightOfView(daysBadge, offset: 4)
.right()
.centerY()
.top()
.bottom()
return v
}()
private lazy var daysPrefixLab: UILabel = {
let label = UILabel()
label.text = "已经在圈子"
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#293445")
return label
}()
private lazy var daysBadge: UIView = {
let view = UIView()
view.addSubview(daysBadgeImg)
view.addSubview(daysLab)
daysBadgeImg.layoutChain.edges()
daysLab.layoutChain
.left(3)
.right(3)
.centerY(view, offset: 3)
return view
}()
private lazy var daysBadgeImg: UIImageView = {
let iv = UIImageView(image: UIImage(named: "Mine/circle_days"))
iv.contentMode = .scaleAspectFit
return iv
}()
private lazy var daysLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14, weight: .heavy)
label.textColor = UIColor(hexStr: "#293445")
label.textAlignment = .center
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.6
return label
}()
private lazy var daysSuffixLab: UILabel = {
let label = UILabel()
label.text = ""
label.font = .systemFont(ofSize: 14, weight: .medium)
label.textColor = UIColor(hexStr: "#293445")
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
layer.addSublayer(ropeShadowLayer)
layer.addSublayer(ropeLayer)
addSubview(collectionView)
addSubview(titleLab)
addSubview(pairHeartImg)
addSubview(pairLeftLab)
addSubview(pairRightLab)
addSubview(daysRow)
collectionView.layoutChain
.top()
.edgesHorzontal()
.height(170)
titleLab.layoutChain
.topToBottomOfView(collectionView, offset: 0)
.centerX()
.height(24)
pairHeartImg.layoutChain
.centerX()
.centerY(titleLab)
.width(11)
.height(9)
pairLeftLab.layoutChain
.centerY(titleLab)
.rightToLeftOfView(pairHeartImg, offset: -8)
.left(16, relation: .greaterThanOrEqual)
pairRightLab.layoutChain
.centerY(titleLab)
.leftToRightOfView(pairHeartImg, offset: 8)
.right(16, relation: .greaterThanOrEqual)
daysRow.layoutChain
.topToBottomOfView(titleLab, offset: 4)
.centerX()
.height(26)
.bottom(4)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
updateRopePath()
let widthChanged = abs(bounds.width - lastLayoutWidth) > 0.5
if widthChanged, let layout = collectionView.collectionViewLayout as? MinePhotoWallFlowLayout {
lastLayoutWidth = bounds.width
applyPairInsetsIfNeeded()
layout.invalidateLayout()
collectionView.layoutIfNeeded()
}
applyArcAndFocus()
}
private func updateRopePath() {
let w = bounds.width
guard w > 0 else { return }
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: ropeEndY))
path.addQuadCurve(
to: CGPoint(x: w, y: ropeEndY),
controlPoint: CGPoint(x: w * 0.5, y: ropeEndY + ropeSag * 2)
)
ropeLayer.path = path.cgPath
ropeLayer.frame = bounds
let shadowPath = UIBezierPath()
let dy: CGFloat = 1.5
shadowPath.move(to: CGPoint(x: 0, y: ropeEndY + dy))
shadowPath.addQuadCurve(
to: CGPoint(x: w, y: ropeEndY + dy),
controlPoint: CGPoint(x: w * 0.5, y: ropeEndY + ropeSag * 2 + dy)
)
ropeShadowLayer.path = shadowPath.cgPath
ropeShadowLayer.frame = bounds
}
private func applyPairInsetsIfNeeded() {
guard let layout = collectionView.collectionViewLayout as? MinePhotoWallFlowLayout else { return }
let topInset = ropeCenterY - clipAttachInCell
let side: CGFloat
if isPairMode {
let count = CGFloat(max(members.count, 1))
let contentW = count * itemSize.width + max(0, count - 1) * itemSpacing
side = max(0, (bounds.width - contentW) / 2)
} else {
side = (bounds.width - itemSize.width) / 2
}
layout.sectionInset = UIEdgeInsets(top: max(0, topInset), left: side, bottom: 0, right: side)
}
private func scrollToIndex(_ index: Int, animated: Bool) {
guard index >= 0, index < members.count else { return }
let indexPath = IndexPath(item: index, section: 0)
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: animated)
}
private func nearestCenterIndex() -> Int {
let centerX = collectionView.contentOffset.x + collectionView.bounds.width / 2
var best = 0
var bestDist = CGFloat.greatestFiniteMagnitude
for i in 0..<members.count {
let attrs = collectionView.layoutAttributesForItem(at: IndexPath(item: i, section: 0))
guard let mid = attrs?.center.x else { continue }
let d = abs(mid - centerX)
if d < bestDist {
bestDist = d
best = i
}
}
return best
}
private func settleToNearest() {
guard !members.isEmpty else { return }
let idx = nearestCenterIndex()
centeredIndex = idx
let before = collectionView.contentOffset
scrollToIndex(idx, animated: true)
applyArcAndFocus()
let model = members[idx]
updateInfo(model)
onCenteredMember?(model)
// offset didEndScrollingAnimation
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let moved = abs(self.collectionView.contentOffset.x - before.x) > 0.5
if !moved && !self.isDragging {
self.playSwayOnce()
}
}
}
/// + 线 + 70×90 / 60×70 +
private func applyArcAndFocus() {
guard bounds.width > 0 else { return }
let scrollCenterX = collectionView.contentOffset.x + collectionView.bounds.width / 2
let span = itemSize.width + itemSpacing
for cell in collectionView.visibleCells {
guard let polaroid = cell as? MinePolaroidCell,
let indexPath = collectionView.indexPath(for: cell),
let attrs = collectionView.layoutAttributesForItem(at: indexPath) else { continue }
cell.transform = .identity
let dist = abs(attrs.center.x - scrollCenterX)
let t = isPairMode ? 0 : min(dist / span, 1)
// scale +
let fw = selectedSize.width + (unselectedSize.width - selectedSize.width) * t
let fh = selectedSize.height + (unselectedSize.height - selectedSize.height) * t
let frameInSelf = collectionView.convert(attrs.frame, to: self)
let attachX = frameInSelf.midX
let attachY = frameInSelf.minY + MinePolaroidCell.clipAttachOffset
let ty = ropeY(atX: attachX) - attachY + 2
let angle = ropeAngle(atX: attachX)
// 70%
let edgeT = min(
1,
max(0, frameInSelf.midX / edgeFadeWidth),
max(0, (bounds.width - frameInSelf.midX) / edgeFadeWidth)
)
let frameAlpha = 0.7 + 0.3 * edgeT
polaroid.setFrameAlpha(frameAlpha)
polaroid.applyFocus(frameSize: CGSize(width: fw, height: fh), angle: angle)
polaroid.setSelectedLook(isPairMode || dist < itemSize.width * 0.45, pairMode: isPairMode)
cell.transform = CGAffineTransform(translationX: 0, y: ty)
}
}
private func updateInfo(_ model: GroupMemberModel?) {
guard let model = model else {
titleLab.text = " "
titleLab.isHidden = false
pairHeartImg.isHidden = true
pairLeftLab.isHidden = true
pairRightLab.isHidden = true
daysRow.isHidden = true
return
}
if isPairMode {
titleLab.isHidden = true
pairHeartImg.isHidden = members.count < 2
pairLeftLab.isHidden = false
pairRightLab.isHidden = members.count < 2
let names = members.map { member -> String in
let name = member.nick_name.trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? "未设置昵称" : name
}
pairLeftLab.text = names.first ?? " "
pairRightLab.text = names.count >= 2 ? names[1] : ""
let selfMember = members.first { $0.user_id == AppContextManager.shared.userId } ?? model
daysLab.text = selfMember.joinDaysDisplay
} else {
titleLab.isHidden = false
pairHeartImg.isHidden = true
pairLeftLab.isHidden = true
pairRightLab.isHidden = true
let nickname = model.nick_name.trimmingCharacters(in: .whitespacesAndNewlines)
titleLab.text = nickname.isEmpty ? "未设置昵称" : nickname
daysLab.text = model.joinDaysDisplay
}
daysRow.isHidden = false
}
private func playSwayOnce() {
for cell in collectionView.visibleCells {
(cell as? MinePolaroidCell)?.playSwayOnce()
}
}
}
// MARK: - Collection
extension MinePhotoWallView: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
members.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell: MinePolaroidCell = collectionView.dequeueReusableCell(for: indexPath)
let model = members[indexPath.item]
let colorIdx = indexPath.item < clipColorIndices.count ? clipColorIndices[indexPath.item] : 0
cell.configure(
model: model,
isOwner: !ownerUserId.isEmpty && ownerUserId == model.user_id,
clipColorIndex: colorIdx
)
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// cell
DispatchQueue.main.async { [weak self] in
self?.applyArcAndFocus()
}
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let model = members[indexPath.item]
if isPairMode {
onTapMember?(model)
return
}
centeredIndex = indexPath.item
let before = collectionView.contentOffset
scrollToIndex(indexPath.item, animated: true)
applyArcAndFocus()
updateInfo(model)
onCenteredMember?(model)
onTapMember?(model)
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if abs(self.collectionView.contentOffset.x - before.x) < 0.5, !self.isDragging {
self.playSwayOnce()
}
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDragging = true
for cell in collectionView.visibleCells {
(cell as? MinePolaroidCell)?.stopSway()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
applyArcAndFocus()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
isDragging = false
settleToNearest()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
isDragging = false
settleToNearest()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
applyArcAndFocus()
if !isDragging {
playSwayOnce()
}
}
}
// MARK: - Flow layout
private final class MinePhotoWallFlowLayout: UICollectionViewFlowLayout {
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint
) -> CGPoint {
guard let cv = collectionView else { return proposedContentOffset }
let rect = CGRect(origin: proposedContentOffset, size: cv.bounds.size)
guard let attrs = layoutAttributesForElements(in: rect), !attrs.isEmpty else {
return proposedContentOffset
}
let centerX = proposedContentOffset.x + cv.bounds.width / 2
var best = attrs[0]
for a in attrs where abs(a.center.x - centerX) < abs(best.center.x - centerX) {
best = a
}
return CGPoint(x: best.center.x - cv.bounds.width / 2, y: proposedContentOffset.y)
}
}
// MARK: - Polaroid cell
final class MinePolaroidCell: UICollectionViewCell {
static let selectedFrameSize = CGSize(width: 70, height: 90)
static let unselectedFrameSize = CGSize(width: 60, height: 70)
/// 16×24
static let clipSize = CGSize(width: 16, height: 24)
static let clipAttachOffset: CGFloat = clipSize.height * 0.32
private static let frameTopInset: CGFloat = 12
private static let avatarInset: CGFloat = 5
static let clipAssetNames = [
"Mine/clothespin",
"Mine/clothespin_blue",
"Mine/clothespin_green",
"Mine/clothespin_pink"
]
private static let swayKey = "minePolaroidSway"
private var isOwner = false
private var relationIdx: String = ""
private var focusFrameSize = selectedFrameSize
private var focusAngle: CGFloat = 0
func configure(model: GroupMemberModel, isOwner: Bool, clipColorIndex: Int) {
self.isOwner = isOwner
self.relationIdx = model.extra.relation_idx
avatarImg.image = model.userIcon
ownerTag.isHidden = !isOwner
let idx = ((clipColorIndex % Self.clipAssetNames.count) + Self.clipAssetNames.count) % Self.clipAssetNames.count
clipImg.image = UIImage(named: Self.clipAssetNames[idx])
stopSway()
applyCaption(visible: true)
}
/// polaroidHost scale
func applyFocus(frameSize: CGSize, angle: CGFloat) {
focusFrameSize = frameSize
focusAngle = angle
layoutPolaroidHostIfNeeded()
polaroidHost.transform = CGAffineTransform(rotationAngle: angle)
}
func setFrameAlpha(_ alpha: CGFloat) {
frameView.alpha = alpha
clipImg.alpha = 1
}
func setSelectedLook(_ selected: Bool, pairMode: Bool = false) {
applyCaption(visible: pairMode || selected)
}
private func applyCaption(visible: Bool) {
let boundName = RelationStore.shared.name(for: relationIdx)
let isHeart = RelationStore.shared.showsHeart(for: relationIdx)
let bound = boundName != nil
if isOwner {
ownerTag.isHidden = false
heartImg.isHidden = !isHeart
relationLab.isHidden = true
layoutPolaroidHostIfNeeded()
return
}
ownerTag.isHidden = true
guard visible else {
heartImg.isHidden = true
relationLab.isHidden = true
return
}
if isHeart || !bound {
heartImg.isHidden = false
relationLab.isHidden = true
} else {
heartImg.isHidden = true
relationLab.isHidden = false
relationLab.text = boundName
}
layoutPolaroidHostIfNeeded()
}
func playSwayOnce() {
stopSway()
let anim = CAKeyframeAnimation(keyPath: "transform.rotation.z")
anim.values = [0, 0.1, 0, -0.1, 0]
anim.keyTimes = [0, 0.25, 0.5, 0.75, 1]
anim.duration = 1.6
anim.repeatCount = 1
anim.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
anim.isRemovedOnCompletion = true
anim.isAdditive = true
swayHost.layer.add(anim, forKey: Self.swayKey)
}
func stopSway() {
swayHost.layer.removeAnimation(forKey: Self.swayKey)
}
override func prepareForReuse() {
super.prepareForReuse()
stopSway()
heartImg.isHidden = true
relationLab.isHidden = true
ownerTag.isHidden = true
isOwner = false
relationIdx = ""
focusFrameSize = Self.selectedFrameSize
focusAngle = 0
polaroidHost.transform = .identity
transform = .identity
frameView.alpha = 1
clipImg.alpha = 1
}
override func layoutSubviews() {
super.layoutSubviews()
layoutPolaroidHostIfNeeded()
polaroidHost.transform = CGAffineTransform(rotationAngle: focusAngle)
}
/// frame host focusFrameSize
private func layoutPolaroidHostIfNeeded() {
let fw = focusFrameSize.width
let fh = focusFrameSize.height
let hostW = max(fw, Self.clipSize.width)
let hostH = Self.frameTopInset + fh
polaroidHost.bounds = CGRect(x: 0, y: 0, width: hostW, height: hostH)
let anchor = CGPoint(x: 0.5, y: Self.clipAttachOffset / max(hostH, 1))
polaroidHost.layer.anchorPoint = anchor
polaroidHost.layer.position = CGPoint(
x: contentView.bounds.midX,
y: Self.clipAttachOffset
)
swayHost.frame = polaroidHost.bounds
clipImg.frame = CGRect(
x: (hostW - Self.clipSize.width) / 2,
y: 0,
width: Self.clipSize.width,
height: Self.clipSize.height
)
frameView.frame = CGRect(x: (hostW - fw) / 2, y: Self.frameTopInset, width: fw, height: fh)
frameView.cornerRadius = 18 * (fw / Self.selectedFrameSize.width)
let avatarSide = max(0, fw - Self.avatarInset * 2)
avatarImg.frame = CGRect(
x: Self.avatarInset,
y: Self.avatarInset,
width: avatarSide,
height: avatarSide
)
avatarImg.cornerRadius = max(4, avatarSide * 0.2)
let captionMidY = Self.avatarInset + avatarSide + (fh - Self.avatarInset - avatarSide) / 2
layoutCaption(frameWidth: fw, midY: captionMidY)
}
private func layoutCaption(frameWidth fw: CGFloat, midY: CGFloat) {
heartImg.bounds = CGRect(x: 0, y: 0, width: 12, height: 10)
relationLab.sizeToFit()
ownerTag.sizeToFit()
var views: [UIView] = []
if !heartImg.isHidden { views.append(heartImg) }
if !relationLab.isHidden { views.append(relationLab) }
if !ownerTag.isHidden { views.append(ownerTag) }
let spacing: CGFloat = 2
let total = views.reduce(CGFloat(0)) { $0 + $1.bounds.width } + spacing * CGFloat(max(0, views.count - 1))
var x = (fw - total) / 2
for view in views {
view.center = CGPoint(x: x + view.bounds.width / 2, y: midY)
x += view.bounds.width + spacing
}
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.clipsToBounds = false
clipsToBounds = false
contentView.addSubview(polaroidHost)
polaroidHost.addSubview(swayHost)
swayHost.addSubview(frameView)
frameView.addSubview(avatarImg)
frameView.addSubview(heartImg)
frameView.addSubview(relationLab)
frameView.addSubview(ownerTag)
swayHost.addSubview(clipImg)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private lazy var polaroidHost: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
private lazy var swayHost: UIView = {
let v = UIView()
v.backgroundColor = .clear
return v
}()
private lazy var frameView: UIView = {
let v = UIView()
v.backgroundColor = .white
v.cornerRadius = 18
v.layer.shadowColor = UIColor.black.withAlphaComponent(0.12).cgColor
v.layer.shadowOffset = CGSize(width: 0, height: 2)
v.layer.shadowRadius = 4
v.layer.shadowOpacity = 1
return v
}()
private lazy var avatarImg: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
iv.cornerRadius = 12
iv.backgroundColor = UIColor(hexStr: "#E8EEF2")
return iv
}()
private lazy var clipImg: UIImageView = {
let iv = UIImageView(image: UIImage(named: "Mine/clothespin"))
iv.contentMode = .scaleAspectFit
return iv
}()
private lazy var heartImg: UIImageView = {
let iv = UIImageView()
if #available(iOS 13.0, *) {
iv.image = UIImage(systemName: "heart.fill")
iv.tintColor = UIColor(hexStr: "#FF6B9D")
}
iv.contentMode = .scaleAspectFit
iv.isHidden = true
return iv
}()
private lazy var relationLab: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 9, weight: .semibold)
label.textColor = UIColor(hexStr: "#FF6B9D")
label.textAlignment = .center
label.isHidden = true
return label
}()
private lazy var ownerTag: UILabel = {
let label = PaddingLabel()
label.text = "圈主"
label.font = .systemFont(ofSize: 9, weight: .semibold)
label.textColor = .white
label.backgroundColor = UIColor(hexStr: "#16B3FF")
label.cornerRadius = 4
label.clipsToBounds = true
label.textAlignment = .center
label.insets = UIEdgeInsets(top: 1, left: 5, bottom: 1, right: 5)
label.isHidden = true
return label
}()
}
private final class PaddingLabel: UILabel {
var insets = UIEdgeInsets.zero
override func drawText(in rect: CGRect) {
super.drawText(in: rect.inset(by: insets))
}
override var intrinsicContentSize: CGSize {
let size = super.intrinsicContentSize
return CGSize(width: size.width + insets.left + insets.right,
height: size.height + insets.top + insets.bottom)
}
}
extension GroupMemberModel {
var joinDaysDisplay: String {
if join_days > 0 { return "\(join_days)" }
if let joinDate {
let days = max(1, Int(Date().timeIntervalSince(joinDate) / 86400))
return "\(days)"
}
return "--"
}
}