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 import GlassBackgroundComponent 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 private let leftString: String private let groupingSeparator: String 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, leftString: String, groupingSeparator: String ) { 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 self.leftString = leftString self.groupingSeparator = groupingSeparator } 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 } if lhs.leftString != rhs.leftString { return false } if lhs.groupingSeparator != rhs.groupingSeparator { 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() private let inactiveValueLabel = ComponentView() private let innerLeftTitleLabel = ComponentView() private let innerRightTitleLabel = ComponentView() private let activeContainer: UIView private let activeBackground: SimpleLayer private let activeTitleLabel = ComponentView() private let activeValueLabel = ComponentView() private let activeChromeView = UIImageView() override init(frame: CGRect) { self.container = UIView() self.container.clipsToBounds = true self.container.layer.cornerRadius = 15.0 self.inactiveBackground = SimpleLayer() self.activeContainer = UIView() self.activeContainer.clipsToBounds = true self.activeContainer.layer.cornerRadius = 15.0 self.activeBackground = SimpleLayer() self.activeBackground.anchorPoint = CGPoint() 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.activeContainer.addSubview(self.activeChromeView) self.activeChromeView.layer.compositingFilter = "overlayBlendMode" self.activeChromeView.alpha = 0.8 self.activeChromeView.image = GlassBackgroundView.generateForegroundImage(size: CGSize(width: 30.0, height: 30.0), isDark: false, fillColor: .clear) self.isUserInteractionEnabled = false } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } 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) 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 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 - 15.0, y: 0.0), size: CGSize(width: size.width - activityPosition + 15.0, 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))) } progressTransition.setFrame(view: self.activeChromeView, frame: CGRect(origin: CGPoint(x: -activityPosition, y: 0.0), size: CGSize(width: activeWidth, height: lineHeight))) if self.activeBackground.animation(forKey: "movement") == nil { self.activeBackground.position = CGPoint(x: -self.activeContainer.frame.width * 0.35, y: lineHeight / 2.0) } } 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: Array(component.activeColors.reversed()), locations: locations, direction: .horizontal) self.activeBackground.contentsGravity = .resizeAspectFill self.activeBackground.contents = gradient?.cgImage self.setupGradientAnimations() } return size } private func setupGradientAnimations() { guard let _ = self.component else { return } if let _ = self.activeBackground.animation(forKey: "movement") { } else { CATransaction.begin() 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 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, transition: ComponentTransition) -> CGSize { return view.update(component: self, availableSize: availableSize, transition: transition) } }