// // 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 } }