2024-06-12 23:04:04 +04:00

280 lines
11 KiB
Swift

import Foundation
import UIKit
import Display
import ComponentFlow
final class AnimatedCounterItemComponent: Component {
public let font: UIFont
public let color: UIColor
public let text: String
public let numericValue: Int
public let alignment: CGFloat
public init(
font: UIFont,
color: UIColor,
text: String,
numericValue: Int,
alignment: CGFloat
) {
self.font = font
self.color = color
self.text = text
self.numericValue = numericValue
self.alignment = alignment
}
public static func ==(lhs: AnimatedCounterItemComponent, rhs: AnimatedCounterItemComponent) -> Bool {
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.text != rhs.text {
return false
}
if lhs.numericValue != rhs.numericValue {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
return true
}
public final class View: UIView {
private let contentView: UIImageView
private var component: AnimatedCounterItemComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.contentView = UIImageView()
super.init(frame: frame)
self.addSubview(self.contentView)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: AnimatedCounterItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let previousNumericValue = self.component?.numericValue
self.component = component
self.state = state
let text = NSAttributedString(string: component.text, font: component.font, textColor: component.color)
let textBounds = text.boundingRect(with: availableSize, options: [.usesLineFragmentOrigin], context: nil)
let size = CGSize(width: ceil(textBounds.width), height: ceil(textBounds.height))
let previousContentImage = self.contentView.image
let previousContentFrame = self.contentView.frame
self.contentView.image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
text.draw(at: textBounds.origin)
UIGraphicsPopContext()
})
self.contentView.frame = CGRect(origin: CGPoint(), size: size)
if !transition.animation.isImmediate, let previousContentImage, !previousContentFrame.isEmpty, let previousNumericValue, previousNumericValue != component.numericValue {
let previousContentView = UIImageView()
previousContentView.image = previousContentImage
previousContentView.frame = CGRect(origin: CGPoint(x: size.width * component.alignment - previousContentFrame.width * component.alignment, y: previousContentFrame.minY), size: previousContentFrame.size)
self.addSubview(previousContentView)
let offsetY: CGFloat = size.height * 0.6 * (previousNumericValue < component.numericValue ? -1.0 : 1.0)
let subTransition = ComponentTransition(animation: .curve(duration: 0.16, curve: .easeInOut))
subTransition.animatePosition(view: self.contentView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true)
subTransition.animateAlpha(view: self.contentView, from: 0.0, to: 1.0)
subTransition.setPosition(view: previousContentView, position: CGPoint(x: previousContentView.layer.position.x, y: previousContentView.layer.position.y - offsetY))
subTransition.setAlpha(view: previousContentView, alpha: 0.0, completion: { [weak previousContentView] _ in
previousContentView?.removeFromSuperview()
})
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
public final class AnimatedCounterComponent: Component {
public enum Alignment {
case left
case right
}
public struct Item: Equatable {
public var id: AnyHashable
public var text: String
public var numericValue: Int
public init(id: AnyHashable, text: String, numericValue: Int) {
self.id = id
self.text = text
self.numericValue = numericValue
}
}
public let font: UIFont
public let color: UIColor
public let alignment: Alignment
public let items: [Item]
public init(
font: UIFont,
color: UIColor,
alignment: Alignment,
items: [Item]
) {
self.font = font
self.color = color
self.alignment = alignment
self.items = items
}
public static func ==(lhs: AnimatedCounterComponent, rhs: AnimatedCounterComponent) -> Bool {
if lhs.font != rhs.font {
return false
}
if lhs.color != rhs.color {
return false
}
if lhs.alignment != rhs.alignment {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
private final class ItemView {
let view = ComponentView<Empty>()
}
public final class View: UIView {
private var itemViews: [AnyHashable: ItemView] = [:]
private var component: AnimatedCounterComponent?
private weak var state: EmptyComponentState?
private var measuredSpaceWidth: CGFloat?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
preconditionFailure()
}
func update(component: AnimatedCounterComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
let spaceWidth: CGFloat
if let measuredSpaceWidth = self.measuredSpaceWidth, let previousComponent = self.component, previousComponent.font.pointSize == component.font.pointSize {
spaceWidth = measuredSpaceWidth
} else {
spaceWidth = ceil(NSAttributedString(string: " ", font: component.font, textColor: .black).boundingRect(with: CGSize(width: 100.0, height: 100.0), options: .usesLineFragmentOrigin, context: nil).width)
self.measuredSpaceWidth = spaceWidth
}
self.component = component
self.state = state
var size = CGSize()
var validIds: [AnyHashable] = []
for item in component.items {
if size.width != 0.0 {
size.width += spaceWidth
}
validIds.append(item.id)
let itemView: ItemView
var itemTransition = transition
if let current = self.itemViews[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ItemView()
self.itemViews[item.id] = itemView
}
let itemSize = itemView.view.update(
transition: itemTransition,
component: AnyComponent(AnimatedCounterItemComponent(
font: component.font,
color: component.color,
text: item.text,
numericValue: item.numericValue,
alignment: component.alignment == .left ? 0.0 : 1.0
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let itemComponentView = itemView.view.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
}
let itemFrame = CGRect(origin: CGPoint(x: size.width, y: 0.0), size: itemSize)
switch component.alignment {
case .left:
itemComponentView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.minX, y: itemFrame.midY))
case .right:
itemComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.5)
itemTransition.setPosition(view: itemComponentView, position: CGPoint(x: itemFrame.maxX, y: itemFrame.midY))
}
itemComponentView.bounds = CGRect(origin: CGPoint(), size: itemFrame.size)
}
size.width += itemSize.width
size.height = max(size.height, itemSize.height)
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.itemViews {
if !validIds.contains(id) {
removeIds.append(id)
if let componentView = itemView.view.view {
transition.setAlpha(view: componentView, alpha: 0.0, completion: { [weak componentView] _ in
componentView?.removeFromSuperview()
})
}
}
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}