321 lines
10 KiB
Swift
321 lines
10 KiB
Swift
//
|
||
// MemberAppUsageView.swift
|
||
// QuickLocation
|
||
//
|
||
|
||
import UIKit
|
||
|
||
struct MemberAppUsageItemPreview {
|
||
let rank: Int
|
||
let name: String
|
||
let tintHex: String
|
||
let durationText: String
|
||
let progress: CGFloat
|
||
}
|
||
|
||
struct MemberAppUsagePreview {
|
||
let totalTimeText: String
|
||
let segmentRatios: [CGFloat]
|
||
let segmentColors: [UIColor]
|
||
let topApps: [MemberAppUsageItemPreview]
|
||
|
||
static let mock = MemberAppUsagePreview(
|
||
totalTimeText: "24h 60m",
|
||
segmentRatios: [0.35, 0.28, 0.18, 0.19],
|
||
segmentColors: [
|
||
UIColor(hexStr: "#FF8FB1"),
|
||
UIColor(hexStr: "#FFD36E"),
|
||
UIColor(hexStr: "#7BE495"),
|
||
UIColor(hexStr: "#6EC1FF")
|
||
],
|
||
topApps: [
|
||
.init(rank: 1, name: "微信", tintHex: "#07C160", durationText: "1h 45m", progress: 1.0),
|
||
.init(rank: 2, name: "抖音", tintHex: "#111111", durationText: "1h 25m", progress: 0.76),
|
||
.init(rank: 3, name: "小红书", tintHex: "#FF2442", durationText: "45m", progress: 0.43),
|
||
.init(rank: 4, name: "美团", tintHex: "#FFC300", durationText: "30m", progress: 0.29),
|
||
.init(rank: 5, name: "Safari", tintHex: "#007AFF", durationText: "18m", progress: 0.17)
|
||
]
|
||
)
|
||
}
|
||
|
||
/// 成员面板「APP使用记录」:环形图 + TOP5
|
||
final class MemberAppUsageView: UIView {
|
||
|
||
private let titleColor = UIColor(hexStr: "#293445")
|
||
|
||
private let sectionIcon = UIImageView()
|
||
private let sectionTitleLab = UILabel()
|
||
private let cardView = UIView()
|
||
private let donutView = AppUsageDonutView()
|
||
private let totalTimeLab = UILabel()
|
||
private let totalHintLab = UILabel()
|
||
private let topTitleLab = UILabel()
|
||
private let topStack = UIStackView()
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
setupUI()
|
||
configure(with: .mock)
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
func configure(with preview: MemberAppUsagePreview?) {
|
||
topStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||
|
||
guard let preview else {
|
||
totalTimeLab.text = "--"
|
||
donutView.setSegments(ratios: [], colors: [])
|
||
return
|
||
}
|
||
|
||
totalTimeLab.text = preview.totalTimeText
|
||
donutView.setSegments(ratios: preview.segmentRatios, colors: preview.segmentColors)
|
||
|
||
for item in preview.topApps {
|
||
let row = AppUsageTopRowView()
|
||
row.configure(with: item)
|
||
topStack.addArrangedSubview(row)
|
||
}
|
||
}
|
||
|
||
private func setupUI() {
|
||
backgroundColor = .clear
|
||
|
||
sectionIcon.image = UIImage(named: "Home/member_app_usage")
|
||
sectionIcon.contentMode = .scaleAspectFit
|
||
addSubview(sectionIcon)
|
||
sectionIcon.layoutChain
|
||
.top()
|
||
.left(15)
|
||
.width(18)
|
||
.heightToWidth(1)
|
||
|
||
sectionTitleLab.text = "APP使用记录"
|
||
sectionTitleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
||
sectionTitleLab.textColor = titleColor
|
||
addSubview(sectionTitleLab)
|
||
sectionTitleLab.layoutChain
|
||
.leftToRightOfView(sectionIcon, offset: 6)
|
||
.centerY(sectionIcon)
|
||
|
||
cardView.backgroundColor = .white
|
||
cardView.cornerRadius = 16
|
||
addSubview(cardView)
|
||
cardView.layoutChain
|
||
.topToBottomOfView(sectionIcon, offset: 12)
|
||
.edgesHorzontal(15)
|
||
.height(220)
|
||
.bottom()
|
||
|
||
cardView.addSubview(donutView)
|
||
donutView.layoutChain
|
||
.left(12)
|
||
.centerY()
|
||
.width(110)
|
||
.height(110)
|
||
|
||
totalTimeLab.font = FontManager.youSheBiaoTiHei(14)
|
||
totalTimeLab.textColor = titleColor
|
||
totalTimeLab.textAlignment = .center
|
||
cardView.addSubview(totalTimeLab)
|
||
totalTimeLab.layoutChain
|
||
.centerX(donutView)
|
||
.centerY(donutView, offset: -6)
|
||
|
||
totalHintLab.text = "今日总计"
|
||
totalHintLab.font = .systemFont(ofSize: 9, weight: .medium)
|
||
totalHintLab.textColor = UIColor(hexStr: "#767676")
|
||
totalHintLab.textAlignment = .center
|
||
cardView.addSubview(totalHintLab)
|
||
totalHintLab.layoutChain
|
||
.centerX(donutView)
|
||
.topToBottomOfView(totalTimeLab, offset: 2)
|
||
|
||
topTitleLab.text = "最爱使用TOP5"
|
||
topTitleLab.font = .systemFont(ofSize: 12, weight: .bold)
|
||
topTitleLab.textColor = titleColor
|
||
cardView.addSubview(topTitleLab)
|
||
topTitleLab.layoutChain
|
||
.top(14)
|
||
.leftToRightOfView(donutView, offset: 16)
|
||
.right(12)
|
||
|
||
topStack.axis = .vertical
|
||
topStack.spacing = 10
|
||
topStack.alignment = .fill
|
||
cardView.addSubview(topStack)
|
||
topStack.layoutChain
|
||
.topToBottomOfView(topTitleLab, offset: 10)
|
||
.leftToRightOfView(donutView, offset: 16)
|
||
.right(12)
|
||
.bottom(14)
|
||
}
|
||
}
|
||
|
||
// MARK: - 环形图
|
||
|
||
private final class AppUsageDonutView: UIView {
|
||
|
||
private var segmentLayers: [CAShapeLayer] = []
|
||
private var ratios: [CGFloat] = []
|
||
private var colors: [UIColor] = []
|
||
private let lineWidth: CGFloat = 14
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
backgroundColor = .clear
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
func setSegments(ratios: [CGFloat], colors: [UIColor]) {
|
||
self.ratios = ratios
|
||
self.colors = colors
|
||
redraw()
|
||
}
|
||
|
||
private func redraw() {
|
||
segmentLayers.forEach { $0.removeFromSuperlayer() }
|
||
segmentLayers.removeAll()
|
||
guard !ratios.isEmpty, bounds.width > 0 else { return }
|
||
|
||
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
||
let radius = min(bounds.width, bounds.height) / 2 - lineWidth / 2
|
||
var startAngle = -CGFloat.pi / 2
|
||
let total = max(ratios.reduce(0, +), 0.001)
|
||
|
||
for (i, ratio) in ratios.enumerated() {
|
||
let sweep = (ratio / total) * CGFloat.pi * 2
|
||
guard sweep > 0.001 else { continue }
|
||
let path = UIBezierPath(arcCenter: center,
|
||
radius: radius,
|
||
startAngle: startAngle,
|
||
endAngle: startAngle + sweep,
|
||
clockwise: true)
|
||
let layer = CAShapeLayer()
|
||
layer.path = path.cgPath
|
||
layer.strokeColor = colors[min(i, colors.count - 1)].cgColor
|
||
layer.fillColor = UIColor.clear.cgColor
|
||
layer.lineWidth = lineWidth
|
||
layer.lineCap = .butt
|
||
self.layer.addSublayer(layer)
|
||
segmentLayers.append(layer)
|
||
startAngle += sweep
|
||
}
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
redraw()
|
||
}
|
||
}
|
||
|
||
// MARK: - TOP 行
|
||
|
||
private final class AppUsageTopRowView: UIView {
|
||
|
||
private var progressRatio: CGFloat = 0
|
||
private var progressWidthConstraint: NSLayoutConstraint?
|
||
private let rankLab = UILabel()
|
||
private let iconView = UIView()
|
||
private let iconLabel = UILabel()
|
||
private let nameLab = UILabel()
|
||
private let progressTrack = UIView()
|
||
private let progressFill = UIView()
|
||
private let durationLab = UILabel()
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
setupUI()
|
||
}
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
func configure(with item: MemberAppUsageItemPreview) {
|
||
rankLab.text = "\(item.rank)"
|
||
nameLab.text = item.name
|
||
durationLab.text = item.durationText
|
||
iconView.backgroundColor = UIColor(hexStr: item.tintHex)
|
||
iconLabel.text = String(item.name.prefix(1))
|
||
progressRatio = max(0.06, min(1, item.progress))
|
||
setNeedsLayout()
|
||
}
|
||
|
||
private func setupUI() {
|
||
rankLab.font = .systemFont(ofSize: 10, weight: .bold)
|
||
rankLab.textColor = UIColor(hexStr: "#353B4F")
|
||
rankLab.textAlignment = .center
|
||
addSubview(rankLab)
|
||
rankLab.layoutChain
|
||
.left()
|
||
.centerY()
|
||
.width(14)
|
||
|
||
iconView.cornerRadius = 8
|
||
addSubview(iconView)
|
||
iconView.layoutChain
|
||
.leftToRightOfView(rankLab, offset: 4)
|
||
.centerY()
|
||
.width(16)
|
||
.height(16)
|
||
|
||
iconLabel.font = .systemFont(ofSize: 9, weight: .bold)
|
||
iconLabel.textColor = .white
|
||
iconLabel.textAlignment = .center
|
||
iconView.addSubview(iconLabel)
|
||
iconLabel.layoutChain.centerX().centerY()
|
||
|
||
durationLab.font = .systemFont(ofSize: 10, weight: .medium)
|
||
durationLab.textColor = UIColor(hexStr: "#767676")
|
||
durationLab.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||
addSubview(durationLab)
|
||
durationLab.layoutChain
|
||
.right()
|
||
.centerY()
|
||
|
||
nameLab.font = .systemFont(ofSize: 11, weight: .medium)
|
||
nameLab.textColor = UIColor(hexStr: "#353B4F")
|
||
addSubview(nameLab)
|
||
nameLab.layoutChain
|
||
.leftToRightOfView(iconView, offset: 6)
|
||
.top()
|
||
.rightToLeftOfView(durationLab, offset: -6)
|
||
|
||
progressTrack.backgroundColor = UIColor(hexStr: "#F0F0F0")
|
||
progressTrack.cornerRadius = 2
|
||
addSubview(progressTrack)
|
||
progressTrack.layoutChain
|
||
.leftToView(nameLab)
|
||
.rightToView(durationLab)
|
||
.topToBottomOfView(nameLab, offset: 4)
|
||
.height(4)
|
||
.bottom()
|
||
|
||
progressFill.backgroundColor = UIColor(hexStr: "#58EDFF")
|
||
progressFill.cornerRadius = 2
|
||
progressTrack.addSubview(progressFill)
|
||
progressFill.layoutChain
|
||
.left()
|
||
.edgesVertical()
|
||
progressWidthConstraint = progressFill.widthAnchor.constraint(equalTo: progressTrack.widthAnchor, multiplier: 0)
|
||
progressWidthConstraint?.isActive = true
|
||
}
|
||
|
||
override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
progressWidthConstraint?.isActive = false
|
||
progressWidthConstraint = progressFill.widthAnchor.constraint(
|
||
equalTo: progressTrack.widthAnchor,
|
||
multiplier: progressRatio
|
||
)
|
||
progressWidthConstraint?.isActive = true
|
||
}
|
||
}
|