// // MemberAppUsageView.swift // QuickLocation // import UIKit import Kingfisher struct MemberAppUsageItemPreview { let rank: Int let name: String let iconURL: String let durationText: String let progress: CGFloat } struct MemberAppUsagePreview { let totalTimeText: String let segmentRatios: [CGFloat] let segmentColors: [UIColor] let topApps: [MemberAppUsageItemPreview] init?(topApps: [PhoneUsageAppModel]) { let apps = Array(topApps.filter { $0.usageTime > 0 }.prefix(5)) guard !apps.isEmpty else { return nil } let usageTimes = apps.map { max($0.usageTime, 0) } let totalUsageTime = usageTimes.reduce(0, +) let colors = Self.segmentColors totalTimeText = Self.durationText(seconds: totalUsageTime) segmentRatios = usageTimes.map(CGFloat.init) segmentColors = usageTimes.enumerated().map { colors[$0.offset % colors.count] } self.topApps = apps.enumerated().map { index, app in MemberAppUsageItemPreview( rank: index + 1, name: app.appName.isEmpty ? "未知应用" : app.appName, iconURL: app.appIcon, durationText: Self.durationText(seconds: max(app.usageTime, 0)), progress: CGFloat(max(app.usageTime, 0)) / CGFloat(24 * 60 * 60) ) } } private static let segmentColors = [ UIColor(hexStr: "#C3ECFF"), UIColor(hexStr: "#E3FF9C"), UIColor(hexStr: "#FFEAA4"), UIColor(hexStr: "#C5FFB7"), UIColor(hexStr: "#FFDCF5") ] private static func durationText(seconds: Int) -> String { let hours = seconds / 3600 let minutes = (seconds % 3600) / 60 if hours > 0 { return minutes > 0 ? "\(hours)h \(minutes)m" : "\(hours)h" } return "\(minutes)m" } } /// 成员面板「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 usageTitleLab = UILabel() private let topTitleLab = UILabel() private let topStack = UIStackView() override init(frame: CGRect) { super.init(frame: frame) setupUI() configure(with: nil) } 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) row.layoutChain.height(20) } } 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(176) .bottom() usageTitleLab.attributedText = Self.cardTitle("使用时长") cardView.addSubview(usageTitleLab) usageTitleLab.layoutChain .top(12) .left(18) topTitleLab.attributedText = Self.cardTitle("最爱使用TOP5") cardView.addSubview(topTitleLab) topTitleLab.layoutChain .top(12) .left(160) .right(14) cardView.addSubview(donutView) donutView.layoutChain .left(18) .top(43) .width(120) .height(120) totalTimeLab.font = .systemFont(ofSize: 16, weight: .semibold) totalTimeLab.textColor = titleColor totalTimeLab.textAlignment = .center cardView.addSubview(totalTimeLab) totalTimeLab.layoutChain .centerX(donutView) .centerY(donutView, offset: -6) totalHintLab.text = "今日总计" totalHintLab.font = .systemFont(ofSize: 10, weight: .medium) totalHintLab.textColor = UIColor(hexStr: "#767676") totalHintLab.textAlignment = .center cardView.addSubview(totalHintLab) totalHintLab.layoutChain .centerX(donutView) .topToBottomOfView(totalTimeLab, offset: 2) topStack.axis = .vertical topStack.spacing = 6 topStack.alignment = .fill cardView.addSubview(topStack) topStack.layoutChain .top(43) .left(160) .right(14) } private static func cardTitle(_ title: String) -> NSAttributedString { let attributedText = NSMutableAttributedString( string: "✦ \(title)", attributes: [ .font: UIFont.systemFont(ofSize: 12, weight: .semibold), .foregroundColor: UIColor(hexStr: "#293445") ] ) attributedText.addAttribute( .foregroundColor, value: UIColor(hexStr: "#A9F33D"), range: NSRange(location: 0, length: 1) ) return attributedText } } // MARK: - 环形图 private final class AppUsageDonutView: UIView { private var segmentLayers: [CAShapeLayer] = [] private var ratios: [CGFloat] = [] private var colors: [UIColor] = [] private let lineWidth: CGFloat = 18 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) let segmentGap: CGFloat = ratios.count > 1 ? 0.025 : 0 for (i, ratio) in ratios.enumerated() { let sweep = (ratio / total) * CGFloat.pi * 2 guard sweep > 0.001 else { continue } let gap = min(segmentGap, sweep * 0.25) let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle + gap, endAngle: startAngle + sweep - gap, 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 = UIImageView() 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.kf.cancelDownloadTask() iconView.image = UIImage(named: "LockDistract/app_unknown") if let url = URL(string: item.iconURL), !item.iconURL.isEmpty { iconView.kf.setImage(with: url, placeholder: iconView.image) } progressRatio = max(0, min(1, item.progress)) setNeedsLayout() } private func setupUI() { rankLab.font = .systemFont(ofSize: 11, weight: .bold) rankLab.textColor = UIColor(hexStr: "#353B4F") rankLab.textAlignment = .center addSubview(rankLab) rankLab.layoutChain .left() .centerY() .width(12) iconView.backgroundColor = UIColor(hexStr: "#F4F6F8") iconView.contentMode = .scaleAspectFill iconView.clipsToBounds = true iconView.cornerRadius = 4 addSubview(iconView) iconView.layoutChain .leftToRightOfView(rankLab, offset: 4) .centerY() .width(16) .height(16) durationLab.font = .systemFont(ofSize: 10, weight: .regular) durationLab.textColor = UIColor(hexStr: "#A8A8A8") durationLab.textAlignment = .right durationLab.setContentCompressionResistancePriority(.required, for: .horizontal) addSubview(durationLab) durationLab.layoutChain .right() .centerY() .width(40) nameLab.font = .systemFont(ofSize: 10, weight: .medium) nameLab.textColor = UIColor(hexStr: "#353B4F") nameLab.lineBreakMode = .byTruncatingTail addSubview(nameLab) nameLab.layoutChain .leftToRightOfView(iconView, offset: 5) .centerY() .width(45) progressTrack.backgroundColor = UIColor(hexStr: "#DDF3FF") progressTrack.cornerRadius = 2 addSubview(progressTrack) progressTrack.layoutChain .leftToRightOfView(nameLab, offset: 5) .rightToLeftOfView(durationLab, offset: -7) .centerY() .height(4) progressFill.backgroundColor = UIColor(hexStr: "#58C9F7") 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 } }