jsdw_ios/QuickLocation/Section/Mine/MinePhotoWallView.swift

861 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,
isCoupleGroup: Bool = false
) {
isPairMode = isCoupleGroup && members.count >= 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
}
let selfColor = UIColor(hexStr: "#16B3FF")
let otherColor = UIColor(hexStr: "#293445")
let currentUserId = AppContextManager.shared.userId
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.showName.trimmingCharacters(in: .whitespacesAndNewlines)
return name.isEmpty ? "未设置昵称" : name
}
pairLeftLab.text = names.first ?? " "
pairRightLab.text = names.count >= 2 ? names[1] : ""
pairLeftLab.textColor = otherColor
pairRightLab.textColor = otherColor
let selfMember = members.first { $0.user_id == currentUserId } ?? model
daysLab.text = selfMember.joinDaysDisplay
} else {
titleLab.isHidden = false
pairHeartImg.isHidden = true
pairLeftLab.isHidden = true
pairRightLab.isHidden = true
let nickname = model.showName.trimmingCharacters(in: .whitespacesAndNewlines)
titleLab.text = nickname.isEmpty ? "未设置昵称" : nickname
titleLab.textColor = model.user_id == currentUserId ? selfColor : otherColor
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 relationIconAvailable = false
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.setHeadPic(model.displayHeadPic)
ownerTag.isHidden = !isOwner
relationIconView.configure(relationIdx: relationIdx) { [weak self] isVisible in
guard let self else { return }
self.relationIconAvailable = isVisible
self.applyCaption()
}
let idx = ((clipColorIndex % Self.clipAssetNames.count) + Self.clipAssetNames.count) % Self.clipAssetNames.count
clipImg.image = UIImage(named: Self.clipAssetNames[idx])
stopSway()
applyCaption()
}
/// 按真实相框尺寸布局,整块 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()
}
private func applyCaption() {
ownerTag.isHidden = !isOwner
relationIconView.isHidden = !relationIconAvailable
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()
relationIconView.clear()
ownerTag.isHidden = true
isOwner = false
relationIdx = ""
relationIconAvailable = false
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) {
relationIconView.bounds = CGRect(x: 0, y: 0, width: 12, height: 12)
let ownerTagSize = ownerTag.intrinsicContentSize
ownerTag.bounds = CGRect(
x: 0,
y: 0,
width: ceil(ownerTagSize.width),
height: ceil(ownerTagSize.height)
)
var views: [UIView] = []
if !relationIconView.isHidden { views.append(relationIconView) }
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(relationIconView)
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 relationIconView = RelationIconImageView()
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.numberOfLines = 1
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 "--"
}
}