mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
808 lines
36 KiB
Swift
808 lines
36 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AsyncDisplayKit
|
|
import TelegramCore
|
|
import SwiftSignalKit
|
|
import AccountContext
|
|
import TelegramPresentationData
|
|
import PresentationDataUtils
|
|
import ComponentFlow
|
|
import MultilineTextComponent
|
|
import Markdown
|
|
import TextFormat
|
|
import RoundedRectWithTailPath
|
|
|
|
public class RemainingCountComponent: Component {
|
|
private let inactiveColor: UIColor
|
|
private let activeColors: [UIColor]
|
|
private let inactiveTitle: String
|
|
private let inactiveValue: String
|
|
private let inactiveTitleColor: UIColor
|
|
private let activeTitle: String
|
|
private let activeValue: String
|
|
private let activeTitleColor: UIColor
|
|
private let badgeText: String?
|
|
private let badgePosition: CGFloat
|
|
private let badgeGraphPosition: CGFloat
|
|
private let invertProgress: Bool
|
|
|
|
public init(
|
|
inactiveColor: UIColor,
|
|
activeColors: [UIColor],
|
|
inactiveTitle: String,
|
|
inactiveValue: String,
|
|
inactiveTitleColor: UIColor,
|
|
activeTitle: String,
|
|
activeValue: String,
|
|
activeTitleColor: UIColor,
|
|
badgeText: String?,
|
|
badgePosition: CGFloat,
|
|
badgeGraphPosition: CGFloat,
|
|
invertProgress: Bool = false
|
|
) {
|
|
self.inactiveColor = inactiveColor
|
|
self.activeColors = activeColors
|
|
self.inactiveTitle = inactiveTitle
|
|
self.inactiveValue = inactiveValue
|
|
self.inactiveTitleColor = inactiveTitleColor
|
|
self.activeTitle = activeTitle
|
|
self.activeValue = activeValue
|
|
self.activeTitleColor = activeTitleColor
|
|
self.badgeText = badgeText
|
|
self.badgePosition = badgePosition
|
|
self.badgeGraphPosition = badgeGraphPosition
|
|
self.invertProgress = invertProgress
|
|
}
|
|
|
|
public static func ==(lhs: RemainingCountComponent, rhs: RemainingCountComponent) -> Bool {
|
|
if lhs.inactiveColor != rhs.inactiveColor {
|
|
return false
|
|
}
|
|
if lhs.activeColors != rhs.activeColors {
|
|
return false
|
|
}
|
|
if lhs.inactiveTitle != rhs.inactiveTitle {
|
|
return false
|
|
}
|
|
if lhs.inactiveValue != rhs.inactiveValue {
|
|
return false
|
|
}
|
|
if lhs.inactiveTitleColor != rhs.inactiveTitleColor {
|
|
return false
|
|
}
|
|
if lhs.activeTitle != rhs.activeTitle {
|
|
return false
|
|
}
|
|
if lhs.activeValue != rhs.activeValue {
|
|
return false
|
|
}
|
|
if lhs.activeTitleColor != rhs.activeTitleColor {
|
|
return false
|
|
}
|
|
if lhs.badgeText != rhs.badgeText {
|
|
return false
|
|
}
|
|
if lhs.badgePosition != rhs.badgePosition {
|
|
return false
|
|
}
|
|
if lhs.badgeGraphPosition != rhs.badgeGraphPosition {
|
|
return false
|
|
}
|
|
if lhs.invertProgress != rhs.invertProgress {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
public final class View: UIView {
|
|
private var component: RemainingCountComponent?
|
|
|
|
private let container: UIView
|
|
private let inactiveBackground: SimpleLayer
|
|
|
|
private let inactiveTitleLabel = ComponentView<Empty>()
|
|
private let inactiveValueLabel = ComponentView<Empty>()
|
|
|
|
private let innerLeftTitleLabel = ComponentView<Empty>()
|
|
private let innerRightTitleLabel = ComponentView<Empty>()
|
|
|
|
private let activeContainer: UIView
|
|
private let activeBackground: SimpleLayer
|
|
|
|
private let activeTitleLabel = ComponentView<Empty>()
|
|
private let activeValueLabel = ComponentView<Empty>()
|
|
|
|
private let badgeView: UIView
|
|
private let badgeMaskView: UIView
|
|
private let badgeShapeLayer = CAShapeLayer()
|
|
|
|
private let badgeForeground: SimpleLayer
|
|
private let badgeLabel: BadgeLabelView
|
|
private let badgeLeftLabel = ComponentView<Empty>()
|
|
private let badgeLabelMaskView = UIImageView()
|
|
|
|
private var badgeTailPosition: CGFloat = 0.0
|
|
private var badgeShapeArguments: (Double, Double, CGSize, CGFloat, CGFloat)?
|
|
|
|
override init(frame: CGRect) {
|
|
self.container = UIView()
|
|
self.container.clipsToBounds = true
|
|
self.container.layer.cornerRadius = 9.0
|
|
|
|
self.inactiveBackground = SimpleLayer()
|
|
|
|
self.activeContainer = UIView()
|
|
self.activeContainer.clipsToBounds = true
|
|
|
|
self.activeBackground = SimpleLayer()
|
|
self.activeBackground.anchorPoint = CGPoint()
|
|
|
|
self.badgeView = UIView()
|
|
self.badgeView.alpha = 0.0
|
|
|
|
self.badgeShapeLayer.fillColor = UIColor.white.cgColor
|
|
self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
|
|
|
|
self.badgeMaskView = UIView()
|
|
self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer)
|
|
self.badgeView.mask = self.badgeMaskView
|
|
|
|
self.badgeForeground = SimpleLayer()
|
|
|
|
self.badgeLabel = BadgeLabelView()
|
|
let _ = self.badgeLabel.update(value: "0", transition: .immediate)
|
|
self.badgeLabel.mask = self.badgeLabelMaskView
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.addSubview(self.container)
|
|
self.container.layer.addSublayer(self.inactiveBackground)
|
|
self.container.addSubview(self.activeContainer)
|
|
self.activeContainer.layer.addSublayer(self.activeBackground)
|
|
|
|
self.addSubview(self.badgeView)
|
|
self.badgeView.layer.addSublayer(self.badgeForeground)
|
|
self.badgeView.addSubview(self.badgeLabel)
|
|
|
|
self.badgeLabelMaskView.contentMode = .scaleToFill
|
|
self.badgeLabelMaskView.image = generateImage(CGSize(width: 2.0, height: 30.0), rotatedContext: { size, context in
|
|
let bounds = CGRect(origin: .zero, size: size)
|
|
context.clear(bounds)
|
|
|
|
let colorsArray: [CGColor] = [
|
|
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
|
|
UIColor(rgb: 0xffffff).cgColor,
|
|
UIColor(rgb: 0xffffff).cgColor,
|
|
UIColor(rgb: 0xffffff, alpha: 0.0).cgColor,
|
|
]
|
|
var locations: [CGFloat] = [0.0, 0.24, 0.76, 1.0]
|
|
let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray as CFArray, locations: &locations)!
|
|
|
|
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
|
})
|
|
|
|
self.isUserInteractionEnabled = false
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
self.badgeShapeAnimator?.invalidate()
|
|
}
|
|
|
|
private var didPlayAppearanceAnimation = false
|
|
func playAppearanceAnimation(component: RemainingCountComponent, badgeFullSize: CGSize, from: CGFloat? = nil) {
|
|
if from == nil {
|
|
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
|
}
|
|
|
|
let rotationAngle: CGFloat
|
|
if badgeFullSize.width > 100.0 {
|
|
rotationAngle = 0.2
|
|
} else {
|
|
rotationAngle = 0.26
|
|
}
|
|
|
|
let to: CGFloat = self.badgeView.center.x
|
|
|
|
let positionAnimation = CABasicAnimation(keyPath: "position.x")
|
|
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: from ?? self.container.frame.width, y: 0.0))
|
|
positionAnimation.toValue = NSValue(cgPoint: CGPoint(x: to, y: 0.0))
|
|
positionAnimation.duration = 0.5
|
|
positionAnimation.fillMode = .forwards
|
|
positionAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
self.badgeView.layer.add(positionAnimation, forKey: "appearance1")
|
|
|
|
if from != to {
|
|
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
rotateAnimation.fromValue = 0.0 as NSNumber
|
|
rotateAnimation.toValue = rotationAngle as NSNumber
|
|
rotateAnimation.duration = 0.15
|
|
rotateAnimation.fillMode = .forwards
|
|
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
rotateAnimation.isRemovedOnCompletion = false
|
|
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
|
|
|
|
Queue.mainQueue().after(0.5, {
|
|
let bounceAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
bounceAnimation.fromValue = rotationAngle as NSNumber
|
|
bounceAnimation.toValue = -0.04 as NSNumber
|
|
bounceAnimation.duration = 0.2
|
|
bounceAnimation.fillMode = .forwards
|
|
bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
bounceAnimation.isRemovedOnCompletion = false
|
|
self.badgeView.layer.add(bounceAnimation, forKey: "appearance3")
|
|
self.badgeView.layer.removeAnimation(forKey: "appearance2")
|
|
|
|
Queue.mainQueue().after(0.2) {
|
|
let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
|
returnAnimation.fromValue = -0.04 as NSNumber
|
|
returnAnimation.toValue = 0.0 as NSNumber
|
|
returnAnimation.duration = 0.15
|
|
returnAnimation.fillMode = .forwards
|
|
returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
|
|
self.badgeView.layer.add(returnAnimation, forKey: "appearance4")
|
|
self.badgeView.layer.removeAnimation(forKey: "appearance3")
|
|
}
|
|
})
|
|
}
|
|
|
|
if from == nil {
|
|
self.badgeView.alpha = 1.0
|
|
self.badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
|
}
|
|
|
|
if let badgeText = component.badgeText {
|
|
let transition: ComponentTransition = .easeInOut(duration: from != nil ? 0.3 : 0.5)
|
|
var frameTransition = transition
|
|
if from == nil {
|
|
frameTransition = frameTransition.withAnimation(.none)
|
|
}
|
|
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: transition)
|
|
frameTransition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
|
|
}
|
|
}
|
|
|
|
var previousAvailableSize: CGSize?
|
|
func update(component: RemainingCountComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
|
self.component = component
|
|
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
|
|
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
|
|
|
|
let size = CGSize(width: availableSize.width, height: 90.0)
|
|
|
|
self.badgeLabel.color = component.activeTitleColor
|
|
|
|
let lineHeight: CGFloat = 30.0
|
|
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: size.height - lineHeight), size: CGSize(width: size.width, height: lineHeight))
|
|
self.container.frame = containerFrame
|
|
|
|
let activityPosition: CGFloat = floor(containerFrame.width * component.badgeGraphPosition)
|
|
let activeWidth: CGFloat = containerFrame.width - activityPosition
|
|
|
|
let leftTextColor: UIColor
|
|
let rightTextColor: UIColor
|
|
if component.invertProgress {
|
|
leftTextColor = component.inactiveTitleColor
|
|
rightTextColor = component.inactiveTitleColor
|
|
} else {
|
|
leftTextColor = component.inactiveTitleColor
|
|
rightTextColor = component.activeTitleColor
|
|
}
|
|
|
|
if "".isEmpty {
|
|
if component.invertProgress {
|
|
let innerLeftTitleSize = self.innerLeftTitleLabel.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(
|
|
NSAttributedString(
|
|
string: component.inactiveTitle,
|
|
font: Font.semibold(15.0),
|
|
textColor: component.activeTitleColor
|
|
)
|
|
)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let view = self.innerLeftTitleLabel.view {
|
|
if view.superview == nil {
|
|
self.activeContainer.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - innerLeftTitleSize.height) / 2.0)), size: innerLeftTitleSize)
|
|
}
|
|
|
|
let innerRightTitleSize = self.innerRightTitleLabel.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(
|
|
NSAttributedString(
|
|
string: component.activeValue,
|
|
font: Font.semibold(15.0),
|
|
textColor: component.activeTitleColor
|
|
)
|
|
)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let view = self.innerRightTitleLabel.view {
|
|
if view.superview == nil {
|
|
self.activeContainer.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - innerRightTitleSize.width, y: floorToScreenPixels((lineHeight - innerRightTitleSize.height) / 2.0)), size: innerRightTitleSize)
|
|
}
|
|
}
|
|
|
|
let inactiveTitleSize = self.inactiveTitleLabel.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(
|
|
NSAttributedString(
|
|
string: component.inactiveTitle,
|
|
font: Font.semibold(15.0),
|
|
textColor: leftTextColor
|
|
)
|
|
)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let view = self.inactiveTitleLabel.view {
|
|
if view.superview == nil {
|
|
self.container.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: 12.0, y: floorToScreenPixels((lineHeight - inactiveTitleSize.height) / 2.0)), size: inactiveTitleSize)
|
|
}
|
|
|
|
let inactiveValueSize = self.inactiveValueLabel.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(
|
|
NSAttributedString(
|
|
string: component.inactiveValue,
|
|
font: Font.semibold(15.0),
|
|
textColor: leftTextColor
|
|
)
|
|
)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let view = self.inactiveValueLabel.view {
|
|
if view.superview == nil {
|
|
self.container.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: activityPosition - 12.0 - inactiveValueSize.width, y: floorToScreenPixels((lineHeight - inactiveValueSize.height) / 2.0)), size: inactiveValueSize)
|
|
}
|
|
|
|
let activeTitleSize = self.activeTitleLabel.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(
|
|
NSAttributedString(
|
|
string: component.activeTitle,
|
|
font: Font.semibold(15.0),
|
|
textColor: rightTextColor
|
|
)
|
|
)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let view = self.activeTitleLabel.view {
|
|
if view.superview == nil {
|
|
self.container.addSubview(view)
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: activityPosition + 12.0, y: floorToScreenPixels((lineHeight - activeTitleSize.height) / 2.0)), size: activeTitleSize)
|
|
}
|
|
|
|
let activeValueSize = self.activeValueLabel.update(
|
|
transition: .immediate,
|
|
component: AnyComponent(
|
|
MultilineTextComponent(
|
|
text: .plain(
|
|
NSAttributedString(
|
|
string: component.activeValue,
|
|
font: Font.semibold(15.0),
|
|
textColor: rightTextColor
|
|
)
|
|
)
|
|
)
|
|
),
|
|
environment: {},
|
|
containerSize: availableSize
|
|
)
|
|
if let view = self.activeValueLabel.view {
|
|
if view.superview == nil {
|
|
self.container.addSubview(view)
|
|
|
|
if component.invertProgress {
|
|
self.container.bringSubviewToFront(self.activeContainer)
|
|
}
|
|
}
|
|
view.frame = CGRect(origin: CGPoint(x: containerFrame.width - 12.0 - activeValueSize.width, y: floorToScreenPixels((lineHeight - activeValueSize.height) / 2.0)), size: activeValueSize)
|
|
}
|
|
}
|
|
|
|
var progressTransition: ComponentTransition = .immediate
|
|
if !transition.animation.isImmediate {
|
|
progressTransition = .easeInOut(duration: 0.5)
|
|
}
|
|
if "".isEmpty {
|
|
if component.invertProgress {
|
|
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: size.width - activityPosition, height: lineHeight)))
|
|
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
|
|
progressTransition.setBounds(layer: self.activeBackground, bounds: CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
|
|
} else {
|
|
progressTransition.setFrame(layer: self.inactiveBackground, frame: CGRect(origin: .zero, size: CGSize(width: activityPosition, height: lineHeight)))
|
|
progressTransition.setFrame(view: self.activeContainer, frame: CGRect(origin: CGPoint(x: activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight)))
|
|
progressTransition.setFrame(layer: self.activeBackground, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: containerFrame.width * 1.35, height: lineHeight)))
|
|
}
|
|
if self.activeBackground.animation(forKey: "movement") == nil {
|
|
self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0)
|
|
}
|
|
}
|
|
|
|
let countWidth: CGFloat
|
|
if let badgeText = component.badgeText {
|
|
countWidth = CGFloat(badgeText.count) * 10.0
|
|
} else {
|
|
countWidth = 51.0
|
|
}
|
|
let badgeWidth: CGFloat = countWidth + 20.0
|
|
|
|
let badgeSize = CGSize(width: badgeWidth, height: 30.0)
|
|
let badgeFullSize = CGSize(width: badgeWidth, height: 30.0 + 8.0)
|
|
let tailSize = CGSize(width: 15.0, height: 6.0)
|
|
let tailRadius: CGFloat = 3.0
|
|
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize)
|
|
self.badgeShapeLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -4.0), size: badgeFullSize)
|
|
|
|
self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
|
|
|
|
let currentBadgeX = self.badgeView.center.x
|
|
|
|
let badgePosition = component.badgePosition
|
|
|
|
if badgePosition > 1.0 - 0.15 {
|
|
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 1.0, y: 1.0))
|
|
|
|
if progressTransition.animation.isImmediate {
|
|
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 1.0).cgPath
|
|
} else {
|
|
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 1.0)
|
|
self.animateBadgeTailPositionChange()
|
|
}
|
|
self.badgeTailPosition = 1.0
|
|
|
|
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
|
} else {
|
|
self.badgeView.center = CGPoint(x: 3.0 + (size.width - 6.0) * badgePosition + 3.0, y: 56.0)
|
|
}
|
|
} else if badgePosition < 0.15 {
|
|
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.0, y: 1.0))
|
|
|
|
if progressTransition.animation.isImmediate {
|
|
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.0).cgPath
|
|
} else {
|
|
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.0)
|
|
self.animateBadgeTailPositionChange()
|
|
}
|
|
self.badgeTailPosition = 0.0
|
|
|
|
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
|
|
|
} else {
|
|
self.badgeView.center = CGPoint(x: (size.width - 6.0) * badgePosition, y: 56.0)
|
|
}
|
|
} else {
|
|
progressTransition.setAnchorPoint(layer: self.badgeView.layer, anchorPoint: CGPoint(x: 0.5, y: 1.0))
|
|
|
|
if progressTransition.animation.isImmediate {
|
|
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: 0.5).cgPath
|
|
} else {
|
|
self.badgeShapeArguments = (CACurrentMediaTime(), 0.5, badgeSize, self.badgeTailPosition, 0.5)
|
|
self.animateBadgeTailPositionChange()
|
|
}
|
|
self.badgeTailPosition = 0.5
|
|
|
|
if let _ = self.badgeView.layer.animation(forKey: "appearance1") {
|
|
|
|
} else {
|
|
self.badgeView.center = CGPoint(x: size.width * badgePosition, y: 56.0)
|
|
}
|
|
}
|
|
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeFullSize.width * 3.0, height: badgeFullSize.height))
|
|
if self.badgeForeground.animation(forKey: "movement") == nil {
|
|
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0 - self.badgeForeground.frame.width * 0.35, y: badgeFullSize.height / 2.0)
|
|
}
|
|
|
|
self.badgeLabelMaskView.frame = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 36.0)
|
|
|
|
if !self.didPlayAppearanceAnimation || !transition.animation.isImmediate {
|
|
self.didPlayAppearanceAnimation = true
|
|
if transition.animation.isImmediate {
|
|
if component.badgePosition < 0.1 {
|
|
self.badgeView.alpha = 1.0
|
|
if let badgeText = component.badgeText {
|
|
let badgeLabelSize = self.badgeLabel.update(value: badgeText, transition: .immediate)
|
|
transition.setFrame(view: self.badgeLabel, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((badgeFullSize.width - badgeLabelSize.width) / 2.0), y: -2.0), size: badgeLabelSize))
|
|
}
|
|
} else {
|
|
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize)
|
|
}
|
|
} else {
|
|
self.playAppearanceAnimation(component: component, badgeFullSize: badgeFullSize, from: currentBadgeX)
|
|
}
|
|
}
|
|
|
|
if self.previousAvailableSize != availableSize {
|
|
self.previousAvailableSize = availableSize
|
|
|
|
var locations: [CGFloat] = []
|
|
let delta = 1.0 / CGFloat(component.activeColors.count - 1)
|
|
for i in 0 ..< component.activeColors.count {
|
|
locations.append(delta * CGFloat(i))
|
|
}
|
|
|
|
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: component.activeColors, locations: locations, direction: .horizontal)
|
|
self.badgeForeground.contentsGravity = .resizeAspectFill
|
|
self.badgeForeground.contents = gradient?.cgImage
|
|
|
|
self.activeBackground.contentsGravity = .resizeAspectFill
|
|
self.activeBackground.contents = gradient?.cgImage
|
|
|
|
self.setupGradientAnimations()
|
|
}
|
|
|
|
return size
|
|
}
|
|
|
|
private var badgeShapeAnimator: ConstantDisplayLinkAnimator?
|
|
private func animateBadgeTailPositionChange() {
|
|
if self.badgeShapeAnimator == nil {
|
|
self.badgeShapeAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
|
|
self?.animateBadgeTailPositionChange()
|
|
})
|
|
self.badgeShapeAnimator?.isPaused = true
|
|
}
|
|
|
|
if let (startTime, duration, badgeSize, initial, target) = self.badgeShapeArguments {
|
|
self.badgeShapeAnimator?.isPaused = false
|
|
|
|
let t = CGFloat(max(0.0, min(1.0, (CACurrentMediaTime() - startTime) / duration)))
|
|
let value = initial + (target - initial) * t
|
|
|
|
let tailSize = CGSize(width: 15.0, height: 6.0)
|
|
let tailRadius: CGFloat = 3.0
|
|
self.badgeShapeLayer.path = generateRoundedRectWithTailPath(rectSize: badgeSize, tailSize: tailSize, tailRadius: tailRadius, tailPosition: value).cgPath
|
|
|
|
if t >= 1.0 {
|
|
self.badgeShapeArguments = nil
|
|
self.badgeShapeAnimator?.isPaused = true
|
|
self.badgeShapeAnimator?.invalidate()
|
|
self.badgeShapeAnimator = nil
|
|
}
|
|
} else {
|
|
self.badgeShapeAnimator?.isPaused = true
|
|
self.badgeShapeAnimator?.invalidate()
|
|
self.badgeShapeAnimator = nil
|
|
}
|
|
}
|
|
|
|
private func setupGradientAnimations() {
|
|
guard let _ = self.component else {
|
|
return
|
|
}
|
|
if let _ = self.badgeForeground.animation(forKey: "movement") {
|
|
} else {
|
|
CATransaction.begin()
|
|
|
|
let badgeOffset = (self.badgeForeground.frame.width - self.badgeView.bounds.width) / 2.0
|
|
let badgePreviousValue = self.badgeForeground.position.x
|
|
var badgeNewValue: CGFloat = badgeOffset
|
|
if badgeOffset - badgePreviousValue < self.badgeForeground.frame.width * 0.25 {
|
|
badgeNewValue -= self.badgeForeground.frame.width * 0.35
|
|
}
|
|
self.badgeForeground.position = CGPoint(x: badgeNewValue, y: self.badgeForeground.bounds.size.height / 2.0)
|
|
|
|
let lineOffset = 0.0
|
|
let linePreviousValue = self.activeBackground.position.x
|
|
var lineNewValue: CGFloat = lineOffset
|
|
if linePreviousValue < 0.0 {
|
|
lineNewValue = 0.0
|
|
} else {
|
|
lineNewValue = -self.activeContainer.bounds.width * 0.35
|
|
}
|
|
lineNewValue -= self.activeContainer.frame.minX
|
|
self.activeBackground.position = CGPoint(x: lineNewValue, y: 0.0)
|
|
|
|
let badgeAnimation = CABasicAnimation(keyPath: "position.x")
|
|
badgeAnimation.duration = 4.5
|
|
badgeAnimation.fromValue = badgePreviousValue
|
|
badgeAnimation.toValue = badgeNewValue
|
|
badgeAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
|
CATransaction.setCompletionBlock { [weak self] in
|
|
self?.setupGradientAnimations()
|
|
}
|
|
self.badgeForeground.add(badgeAnimation, forKey: "movement")
|
|
|
|
let lineAnimation = CABasicAnimation(keyPath: "position.x")
|
|
lineAnimation.duration = 4.5
|
|
lineAnimation.fromValue = linePreviousValue
|
|
lineAnimation.toValue = lineNewValue
|
|
lineAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
self.activeBackground.add(lineAnimation, forKey: "movement")
|
|
|
|
CATransaction.commit()
|
|
}
|
|
}
|
|
}
|
|
|
|
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, transition: transition)
|
|
}
|
|
}
|
|
|
|
|
|
private let labelWidth: CGFloat = 10.0
|
|
private let labelHeight: CGFloat = 30.0
|
|
private let labelSize = CGSize(width: labelWidth, height: labelHeight)
|
|
private let font = Font.with(size: 15.0, design: .regular, weight: .semibold, traits: [])
|
|
|
|
final class BadgeLabelView: UIView {
|
|
private class StackView: UIView {
|
|
var labels: [UILabel] = []
|
|
|
|
var currentValue: Int32 = 0
|
|
|
|
var color: UIColor = .white {
|
|
didSet {
|
|
for view in self.labels {
|
|
view.textColor = self.color
|
|
}
|
|
}
|
|
}
|
|
|
|
init() {
|
|
super.init(frame: CGRect(origin: .zero, size: labelSize))
|
|
|
|
var height: CGFloat = -labelHeight
|
|
for i in -1 ..< 10 {
|
|
let label = UILabel()
|
|
if i == -1 {
|
|
label.text = "9"
|
|
} else {
|
|
label.text = "\(i)"
|
|
}
|
|
label.textColor = self.color
|
|
label.font = font
|
|
label.textAlignment = .center
|
|
label.frame = CGRect(x: 0, y: height, width: labelWidth, height: labelHeight)
|
|
self.addSubview(label)
|
|
self.labels.append(label)
|
|
|
|
height += labelHeight
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
func update(value: Int32, isFirst: Bool, isLast: Bool, transition: ComponentTransition) {
|
|
let previousValue = self.currentValue
|
|
self.currentValue = value
|
|
|
|
self.labels[1].alpha = isFirst && !isLast ? 0.0 : 1.0
|
|
|
|
if previousValue == 9 && value < 9 {
|
|
self.bounds = CGRect(
|
|
origin: CGPoint(
|
|
x: 0.0,
|
|
y: -1.0 * labelSize.height
|
|
),
|
|
size: labelSize
|
|
)
|
|
}
|
|
|
|
let bounds = CGRect(
|
|
origin: CGPoint(
|
|
x: 0.0,
|
|
y: CGFloat(value) * labelSize.height
|
|
),
|
|
size: labelSize
|
|
)
|
|
transition.setBounds(view: self, bounds: bounds)
|
|
}
|
|
}
|
|
|
|
private var itemViews: [Int: StackView] = [:]
|
|
|
|
init() {
|
|
super.init(frame: .zero)
|
|
|
|
self.clipsToBounds = true
|
|
self.isUserInteractionEnabled = false
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
var color: UIColor = .white {
|
|
didSet {
|
|
for (_, view) in self.itemViews {
|
|
view.color = self.color
|
|
}
|
|
}
|
|
}
|
|
|
|
func update(value: String, transition: ComponentTransition) -> CGSize {
|
|
let string = value
|
|
let stringArray = Array(string.map { String($0) }.reversed())
|
|
|
|
let totalWidth = CGFloat(stringArray.count) * labelWidth
|
|
|
|
var validIds: [Int] = []
|
|
for i in 0 ..< stringArray.count {
|
|
validIds.append(i)
|
|
|
|
let itemView: StackView
|
|
var itemTransition = transition
|
|
if let current = self.itemViews[i] {
|
|
itemView = current
|
|
} else {
|
|
itemTransition = transition.withAnimation(.none)
|
|
itemView = StackView()
|
|
itemView.color = self.color
|
|
self.itemViews[i] = itemView
|
|
self.addSubview(itemView)
|
|
}
|
|
|
|
let digit = Int32(stringArray[i]) ?? 0
|
|
itemView.update(value: digit, isFirst: i == stringArray.count - 1, isLast: i == 0, transition: transition)
|
|
|
|
itemTransition.setFrame(
|
|
view: itemView,
|
|
frame: CGRect(x: totalWidth - labelWidth * CGFloat(i + 1), y: 0.0, width: labelWidth, height: labelHeight)
|
|
)
|
|
}
|
|
|
|
var removeIds: [Int] = []
|
|
for (id, itemView) in self.itemViews {
|
|
if !validIds.contains(id) {
|
|
removeIds.append(id)
|
|
|
|
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
|
|
itemView.removeFromSuperview()
|
|
})
|
|
}
|
|
}
|
|
for id in removeIds {
|
|
self.itemViews.removeValue(forKey: id)
|
|
}
|
|
return CGSize(width: totalWidth, height: labelHeight)
|
|
}
|
|
}
|