647 lines
23 KiB
Swift
647 lines
23 KiB
Swift
//
|
|
// AppUsageReportView.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
import Kingfisher
|
|
|
|
final class AppUsageReportView: UIView {
|
|
|
|
lazy var navView = BaseNavigationView(title: "APP报告")
|
|
let memberCV: UICollectionView = {
|
|
let layout = UICollectionViewFlowLayout()
|
|
layout.scrollDirection = .horizontal
|
|
layout.itemSize = CGSize(width: 61, height: 90)
|
|
layout.minimumLineSpacing = 12
|
|
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
view.backgroundColor = .white
|
|
view.showsHorizontalScrollIndicator = false
|
|
view.contentInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
|
|
view.register(GroupMemberListCell.self)
|
|
view.applyAppUsageReportCardStyle(cornerRadius: 20)
|
|
return view
|
|
}()
|
|
|
|
var onPeriodChanged: ((AppUsageReportPeriod) -> Void)?
|
|
|
|
private let navBgView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
|
|
private let scrollView = UIScrollView()
|
|
private let contentView = UIView()
|
|
private let frequentSection = AppUsageFrequentSectionView()
|
|
private let durationSection = AppUsageDurationSectionView()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
|
setupUI()
|
|
bindPeriodControls()
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
func apply(_ snapshot: AppUsageReportViewModel.Snapshot) {
|
|
frequentSection.setPeriod(snapshot.period)
|
|
durationSection.setPeriod(snapshot.period)
|
|
frequentSection.configure(apps: snapshot.apps, period: snapshot.period, state: snapshot.state)
|
|
durationSection.configure(
|
|
apps: snapshot.apps,
|
|
totalUsageTime: snapshot.totalUsageTime,
|
|
period: snapshot.period,
|
|
state: snapshot.state
|
|
)
|
|
}
|
|
|
|
private func bindPeriodControls() {
|
|
let onChange: (AppUsageReportPeriod) -> Void = { [weak self] period in
|
|
self?.onPeriodChanged?(period)
|
|
}
|
|
frequentSection.onPeriodChanged = onChange
|
|
durationSection.onPeriodChanged = onChange
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(navBgView)
|
|
addSubview(navView)
|
|
navBgView.contentMode = .scaleAspectFill
|
|
navBgView.layoutChain.edges(excludingEdge: .bottom).heightToWidth(160 / 375)
|
|
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
|
|
|
|
addSubview(scrollView)
|
|
scrollView.showsVerticalScrollIndicator = false
|
|
scrollView.layoutChain.topToBottomOfView(navView).edgesHorzontal().bottom()
|
|
scrollView.addSubview(contentView)
|
|
contentView.layoutChain.edges().widthToView(scrollView)
|
|
|
|
contentView.addSubview(memberCV)
|
|
memberCV.layoutChain.top(8).edgesHorzontal(15).height(100)
|
|
|
|
contentView.addSubview(frequentSection)
|
|
frequentSection.layoutChain.topToBottomOfView(memberCV, offset: 14).edgesHorzontal(15).height(253)
|
|
|
|
contentView.addSubview(durationSection)
|
|
durationSection.layoutChain
|
|
.topToBottomOfView(frequentSection, offset: 14)
|
|
.edgesHorzontal(15)
|
|
.height(220)
|
|
.bottom(24 + kSafeBottomMargin)
|
|
}
|
|
}
|
|
|
|
private final class AppUsageSectionIconView: UIView {
|
|
|
|
private let iconView = UIImageView()
|
|
|
|
init(symbolName: String) {
|
|
super.init(frame: .zero)
|
|
backgroundColor = UIColor(hexStr: "#293445")
|
|
iconView.image = UIImage(
|
|
systemName: symbolName,
|
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: 9, weight: .bold)
|
|
)
|
|
iconView.tintColor = .white
|
|
iconView.contentMode = .scaleAspectFit
|
|
addSubview(iconView)
|
|
iconView.layoutChain.centerX().centerY().width(10).height(10)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
layer.cornerRadius = min(bounds.width, bounds.height) / 2
|
|
}
|
|
}
|
|
|
|
private enum AppUsageReportStyle {
|
|
static func cardTitle(_ title: String) -> NSAttributedString {
|
|
let attributedText = NSMutableAttributedString(
|
|
string: "✦ \(title)",
|
|
attributes: [
|
|
.font: UIFont.systemFont(ofSize: 13, weight: .semibold),
|
|
.foregroundColor: UIColor(hexStr: "#293445")
|
|
]
|
|
)
|
|
attributedText.addAttribute(
|
|
.foregroundColor,
|
|
value: UIColor(hexStr: "#A9F33D"),
|
|
range: NSRange(location: 0, length: 1)
|
|
)
|
|
return attributedText
|
|
}
|
|
}
|
|
|
|
private final class AppUsageFrequentSectionView: UIView {
|
|
|
|
var onPeriodChanged: ((AppUsageReportPeriod) -> Void)?
|
|
|
|
private let titleIcon = AppUsageSectionIconView(symbolName: "heart.fill")
|
|
private let titleLab = UILabel()
|
|
private let cardView = UIView()
|
|
private let cardTitleLab = UILabel()
|
|
private let periodControl = AppUsagePeriodControl()
|
|
private let rowsStack = UIStackView()
|
|
private let statusLab = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
periodControl.onChanged = { [weak self] period in
|
|
self?.onPeriodChanged?(period)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
|
|
func setPeriod(_ period: AppUsageReportPeriod) {
|
|
periodControl.setPeriod(period)
|
|
}
|
|
|
|
func configure(
|
|
apps: [AppUsageReportViewModel.AppItem],
|
|
period: AppUsageReportPeriod,
|
|
state: AppUsageReportViewModel.ContentState
|
|
) {
|
|
rowsStack.arrangedSubviews.forEach {
|
|
rowsStack.removeArrangedSubview($0)
|
|
$0.removeFromSuperview()
|
|
}
|
|
|
|
let statusText = AppUsageReportStatus.text(for: state)
|
|
statusLab.text = statusText
|
|
statusLab.isHidden = statusText == nil
|
|
rowsStack.isHidden = statusText != nil
|
|
|
|
guard statusText == nil else { return }
|
|
for app in apps {
|
|
let row = AppUsageRankRowView()
|
|
row.configure(app: app, maximumUsageSeconds: period.maximumUsageSeconds)
|
|
row.layoutChain.height(20)
|
|
rowsStack.addArrangedSubview(row)
|
|
}
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(titleIcon)
|
|
titleIcon.layoutChain.top().left().width(18).height(18)
|
|
|
|
titleLab.text = "经常使用"
|
|
titleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
|
titleLab.textColor = UIColor(hexStr: "#293445")
|
|
addSubview(titleLab)
|
|
titleLab.layoutChain.leftToRightOfView(titleIcon, offset: 7).centerY(titleIcon)
|
|
|
|
cardView.backgroundColor = .white
|
|
cardView.applyAppUsageReportCardStyle(cornerRadius: 20)
|
|
addSubview(cardView)
|
|
cardView.layoutChain.topToBottomOfView(titleIcon, offset: 12).edgesHorzontal().bottom()
|
|
|
|
cardTitleLab.attributedText = AppUsageReportStyle.cardTitle("最爱使用TOP5")
|
|
cardView.addSubview(cardTitleLab)
|
|
cardTitleLab.layoutChain.top(22).left(20)
|
|
|
|
cardView.addSubview(periodControl)
|
|
periodControl.layoutChain.top(12).right(14).width(110).height(32)
|
|
|
|
rowsStack.axis = .vertical
|
|
rowsStack.spacing = 12
|
|
cardView.addSubview(rowsStack)
|
|
rowsStack.layoutChain.top(62).left(20).right(14)
|
|
|
|
statusLab.font = .systemFont(ofSize: 14, weight: .medium)
|
|
statusLab.textColor = UIColor(hexStr: "#9CA3AF")
|
|
statusLab.textAlignment = .center
|
|
statusLab.numberOfLines = 0
|
|
cardView.addSubview(statusLab)
|
|
statusLab.layoutChain.centerX().centerY(cardView, offset: 16).left(24).right(24)
|
|
}
|
|
|
|
}
|
|
|
|
private final class AppUsageDurationSectionView: UIView {
|
|
|
|
var onPeriodChanged: ((AppUsageReportPeriod) -> Void)?
|
|
|
|
private let titleIcon = AppUsageSectionIconView(symbolName: "clock.fill")
|
|
private let titleLab = UILabel()
|
|
private let cardView = UIView()
|
|
private let cardTitleLab = UILabel()
|
|
private let periodControl = AppUsagePeriodControl()
|
|
private let donutView = AppUsageReportDonutView()
|
|
private let totalTimeLab = UILabel()
|
|
private let totalHintLab = UILabel()
|
|
private let legendStack = UIStackView()
|
|
private let statusLab = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
periodControl.onChanged = { [weak self] period in
|
|
self?.onPeriodChanged?(period)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:)") }
|
|
|
|
func setPeriod(_ period: AppUsageReportPeriod) {
|
|
periodControl.setPeriod(period)
|
|
}
|
|
|
|
func configure(
|
|
apps: [AppUsageReportViewModel.AppItem],
|
|
totalUsageTime: Int,
|
|
period: AppUsageReportPeriod,
|
|
state: AppUsageReportViewModel.ContentState
|
|
) {
|
|
legendStack.arrangedSubviews.forEach {
|
|
legendStack.removeArrangedSubview($0)
|
|
$0.removeFromSuperview()
|
|
}
|
|
|
|
let statusText = AppUsageReportStatus.text(for: state)
|
|
statusLab.text = statusText
|
|
statusLab.isHidden = statusText == nil
|
|
donutView.isHidden = statusText != nil
|
|
totalTimeLab.isHidden = statusText != nil
|
|
totalHintLab.isHidden = statusText != nil
|
|
legendStack.isHidden = statusText != nil
|
|
|
|
guard statusText == nil else {
|
|
donutView.setSegments(ratios: [], colors: [])
|
|
return
|
|
}
|
|
|
|
totalTimeLab.text = AppUsageReportFormat.durationText(seconds: totalUsageTime)
|
|
donutView.setSegments(ratios: apps.map { CGFloat($0.usageTime) }, colors: apps.map(\.color))
|
|
for app in apps {
|
|
let row = AppUsageLegendRowView()
|
|
row.configure(
|
|
app: app,
|
|
progress: CGFloat(app.usageTime) / CGFloat(period.maximumUsageSeconds)
|
|
)
|
|
row.layoutChain.height(20)
|
|
legendStack.addArrangedSubview(row)
|
|
}
|
|
}
|
|
|
|
private func setupUI() {
|
|
addSubview(titleIcon)
|
|
titleIcon.layoutChain.top().left().width(18).height(18)
|
|
|
|
titleLab.text = "使用时长"
|
|
titleLab.font = .systemFont(ofSize: 16, weight: .bold)
|
|
titleLab.textColor = UIColor(hexStr: "#293445")
|
|
addSubview(titleLab)
|
|
titleLab.layoutChain.leftToRightOfView(titleIcon, offset: 7).centerY(titleIcon)
|
|
|
|
cardView.backgroundColor = .white
|
|
cardView.applyAppUsageReportCardStyle(cornerRadius: 20)
|
|
addSubview(cardView)
|
|
cardView.layoutChain.topToBottomOfView(titleIcon, offset: 12).edgesHorzontal().bottom()
|
|
|
|
cardTitleLab.attributedText = AppUsageReportStyle.cardTitle("App使用详情")
|
|
cardView.addSubview(cardTitleLab)
|
|
cardTitleLab.layoutChain.top(22).left(20)
|
|
|
|
cardView.addSubview(periodControl)
|
|
periodControl.layoutChain.top(12).right(14).width(110).height(32)
|
|
|
|
cardView.addSubview(donutView)
|
|
donutView.layoutChain.left(20).top(56).width(122).height(122)
|
|
|
|
totalTimeLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
totalTimeLab.textColor = UIColor(hexStr: "#293445")
|
|
totalTimeLab.textAlignment = .center
|
|
cardView.addSubview(totalTimeLab)
|
|
totalTimeLab.layoutChain.centerX(donutView).centerY(donutView, offset: -8)
|
|
|
|
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: 4)
|
|
|
|
legendStack.axis = .vertical
|
|
legendStack.spacing = 5
|
|
cardView.addSubview(legendStack)
|
|
legendStack.layoutChain
|
|
.top(54)
|
|
.left(154)
|
|
.right(14)
|
|
|
|
statusLab.font = .systemFont(ofSize: 14, weight: .medium)
|
|
statusLab.textColor = UIColor(hexStr: "#9CA3AF")
|
|
statusLab.textAlignment = .center
|
|
statusLab.numberOfLines = 0
|
|
cardView.addSubview(statusLab)
|
|
statusLab.layoutChain.centerX().centerY(cardView, offset: 18).left(24).right(24)
|
|
}
|
|
}
|
|
|
|
private final class AppUsagePeriodControl: UIView {
|
|
|
|
var onChanged: ((AppUsageReportPeriod) -> Void)?
|
|
|
|
private let backgroundView = UIView()
|
|
private let monthButton = UIButton(type: .custom)
|
|
private let weekButton = UIButton(type: .custom)
|
|
private var period: AppUsageReportPeriod = .week
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
backgroundColor = UIColor(hexStr: "#EEEEEE")
|
|
cornerRadius = 16
|
|
setupUI()
|
|
setPeriod(.week)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:)") }
|
|
|
|
func setPeriod(_ period: AppUsageReportPeriod) {
|
|
self.period = period
|
|
let selectedColor = UIColor(hexStr: "#293445")
|
|
let normalColor = UIColor(hexStr: "#8C8C8C")
|
|
monthButton.setTitleColor(period == .month ? selectedColor : normalColor, for: .normal)
|
|
weekButton.setTitleColor(period == .week ? selectedColor : normalColor, for: .normal)
|
|
backgroundView.frame = period == .month
|
|
? CGRect(x: 3, y: 3, width: bounds.width / 2 - 3, height: bounds.height - 6)
|
|
: CGRect(x: bounds.width / 2, y: 3, width: bounds.width / 2 - 3, height: bounds.height - 6)
|
|
setNeedsLayout()
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
let width = bounds.width / 2 - 4
|
|
backgroundView.frame = period == .month
|
|
? CGRect(x: 3, y: 3, width: width + 1, height: bounds.height - 6)
|
|
: CGRect(x: bounds.width / 2, y: 3, width: width + 1, height: bounds.height - 6)
|
|
}
|
|
|
|
private func setupUI() {
|
|
backgroundView.backgroundColor = .white
|
|
backgroundView.cornerRadius = 13
|
|
addSubview(backgroundView)
|
|
sendSubviewToBack(backgroundView)
|
|
|
|
[monthButton, weekButton].forEach {
|
|
$0.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
|
addSubview($0)
|
|
}
|
|
monthButton.setTitle("月", for: .normal)
|
|
weekButton.setTitle("周", for: .normal)
|
|
monthButton.layoutChain.left().top().bottom().widthToView(self, multiplier: 0.5)
|
|
weekButton.layoutChain.right().top().bottom().widthToView(self, multiplier: 0.5)
|
|
monthButton.addTarget(self, action: #selector(selectMonth), for: .touchUpInside)
|
|
weekButton.addTarget(self, action: #selector(selectWeek), for: .touchUpInside)
|
|
}
|
|
|
|
@objc private func selectMonth() {
|
|
guard period != .month else { return }
|
|
onChanged?(.month)
|
|
}
|
|
|
|
@objc private func selectWeek() {
|
|
guard period != .week else { return }
|
|
onChanged?(.week)
|
|
}
|
|
}
|
|
|
|
private final class AppUsageRankRowView: UIView {
|
|
|
|
private var progressRatio: CGFloat = 0
|
|
private var progressConstraint: NSLayoutConstraint?
|
|
private let rankLab = UILabel()
|
|
private let iconView = UIImageView()
|
|
private let nameLab = UILabel()
|
|
private let trackView = UIView()
|
|
private let fillView = UIView()
|
|
private let percentageLab = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:)") }
|
|
|
|
func configure(app: AppUsageReportViewModel.AppItem, maximumUsageSeconds: Int) {
|
|
rankLab.text = "\(app.rank)"
|
|
nameLab.text = app.name
|
|
let periodProgress = min(max(CGFloat(app.usageTime) / CGFloat(maximumUsageSeconds), 0), 1)
|
|
percentageLab.text = "\(Int((periodProgress * 100).rounded()))%"
|
|
iconView.kf.cancelDownloadTask()
|
|
iconView.image = UIImage(named: "LockDistract/app_unknown")
|
|
if let url = URL(string: app.iconURL), !app.iconURL.isEmpty {
|
|
iconView.kf.setImage(with: url, placeholder: iconView.image)
|
|
}
|
|
progressRatio = periodProgress
|
|
setNeedsLayout()
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
progressConstraint?.isActive = false
|
|
progressConstraint = fillView.widthAnchor.constraint(equalTo: trackView.widthAnchor, multiplier: progressRatio)
|
|
progressConstraint?.isActive = true
|
|
}
|
|
|
|
private func setupUI() {
|
|
rankLab.font = .systemFont(ofSize: 12, weight: .bold)
|
|
rankLab.textColor = UIColor(hexStr: "#687385")
|
|
rankLab.textAlignment = .center
|
|
addSubview(rankLab)
|
|
rankLab.layoutChain.left().centerY().width(14)
|
|
|
|
iconView.backgroundColor = UIColor(hexStr: "#F4F6F8")
|
|
iconView.image = UIImage(named: "LockDistract/app_unknown")
|
|
iconView.contentMode = .scaleAspectFill
|
|
iconView.cornerRadius = 4
|
|
iconView.clipsToBounds = true
|
|
addSubview(iconView)
|
|
iconView.layoutChain.leftToRightOfView(rankLab, offset: 8).centerY().width(16).height(16)
|
|
|
|
nameLab.font = .systemFont(ofSize: 11, weight: .medium)
|
|
nameLab.textColor = UIColor(hexStr: "#4B5563")
|
|
nameLab.lineBreakMode = .byTruncatingTail
|
|
addSubview(nameLab)
|
|
nameLab.layoutChain.leftToRightOfView(iconView, offset: 7).centerY().width(62)
|
|
|
|
percentageLab.font = .systemFont(ofSize: 10, weight: .regular)
|
|
percentageLab.textColor = UIColor(hexStr: "#A8A8A8")
|
|
percentageLab.textAlignment = .right
|
|
addSubview(percentageLab)
|
|
percentageLab.layoutChain.right().centerY().width(34)
|
|
|
|
trackView.backgroundColor = UIColor(hexStr: "#DDF3FF")
|
|
trackView.cornerRadius = 2
|
|
addSubview(trackView)
|
|
trackView.layoutChain.leftToRightOfView(nameLab, offset: 8).rightToLeftOfView(percentageLab, offset: -8).centerY().height(4)
|
|
|
|
fillView.backgroundColor = UIColor(hexStr: "#5BC5F7")
|
|
fillView.cornerRadius = 2
|
|
trackView.addSubview(fillView)
|
|
fillView.layoutChain.left().top().bottom()
|
|
}
|
|
}
|
|
|
|
private final class AppUsageLegendRowView: UIView {
|
|
|
|
private var progressRatio: CGFloat = 0
|
|
private var progressConstraint: NSLayoutConstraint?
|
|
private let colorDot = UIView()
|
|
private let iconView = UIImageView()
|
|
private let nameLab = UILabel()
|
|
private let trackView = UIView()
|
|
private let fillView = UIView()
|
|
private let durationLab = UILabel()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setupUI()
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:)") }
|
|
|
|
func configure(app: AppUsageReportViewModel.AppItem, progress: CGFloat) {
|
|
colorDot.backgroundColor = app.color
|
|
nameLab.text = app.name
|
|
durationLab.text = app.durationText
|
|
iconView.kf.cancelDownloadTask()
|
|
iconView.image = UIImage(named: "LockDistract/app_unknown")
|
|
if let url = URL(string: app.iconURL), !app.iconURL.isEmpty {
|
|
iconView.kf.setImage(with: url, placeholder: iconView.image)
|
|
}
|
|
progressRatio = max(0, min(progress, 1))
|
|
setNeedsLayout()
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
progressConstraint?.isActive = false
|
|
progressConstraint = fillView.widthAnchor.constraint(equalTo: trackView.widthAnchor, multiplier: progressRatio)
|
|
progressConstraint?.isActive = true
|
|
}
|
|
|
|
private func setupUI() {
|
|
colorDot.cornerRadius = 3
|
|
addSubview(colorDot)
|
|
colorDot.layoutChain.left().centerY().width(6).height(6)
|
|
|
|
iconView.backgroundColor = UIColor(hexStr: "#F4F6F8")
|
|
iconView.image = UIImage(named: "LockDistract/app_unknown")
|
|
iconView.contentMode = .scaleAspectFill
|
|
iconView.cornerRadius = 4
|
|
iconView.clipsToBounds = true
|
|
addSubview(iconView)
|
|
iconView.layoutChain.leftToRightOfView(colorDot, offset: 7).centerY().width(16).height(16)
|
|
|
|
durationLab.font = .systemFont(ofSize: 10, weight: .regular)
|
|
durationLab.textColor = UIColor(hexStr: "#A8A8A8")
|
|
durationLab.textAlignment = .right
|
|
addSubview(durationLab)
|
|
durationLab.layoutChain.right().centerY().width(42)
|
|
|
|
nameLab.font = .systemFont(ofSize: 10, weight: .medium)
|
|
nameLab.textColor = UIColor(hexStr: "#4B5563")
|
|
nameLab.lineBreakMode = .byTruncatingTail
|
|
addSubview(nameLab)
|
|
nameLab.layoutChain.leftToRightOfView(iconView, offset: 6).centerY().width(42)
|
|
|
|
trackView.backgroundColor = UIColor(hexStr: "#DDF3FF")
|
|
trackView.cornerRadius = 2
|
|
addSubview(trackView)
|
|
trackView.layoutChain.leftToRightOfView(nameLab, offset: 5).rightToLeftOfView(durationLab, offset: -7).centerY().height(4)
|
|
|
|
fillView.backgroundColor = UIColor(hexStr: "#5BC5F7")
|
|
fillView.cornerRadius = 2
|
|
trackView.addSubview(fillView)
|
|
fillView.layoutChain.left().top().bottom()
|
|
}
|
|
}
|
|
|
|
private final class AppUsageReportDonutView: UIView {
|
|
|
|
private var segmentLayers: [CAShapeLayer] = []
|
|
private var ratios: [CGFloat] = []
|
|
private var colors: [UIColor] = []
|
|
private let lineWidth: CGFloat = 18
|
|
|
|
func setSegments(ratios: [CGFloat], colors: [UIColor]) {
|
|
self.ratios = ratios
|
|
self.colors = colors
|
|
setNeedsLayout()
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
segmentLayers.forEach { $0.removeFromSuperlayer() }
|
|
segmentLayers.removeAll()
|
|
guard !ratios.isEmpty, !colors.isEmpty, bounds.width > 0 else { return }
|
|
|
|
let total = max(ratios.reduce(0, +), 0.001)
|
|
let radius = min(bounds.width, bounds.height) / 2 - lineWidth / 2
|
|
let center = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
var startAngle = -CGFloat.pi / 2
|
|
let segmentGap: CGFloat = ratios.count > 1 ? 0.025 : 0
|
|
for (index, ratio) in ratios.enumerated() {
|
|
let sweep = ratio / total * 2 * CGFloat.pi
|
|
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.fillColor = UIColor.clear.cgColor
|
|
layer.strokeColor = colors[index % colors.count].cgColor
|
|
layer.lineWidth = lineWidth
|
|
layer.lineCap = .butt
|
|
self.layer.addSublayer(layer)
|
|
segmentLayers.append(layer)
|
|
startAngle += sweep
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum AppUsageReportFormat {
|
|
static func durationText(seconds: Int) -> String {
|
|
let hours = max(seconds, 0) / 3600
|
|
let minutes = (max(seconds, 0) % 3600) / 60
|
|
if hours > 0 {
|
|
return minutes > 0 ? "\(hours)h \(minutes)m" : "\(hours)h"
|
|
}
|
|
return "\(minutes)m"
|
|
}
|
|
}
|
|
|
|
private enum AppUsageReportStatus {
|
|
static func text(for state: AppUsageReportViewModel.ContentState) -> String? {
|
|
switch state {
|
|
case .loading:
|
|
return "正在加载..."
|
|
case .empty:
|
|
return "暂无APP使用记录"
|
|
case .failure:
|
|
return "加载失败,请稍后重试"
|
|
case .content:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension UIView {
|
|
func applyAppUsageReportCardStyle(cornerRadius: CGFloat) {
|
|
layer.cornerRadius = cornerRadius
|
|
layer.masksToBounds = false
|
|
layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.08).cgColor
|
|
layer.shadowOffset = CGSize(width: 0, height: 2)
|
|
layer.shadowOpacity = 1
|
|
layer.shadowRadius = 9
|
|
}
|
|
}
|