Piechart update

This commit is contained in:
Ali 2022-12-29 20:52:47 +04:00
parent e71ebf65fb
commit 198ba578ae
16 changed files with 844 additions and 315 deletions

View File

@ -181,77 +181,76 @@ final class PieChartComponent: Component {
return true
}
private final class ChartDataView: UIView {
private(set) var theme: PresentationTheme?
private(set) var data: ChartData?
private(set) var selectedKey: AnyHashable?
private struct CalculatedLabel {
var image: UIImage
var alpha: CGFloat
var angle: CGFloat
var radius: CGFloat
var scale: CGFloat
private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)?
private var animator: DisplayLinkAnimator?
private var labels: [AnyHashable: ChartLabel] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = nil
self.isOpaque = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.animator?.invalidate()
}
func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) {
let data = processChartData(data: data)
if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
self.theme = theme
self.selectedKey = selectedKey
if animated, let previous = self.data {
var initialState = previous
if let currentAnimation = self.currentAnimation {
initialState = currentAnimation.current
}
self.currentAnimation = (initialState, data, initialState, 0.0)
self.animator?.invalidate()
self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in
guard let self else {
return
}
let progress = listViewAnimationCurveSystem(progress)
if let currentAnimationValue = self.currentAnimation {
self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress), progress)
self.setNeedsDisplay()
}
}, completion: { [weak self] in
guard let self else {
return
}
self.currentAnimation = nil
self.setNeedsDisplay()
})
}
self.data = data
self.setNeedsDisplay()
init(
image: UIImage,
alpha: CGFloat,
angle: CGFloat,
radius: CGFloat,
scale: CGFloat
) {
self.image = image
self.alpha = alpha
self.angle = angle
self.radius = radius
self.scale = scale
}
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
private struct CalculatedSection {
var id: StorageUsageScreenComponent.Category
var color: UIColor
var innerAngle: Range<CGFloat>
var outerAngle: Range<CGFloat>
var innerRadius: CGFloat
var outerRadius: CGFloat
var label: CalculatedLabel?
init(
id: StorageUsageScreenComponent.Category,
color: UIColor,
innerAngle: Range<CGFloat>,
outerAngle: Range<CGFloat>,
innerRadius: CGFloat,
outerRadius: CGFloat,
label: CalculatedLabel?
) {
self.id = id
self.color = color
self.innerAngle = innerAngle
self.outerAngle = outerAngle
self.innerRadius = innerRadius
self.outerRadius = outerRadius
self.label = label
}
guard let _ = self.theme, let data = self.currentAnimation?.current ?? self.data else {
return
}
if data.items.isEmpty {
private struct ItemAngleData {
var angleValue: CGFloat
var startAngle: CGFloat
var endAngle: CGFloat
}
private struct CalculatedLayout {
var size: CGSize
var sections: [CalculatedSection]
init(size: CGSize, sections: [CalculatedSection]) {
self.size = size
self.sections = sections
}
init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?) {
self.size = size
self.sections = []
if items.isEmpty {
return
}
@ -260,8 +259,8 @@ final class PieChartComponent: Component {
let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5)
var angles: [Double] = []
for i in 0 ..< data.items.count {
let item = data.items[i]
for i in 0 ..< items.count {
let item = items[i]
let angle = item.value * CGFloat.pi * 2.0
angles.append(angle)
}
@ -269,22 +268,14 @@ final class PieChartComponent: Component {
let diameter: CGFloat = 200.0
let reducedDiameter: CGFloat = 170.0
let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter))
struct ItemAngleData {
var angleValue: CGFloat
var startAngle: CGFloat
var endAngle: CGFloat
}
var anglesData: [ItemAngleData] = []
var startAngle: CGFloat = 0.0
for i in 0 ..< data.items.count {
let item = data.items[i]
for i in 0 ..< items.count {
let item = items[i]
let itemOuterDiameter: CGFloat
if let selectedKey = self.selectedKey {
if let selectedKey {
if selectedKey == AnyHashable(item.id) {
itemOuterDiameter = diameter
} else {
@ -303,16 +294,16 @@ final class PieChartComponent: Component {
if item.mergeable {
let previousItem: ChartData.Item
if i == 0 {
previousItem = data.items[data.items.count - 1]
previousItem = items[items.count - 1]
} else {
previousItem = data.items[i - 1]
previousItem = items[i - 1]
}
let nextItem: ChartData.Item
if i == data.items.count - 1 {
nextItem = data.items[0]
if i == items.count - 1 {
nextItem = items[0]
} else {
nextItem = data.items[i + 1]
nextItem = items[i + 1]
}
if previousItem.mergeable {
@ -338,21 +329,67 @@ final class PieChartComponent: Component {
var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction
arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle)
let path = CGMutablePath()
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: arcInnerEndAngle, endAngle: arcInnerStartAngle, clockwise: true)
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: arcOuterStartAngle, endAngle: arcOuterEndAngle, clockwise: false)
context.addPath(path)
context.setFillColor(item.color.cgColor)
context.fillPath()
self.sections.append(CalculatedSection(
id: item.id,
color: item.color,
innerAngle: arcInnerStartAngle ..< arcInnerEndAngle,
outerAngle: arcOuterStartAngle ..< arcOuterEndAngle,
innerRadius: innerDiameter * 0.5,
outerRadius: itemOuterDiameter * 0.5,
label: nil
))
startAngle += angleValue
anglesData.append(ItemAngleData(angleValue: angleValue, startAngle: innerStartAngle, endAngle: innerEndAngle))
}
func updateItemLabel(id: AnyHashable, displayValue: Double, mergeFactor: CGFloat, angleData: ItemAngleData) {
var mergedItem: (displayValue: Double, angleData: ItemAngleData, mergeFactor: CGFloat)?
for i in 0 ..< items.count {
let item = items[i]
let angleData = anglesData[i]
self.updateLabel(
index: i,
displayValue: item.displayValue,
mergeFactor: item.mergeFactor,
innerAngle: self.sections[i].innerAngle,
outerAngle: self.sections[i].outerAngle,
innerRadius: self.sections[i].innerRadius,
outerRadius: self.sections[i].outerRadius
)
if item.mergeable {
if var currentMergedItem = mergedItem {
currentMergedItem.displayValue += item.displayValue
currentMergedItem.angleData.startAngle = min(currentMergedItem.angleData.startAngle, angleData.startAngle)
currentMergedItem.angleData.endAngle = max(currentMergedItem.angleData.endAngle, angleData.endAngle)
mergedItem = currentMergedItem
} else {
let invertedMergeFactor: CGFloat = 1.0 - max(0.0, item.mergeFactor)
mergedItem = (item.displayValue, angleData, invertedMergeFactor)
}
}
}
/*if let mergedItem {
updateItemLabel(id: "merged", displayValue: mergedItem.displayValue, mergeFactor: mergedItem.mergeFactor, angleData: mergedItem.angleData)
} else {
if let label = self.labels["merged"] {
self.labels.removeValue(forKey: "merged")
label.removeFromSuperview()
}
}*/
}
private mutating func updateLabel(
index: Int,
displayValue: Double,
mergeFactor: CGFloat,
innerAngle: Range<CGFloat>,
outerAngle: Range<CGFloat>,
innerRadius: CGFloat,
outerRadius: CGFloat
) {
let fractionValue: Double = floor(displayValue * 100.0 * 10.0) / 10.0
let fractionString: String
if fractionValue < 0.1 {
@ -363,39 +400,33 @@ final class PieChartComponent: Component {
fractionString = "\(fractionValue)"
}
let label: ChartLabel
if let current = self.labels[id] {
label = current
} else {
label = ChartLabel()
self.labels[id] = label
let labelString = NSAttributedString(string: "\(fractionString)%", font: chartLabelFont, textColor: .white)
let labelBounds = labelString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil)
let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height))
guard let labelImage = generateImage(labelSize, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
labelString.draw(in: labelBounds)
UIGraphicsPopContext()
}) else {
return
}
let labelSize = label.update(text: "\(fractionString)%")
var labelFrame: CGRect?
var resultLabel: CalculatedLabel?
let angleValue = angleData.angleValue
let innerStartAngle = angleData.startAngle
let innerEndAngle = angleData.endAngle
if angleValue >= 0.001 {
for step in 0 ... 20 {
let stepFraction: CGFloat = CGFloat(step) / 20.0
if innerAngle.upperBound - innerAngle.lowerBound >= 0.001 {
for step in 0 ... 10 {
let stepFraction: CGFloat = CGFloat(step) / 10.0
let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset)
let midAngle: CGFloat = (innerAngle.lowerBound + innerAngle.upperBound) * 0.5
let centerDistance: CGFloat = (innerRadius + (outerRadius - innerRadius) * centerOffset)
let relLabelCenter = CGPoint(
x: cos(midAngle) * centerDistance,
y: sin(midAngle) * centerDistance
)
let labelCenter = CGPoint(
x: shapeLayerFrame.midX + relLabelCenter.x,
y: shapeLayerFrame.midY + relLabelCenter.y
)
func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat {
let dx: CGFloat = p2.x - p1.x
let dy: CGFloat = p2.y - p1.y
@ -438,19 +469,19 @@ final class PieChartComponent: Component {
return distance
}
let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5)
let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5)
let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5)
let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5)
let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), outerRadius)
let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerRadius)
let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), outerRadius)
let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerRadius)
let horizontalInset: CGFloat = 2.0
let intersectionOuterLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), diameter * 0.5) - horizontalInset
let intersectionInnerLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), innerDiameter * 0.5) - horizontalInset
let intersectionOuterLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), outerRadius) - horizontalInset
let intersectionInnerLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), innerRadius) - horizontalInset
let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.lowerBound), y: sin(innerAngle.lowerBound)))
let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.lowerBound), y: sin(innerAngle.lowerBound)))
let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.upperBound), y: sin(innerAngle.upperBound)))
let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.upperBound), y: sin(innerAngle.upperBound)))
var distances: [CGFloat] = [
intersectionOuterTopRight,
@ -461,7 +492,7 @@ final class PieChartComponent: Component {
intersectionInnerLeft
]
if angleValue < CGFloat.pi / 2.0 {
if innerAngle.upperBound - innerAngle.lowerBound < CGFloat.pi / 2.0 {
distances.append(contentsOf: [
intersectionLine1TopRight,
intersectionLine1BottomRight,
@ -483,122 +514,478 @@ final class PieChartComponent: Component {
let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0)
let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height))
let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize)
let currentScale = finalSize.width / labelSize.width
if finalSize.width >= labelSize.width {
labelFrame = currentFrame
if currentScale >= 1.0 - 0.001 {
resultLabel = CalculatedLabel(
image: labelImage,
alpha: 1.0,
angle: midAngle,
radius: centerDistance,
scale: 1.0
)
break
}
if let labelFrame {
if labelFrame.width > finalSize.width {
if let resultLabel {
if resultLabel.scale > currentScale {
continue
}
}
labelFrame = currentFrame
}
} else {
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * 0.5)
let relLabelCenter = CGPoint(
x: cos(midAngle) * centerDistance,
y: sin(midAngle) * centerDistance
resultLabel = CalculatedLabel(
image: labelImage,
alpha: currentScale >= 0.2 ? 1.0 : 0.0,
angle: midAngle,
radius: centerDistance,
scale: currentScale
)
}
} else {
let midAngle: CGFloat = (innerAngle.lowerBound + innerAngle.upperBound) * 0.5
let centerDistance: CGFloat = (innerRadius + (outerRadius - innerRadius) * 0.5)
let labelCenter = CGPoint(
x: shapeLayerFrame.midX + relLabelCenter.x,
y: shapeLayerFrame.midY + relLabelCenter.y
resultLabel = CalculatedLabel(
image: labelImage,
alpha: 0.0,
angle: midAngle,
radius: centerDistance,
scale: 0.001
)
let minSize = labelSize.aspectFitted(CGSize(width: 4.0, height: 4.0))
labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - minSize.width * 0.5, y: labelCenter.y - minSize.height * 0.5), size: minSize)
}
let labelView = label
if let labelFrame {
var animateIn: Bool = false
if labelView.superview == nil {
animateIn = true
self.addSubview(labelView)
if let resultLabel {
self.sections[index].label = resultLabel
}
}
}
var labelScale = labelFrame.width / labelSize.width
private struct Particle {
var trackIndex: Int
var position: CGPoint
var scale: CGFloat
var alpha: CGFloat
var direction: CGPoint
var velocity: CGFloat
var normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0
normalAlpha *= max(0.0, mergeFactor)
init(
trackIndex: Int,
position: CGPoint,
scale: CGFloat,
alpha: CGFloat,
direction: CGPoint,
velocity: CGFloat
) {
self.trackIndex = trackIndex
self.position = position
self.scale = scale
self.alpha = alpha
self.direction = direction
self.velocity = velocity
}
var relLabelCenter = CGPoint(
x: labelFrame.midX - shapeLayerFrame.midX,
y: labelFrame.midY - shapeLayerFrame.midY
mutating func update(deltaTime: CGFloat) {
var position = self.position
position.x += self.direction.x * self.velocity * deltaTime
position.y += self.direction.y * self.velocity * deltaTime
self.position = position
}
}
private final class ParticleSet {
private(set) var particles: [Particle] = []
init() {
self.generateParticles(preAdvance: true)
}
private func generateParticles(preAdvance: Bool) {
let maxDirections = 24
if self.particles.count < maxDirections {
var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections)
for i in 0 ..< maxDirections {
allTrackIndices[i] = i
}
var takenIndexCount = 0
for particle in self.particles {
allTrackIndices[particle.trackIndex] = -1
takenIndexCount += 1
}
var availableTrackIndices: [Int] = []
availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount)
for index in allTrackIndices {
if index != -1 {
availableTrackIndices.append(index)
}
}
if !availableTrackIndices.isEmpty {
availableTrackIndices.shuffle()
for takeIndex in availableTrackIndices {
let directionIndex = takeIndex
let angle = (CGFloat(directionIndex % maxDirections) / CGFloat(maxDirections)) * CGFloat.pi * 2.0
let direction = CGPoint(x: cos(angle), y: sin(angle))
let velocity = CGFloat.random(in: 20.0 ..< 40.0)
let alpha = CGFloat.random(in: 0.1 ..< 0.4)
let scale = CGFloat.random(in: 0.5 ... 1.0) * 0.22
var position = CGPoint(x: 100.0, y: 100.0)
var initialOffset: CGFloat = 0.4
if preAdvance {
initialOffset = CGFloat.random(in: initialOffset ... 1.0)
}
position.x += direction.x * initialOffset * 105.0
position.y += direction.y * initialOffset * 105.0
let particle = Particle(
trackIndex: directionIndex,
position: position,
scale: scale,
alpha: alpha,
direction: direction,
velocity: velocity
)
self.particles.append(particle)
}
}
}
}
let labelAlpha: CGFloat
if let selectedKey = self.selectedKey {
if selectedKey == id {
labelAlpha = normalAlpha
func update(deltaTime: CGFloat) {
let size = CGSize(width: 200.0, height: 200.0)
let radius2 = pow(size.width * 0.5 + 10.0, 2.0)
for i in (0 ..< self.particles.count).reversed() {
self.particles[i].update(deltaTime: deltaTime)
let position = self.particles[i].position
if pow(position.x - size.width * 0.5, 2.0) + pow(position.y - size.height * 0.5, 2.0) > radius2 {
self.particles.remove(at: i)
}
}
self.generateParticles(preAdvance: false)
}
}
private final class SectionLayer: SimpleLayer {
private let maskLayer: SimpleShapeLayer
private let gradientLayer: SimpleGradientLayer
private let labelLayer: SimpleLayer
private var currentLabelImage: UIImage?
private var particleImage: UIImage?
private var particleLayers: [SimpleLayer] = []
init(category: StorageUsageScreenComponent.Category) {
self.maskLayer = SimpleShapeLayer()
self.maskLayer.fillColor = UIColor.white.cgColor
self.gradientLayer = SimpleGradientLayer()
self.gradientLayer.type = .radial
self.gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
self.labelLayer = SimpleLayer()
super.init()
self.mask = self.maskLayer
self.addSublayer(self.gradientLayer)
self.addSublayer(self.labelLayer)
switch category {
case .photos:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticlePhotos")?.precomposed()
case .videos:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleVideos")?.precomposed()
case .files:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleDocuments")?.precomposed()
case .music:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleMusic")?.precomposed()
case .other:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleOther")?.precomposed()
case .stickers:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleStickers")?.precomposed()
case .avatars:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleAvatars")?.precomposed()
case .misc:
self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleOther")?.precomposed()
}
}
override init(layer: Any) {
self.maskLayer = SimpleShapeLayer()
self.gradientLayer = SimpleGradientLayer()
self.labelLayer = SimpleLayer()
super.init(layer: layer)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(size: CGSize, section: CalculatedSection) {
self.maskLayer.frame = CGRect(origin: CGPoint(), size: size)
self.gradientLayer.frame = CGRect(origin: CGPoint(), size: size)
let normalColor = section.color.cgColor
let darkerColor = section.color.withMultipliedBrightnessBy(0.96).cgColor
let colors: [CGColor] = [
darkerColor,
normalColor,
normalColor,
normalColor,
darkerColor
]
self.gradientLayer.colors = colors
let locations: [CGFloat] = [
0.0,
0.3,
0.5,
0.7,
1.0
]
self.gradientLayer.locations = locations.map { location in
let location = location * 0.5 + 0.5
return location as NSNumber
}
let path = CGMutablePath()
path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.innerRadius, startAngle: section.innerAngle.upperBound, endAngle: section.innerAngle.lowerBound, clockwise: true)
path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.outerRadius, startAngle: section.outerAngle.lowerBound, endAngle: section.outerAngle.upperBound, clockwise: false)
self.maskLayer.path = path
if let label = section.label {
if self.currentLabelImage !== label.image {
self.currentLabelImage = label.image
self.labelLayer.contents = label.image.cgImage
}
let position = CGPoint(x: size.width * 0.5 + cos(label.angle) * label.radius, y: size.height * 0.5 + sin(label.angle) * label.radius)
let labelSize = CGSize(width: label.image.size.width * label.scale, height: label.image.size.height * label.scale)
let labelFrame = CGRect(origin: CGPoint(x: position.x - labelSize.width * 0.5, y: position.y - labelSize.height * 0.5), size: labelSize)
self.labelLayer.frame = labelFrame
self.labelLayer.opacity = Float(label.alpha)
} else {
labelAlpha = 0.0
let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter)
let reducedDiameterFactor: CGFloat = reducedDiameter / diameter
labelScale *= reducedFactor
relLabelCenter.x *= reducedDiameterFactor
relLabelCenter.y *= reducedDiameterFactor
self.currentLabelImage = nil
self.labelLayer.contents = nil
}
}
func updateParticles(particleSet: ParticleSet) {
guard let particleImage = self.particleImage else {
return
}
for i in 0 ..< particleSet.particles.count {
let particle = particleSet.particles[i]
let particleLayer: SimpleLayer
if i < self.particleLayers.count {
particleLayer = self.particleLayers[i]
particleLayer.isHidden = false
} else {
labelAlpha = normalAlpha
}
if labelView.alpha != labelAlpha {
let transition: Transition
if animateIn || "".isEmpty {
transition = .immediate
} else {
transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut))
}
transition.setAlpha(view: labelView, alpha: labelAlpha)
particleLayer = SimpleLayer()
particleLayer.contents = particleImage.cgImage
particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size)
self.particleLayers.append(particleLayer)
self.insertSublayer(particleLayer, above: self.gradientLayer)
}
let labelCenter = CGPoint(
x: shapeLayerFrame.midX + relLabelCenter.x,
y: shapeLayerFrame.midY + relLabelCenter.y
particleLayer.position = particle.position
particleLayer.transform = CATransform3DMakeScale(particle.scale, particle.scale, 1.0)
particleLayer.opacity = Float(particle.alpha)
}
if particleSet.particles.count < self.particleLayers.count {
for i in particleSet.particles.count ..< self.particleLayers.count {
self.particleLayers[i].isHidden = true
}
}
}
}
private final class ChartDataView: UIView {
private(set) var theme: PresentationTheme?
private(set) var data: ChartData?
private(set) var selectedKey: AnyHashable?
private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)?
private var currentLayout: CalculatedLayout?
private var animator: DisplayLinkAnimator?
private var displayLink: SharedDisplayLinkDriver.Link?
private var sectionLayers: [AnyHashable: SectionLayer] = [:]
private let particleSet: ParticleSet
private var labels: [AnyHashable: ChartLabel] = [:]
override init(frame: CGRect) {
self.particleSet = ParticleSet()
super.init(frame: frame)
self.backgroundColor = nil
self.isOpaque = false
self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: true, { [weak self] in
self?.update()
})
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.animator?.invalidate()
}
func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) {
let data = processChartData(data: data)
if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
self.theme = theme
self.selectedKey = selectedKey
if animated, let previous = self.data {
var initialState = previous
if let currentAnimation = self.currentAnimation {
initialState = currentAnimation.current
}
self.currentAnimation = (initialState, data, initialState, 0.0)
self.currentLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0),
items: initialState.items,
selectedKey: self.selectedKey
)
labelView.center = labelCenter
labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale)
self.animator?.invalidate()
self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in
guard let self else {
return
}
let progress = listViewAnimationCurveSystem(progress)
if let currentAnimationValue = self.currentAnimation {
let interpolatedValue = interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress)
self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolatedValue, progress)
self.currentLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0),
items: interpolatedValue.items,
selectedKey: self.selectedKey
)
self.update()
}
var mergedItem: (displayValue: Double, angleData: ItemAngleData, mergeFactor: CGFloat)?
for i in 0 ..< data.items.count {
let item = data.items[i]
let angleData = anglesData[i]
updateItemLabel(id: item.id, displayValue: item.displayValue, mergeFactor: item.mergeFactor, angleData: angleData)
if item.mergeable {
if var currentMergedItem = mergedItem {
currentMergedItem.displayValue += item.displayValue
currentMergedItem.angleData.startAngle = min(currentMergedItem.angleData.startAngle, angleData.startAngle)
currentMergedItem.angleData.endAngle = max(currentMergedItem.angleData.endAngle, angleData.endAngle)
mergedItem = currentMergedItem
}, completion: { [weak self] in
guard let self else {
return
}
self.currentAnimation = nil
self.update()
})
} else {
let invertedMergeFactor: CGFloat = 1.0 - max(0.0, item.mergeFactor)
mergedItem = (item.displayValue, angleData, invertedMergeFactor)
self.currentLayout = CalculatedLayout(
size: CGSize(width: 200.0, height: 200.0),
items: data.items,
selectedKey: self.selectedKey
)
}
self.data = data
self.update()
}
}
if let mergedItem {
updateItemLabel(id: "merged", displayValue: mergedItem.displayValue, mergeFactor: mergedItem.mergeFactor, angleData: mergedItem.angleData)
private func update() {
self.particleSet.update(deltaTime: 1.0 / 60.0)
var validIds: [AnyHashable] = []
if let currentLayout = self.currentLayout {
for section in currentLayout.sections {
validIds.append(section.id)
let sectionLayer: SectionLayer
if let current = self.sectionLayers[section.id] {
sectionLayer = current
} else {
if let label = self.labels["merged"] {
self.labels.removeValue(forKey: "merged")
label.removeFromSuperview()
sectionLayer = SectionLayer(category: section.id)
self.sectionLayers[section.id] = sectionLayer
self.layer.addSublayer(sectionLayer)
}
sectionLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 200.0, height: 200.0))
sectionLayer.update(size: sectionLayer.bounds.size, section: section)
sectionLayer.updateParticles(particleSet: self.particleSet)
}
}
var removeIds: [AnyHashable] = []
for (id, sectionLayer) in self.sectionLayers {
if !validIds.contains(id) {
removeIds.append(id)
sectionLayer.removeFromSuperlayer()
}
}
for id in removeIds {
self.sectionLayers.removeValue(forKey: id)
}
}
/*override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
guard let currentLayout = self.currentLayout else {
return
}
let size = CGSize(width: rect.width, height: rect.height)
for section in currentLayout.sections {
if section.innerAngle.lowerBound == section.innerAngle.upperBound {
continue
}
let path = CGMutablePath()
path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.innerRadius, startAngle: section.innerAngle.upperBound, endAngle: section.innerAngle.lowerBound, clockwise: true)
path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.outerRadius, startAngle: section.outerAngle.lowerBound, endAngle: section.outerAngle.upperBound, clockwise: false)
context.addPath(path)
context.clip()
let colors: [CGColor] = [
section.color.withMultipliedBrightnessBy(0.9).cgColor,
section.color.cgColor,
section.color.cgColor,
section.color.cgColor,
section.color.withMultipliedBrightnessBy(0.9).cgColor
]
var locations: [CGFloat] = [
1.0,
0.9,
0.5,
0.1,
0.0
]
if let gradient = CGGradient(colorsSpace: nil, colors: colors as CFArray, locations: &locations) {
context.drawRadialGradient(gradient, startCenter: CGPoint(x: size.width * 0.5, y: size.height * 0.5), startRadius: section.innerRadius, endCenter: CGPoint(x: size.width * 0.5, y: size.height * 0.5), endRadius: section.outerRadius, options: [])
}
context.resetClip()
//context.setFillColor(section.color.cgColor)
//context.fillPath()
if let label = section.label {
let position = CGPoint(x: size.width * 0.5 + cos(label.angle) * label.radius, y: size.height * 0.5 + sin(label.angle) * label.radius)
let labelSize = CGSize(width: label.image.size.width * label.scale, height: label.image.size.height * label.scale)
let labelFrame = CGRect(origin: CGPoint(x: position.x - labelSize.width * 0.5, y: position.y - labelSize.height * 0.5), size: labelSize)
label.image.draw(in: labelFrame, blendMode: .normal, alpha: label.alpha)
}
}
}*/
}
class View: UIView {
private let dataView: ChartDataView

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Profile Photos.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Profile Photos</title>
<g id="Icon-/-Profile-Photos" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M36,10 C21.648,10 10,21.648 10,36 C10,50.352 21.648,62 36,62 C50.352,62 62,50.352 62,36 C62,21.648 50.352,10 36,10 Z M36,19.8 C40.316,19.8 43.8,23.284 43.8,27.6 C43.8,31.916 40.316,35.4 36,35.4 C31.684,35.4 28.2,31.916 28.2,27.6 C28.2,23.284 31.684,19.8 36,19.8 Z M36,54.72 C29.5,54.72 23.754,51.4742263 20.4,46.5548505 C20.478,42.4839792 29.826,39.72 36,39.72 C42.174,39.72 51.522,42.4839792 51.6,46.5548505 C48.246,51.4742263 42.5,54.72 36,54.72 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 860 B

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Documents.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Documents</title>
<g id="Icon-/-Documents" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M43.15125,14.3808511 C42.24875,13.4914894 41.0375,13 39.77875,13 L24.7,13 C21.1375,13 18,16.106383 18,19.6170213 L18,50.3829787 C18,53.893617 21.11375,57 24.67625,57 L49.3,57 C52.8625,57 56,53.893617 56,50.3829787 L56,28.9851064 C56,27.7446809 55.50125,26.5510638 54.59875,25.6851064 L43.15125,14.3808511 Z M39.375,27.0425532 L39.375,16.5106383 L52.4375,29.3829787 L41.75,29.3829787 C40.44375,29.3829787 39.375,28.3297872 39.375,27.0425532 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 842 B

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Music.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Music</title>
<g id="Icon-/-Music" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M36,10 C50.352,10 62,21.648 62,36 C62,50.352 50.352,62 36,62 C21.648,62 10,50.352 10,36 C10,21.648 21.648,10 36,10 Z M33.1111111,23.950754 C31.5156218,23.950754 30.2222222,25.2441536 30.2222222,26.8396429 L30.2222222,45.6296296 C30.2222222,46.2546991 30.4249583,46.8629074 30.8,47.362963 C31.7572936,48.6393544 33.568053,48.8980343 34.8444444,47.9407407 L46.5881862,39.1329344 C46.7600033,39.0040716 46.9169145,38.8564504 47.0560109,38.6928077 C48.089327,37.4771416 47.9415032,35.6539814 46.7258372,34.6206653 L34.9820954,24.6384848 C34.4597827,24.194519 33.7966155,23.950754 33.1111111,23.950754 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 991 B

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Other.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Other</title>
<g id="Icon-/-Other" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M31.2684326,15 L17.9882353,15 C15.2447059,15 13.0249412,17.25 13.0249412,20 L13,49.0697674 C13,52.75 15.2447059,55 18.9294118,55 L55.0705882,55 C58.7552941,55 61,52.75 61,49.0697674 L61,25.9302326 C61,22.25 58.7552941,20.5813953 55.0705882,20.5813953 L40.0468122,20.5813953 C38.7070557,20.5813953 37.430813,20.0103436 36.5380314,19.0113998 L34.0754572,16.2559964 C33.361232,15.4568414 32.3402378,15 31.2684326,15 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 806 B

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Photos.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Photos</title>
<g id="Icon-/-Photos" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M48,12 C54.627417,12 60,17.372583 60,24 L60,48 C60,54.627417 54.627417,60 48,60 L24,60 C17.372583,60 12,54.627417 12,48 L12,24 C12,17.372583 17.372583,12 24,12 L48,12 Z M44.3651844,34.6306685 C43.8183724,33.9010116 42.7247485,33.9010116 42.1779366,34.6036442 L33.7023513,45.3863509 L27.9608258,38.549196 C27.3866733,37.8735878 26.3477306,37.9006121 25.8282592,38.6032446 L19.9804504,47.2510295 C19.2695949,48.1428323 19.8984286,50.4 21.0467337,50.4 L51.9081103,50.4 C52.9904208,50.4 53.639443,48.3210078 53.0676996,47.374937 L44.3651844,34.6306685 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 924 B

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Stickers.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Stickers</title>
<g id="Icon-/-Stickers" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M60,35.5 L59.9953966,35.670698 C59.9067791,37.309531 58.5498341,38.6111111 56.8888889,38.6111111 L50.6666667,38.6111111 L50.3912615,38.6142092 C45.9287855,38.7147025 42.0676082,41.2512307 40.0852923,44.9462311 C39.7167534,44.8455073 39.3161253,44.8387068 38.9195771,44.9463572 C37.9769648,45.202247 36.9982344,45.3333333 36,45.3333333 C33.4531941,45.3333333 29.5907927,43.2780139 28.1265456,41.3638425 L27.9058916,41.062472 C27.1856257,40.1544843 25.8744537,39.9480152 24.9057896,40.6151336 C23.8950097,41.3112571 23.6399302,42.6949756 24.3360537,43.7057556 C26.5046744,46.8546196 31.8881746,49.7777778 36,49.7777778 C36.9171651,49.7777778 37.8247038,49.6981543 38.7149707,49.541295 C38.6826363,49.8921863 38.6666667,50.2497837 38.6666667,50.6111111 L38.6666667,56.8888889 L38.6620632,57.0595868 C38.5734457,58.6984199 37.2165008,60 35.5555556,60 L24.4137931,60 C17.5578445,60 12,54.4421555 12,47.5862069 L12,24.4137931 C12,17.5578445 17.5578445,12 24.4137931,12 L47.5862069,12 C54.4421555,12 60,17.5578445 60,24.4137931 L60,35.5 Z M42.6383189,59.9550937 L42.457984,59.9664377 C42.8777423,59.0264139 43.1111111,57.9849045 43.1111111,56.8888889 L43.1111111,50.6111111 L43.1144503,50.384302 C43.2343809,46.3164026 46.569718,43.0555556 50.6666667,43.0555556 L56.8888889,43.0555556 L57.115698,43.0522164 C58.130088,43.0223099 59.0942932,42.7924609 59.9702649,42.4007181 C59.4478271,51.6700337 52.1614105,59.1320852 42.9674765,59.929458 L42.6383189,59.9550937 Z M44.4444444,28 C42.7262252,28 41.3333333,29.790861 41.3333333,32 C41.3333333,34.209139 42.7262252,36 44.4444444,36 C46.1626637,36 47.5555556,34.209139 47.5555556,32 C47.5555556,29.790861 46.1626637,28 44.4444444,28 Z M27.5555556,28 C25.8373363,28 24.4444444,29.790861 24.4444444,32 C24.4444444,34.209139 25.8373363,36 27.5555556,36 C29.2737748,36 30.6666667,34.209139 30.6666667,32 C30.6666667,29.790861 29.2737748,28 27.5555556,28 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Videos.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Icon / Videos</title>
<g id="Icon-/-Videos" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M18.5095398,18.85 L36.9719416,18.85 C40.5670612,18.85 43.4814815,21.7644203 43.4814815,25.3595398 L43.4814815,45.6404602 C43.4814815,49.2355797 40.5670612,52.15 36.9719416,52.15 L18.5095398,52.15 C14.9144203,52.15 12,49.2355797 12,45.6404602 L12,25.3595398 C12,21.7644203 14.9144203,18.85 18.5095398,18.85 Z M49.5847133,27.1084323 L56.6902242,21.3231792 C58.0841802,20.1882308 60.1342623,20.3981979 61.2692107,21.7921539 C61.741904,22.3727209 62,23.0984963 62,23.8471598 L62,47.6096485 C62,49.4072083 60.5427899,50.8644185 58.7452301,50.8644185 C58.0575374,50.8644185 57.3875158,50.6465978 56.831289,50.2422056 L49.8668429,45.1788626 C48.1820423,43.9539665 47.1851852,41.9967567 47.1851852,39.9137485 L47.1851852,32.1563935 C47.1851852,30.1985078 48.0664292,28.3446077 49.5847133,27.1084323 Z" id="Shape" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB