jsdw_ios/QuickLocation/Section/Home/MemberAppUsageView.swift

367 lines
12 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.

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