mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Various improvements
This commit is contained in:
parent
ff7cc72a71
commit
5e65806ac3
@ -321,13 +321,18 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch
|
||||
case .limitExceeded:
|
||||
f(.default)
|
||||
|
||||
let limitScreen = PremiumLimitScreen(context: context, subject: .pins, action: {
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .pins, action: {
|
||||
dismissImpl?()
|
||||
let premiumScreen = PremiumIntroScreen(context: context, action: {
|
||||
|
||||
})
|
||||
chatListController?.push(premiumScreen)
|
||||
})
|
||||
chatListController?.push(limitScreen)
|
||||
chatListController?.push(controller)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
})
|
||||
})))
|
||||
|
@ -10,6 +10,7 @@ import ItemListUI
|
||||
import AccountContext
|
||||
import ItemListPeerActionItem
|
||||
import ChatListFilterSettingsHeaderItem
|
||||
import PremiumUI
|
||||
|
||||
private final class ChatListFilterPresetListControllerArguments {
|
||||
let context: AccountContext
|
||||
@ -246,6 +247,17 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
||||
var pushControllerImpl: ((ViewController) -> Void)?
|
||||
var presentControllerImpl: ((ViewController) -> Void)?
|
||||
|
||||
let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters()
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in
|
||||
return .single(filters.map { filter -> (ChatListFilter, Int) in
|
||||
return (filter, 0)
|
||||
})
|
||||
}
|
||||
|
||||
let filtersWithCounts = Promise<[(ChatListFilter, Int)]>()
|
||||
filtersWithCounts.set(filtersWithCountsSignal)
|
||||
|
||||
let arguments = ChatListFilterPresetListControllerArguments(context: context,
|
||||
addSuggestedPresed: { title, data in
|
||||
let _ = (context.engine.peers.updateChatListFiltersInteractively { filters in
|
||||
@ -259,7 +271,42 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
||||
}, openPreset: { preset in
|
||||
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: preset, updated: { _ in }))
|
||||
}, addNew: {
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId),
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false),
|
||||
TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true)
|
||||
),
|
||||
filtersWithCounts.get() |> take(1)
|
||||
).start(next: { result, filters in
|
||||
let (accountPeer, limits, premiumLimits) = result
|
||||
let limit = limits.maxFoldersCount
|
||||
let premiumLimit = premiumLimits.maxFoldersCount
|
||||
if let accountPeer = accountPeer, accountPeer.isPremium {
|
||||
if filters.count >= premiumLimit {
|
||||
//printPremiumError
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if filters.count >= limit {
|
||||
var dismissImpl: (() -> Void)?
|
||||
let controller = PremiumLimitScreen(context: context, subject: .folders, action: {
|
||||
dismissImpl?()
|
||||
let controller = PremiumIntroScreen(context: context, action: {
|
||||
|
||||
})
|
||||
pushControllerImpl?(controller)
|
||||
})
|
||||
pushControllerImpl?(controller)
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismiss()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
pushControllerImpl?(chatListFilterPresetController(context: context, currentPreset: nil, updated: { _ in }))
|
||||
})
|
||||
}, setItemWithRevealedOptions: { preset, fromPreset in
|
||||
updateState { state in
|
||||
var state = state
|
||||
@ -297,14 +344,6 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
||||
presentControllerImpl?(actionSheet)
|
||||
})
|
||||
|
||||
let filtersWithCountsSignal = context.engine.peers.updatedChatListFilters()
|
||||
|> distinctUntilChanged
|
||||
|> mapToSignal { filters -> Signal<[(ChatListFilter, Int)], NoError> in
|
||||
return .single(filters.map { filter -> (ChatListFilter, Int) in
|
||||
return (filter, 0)
|
||||
})
|
||||
}
|
||||
|
||||
let featuredFilters = context.account.postbox.preferencesView(keys: [PreferencesKeys.chatListFiltersFeaturedState])
|
||||
|> map { preferences -> [ChatListFeaturedFilter] in
|
||||
guard let state = preferences.values[PreferencesKeys.chatListFiltersFeaturedState]?.get(ChatListFiltersFeaturedState.self) else {
|
||||
@ -314,9 +353,6 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let filtersWithCounts = Promise<[(ChatListFilter, Int)]>()
|
||||
filtersWithCounts.set(filtersWithCountsSignal)
|
||||
|
||||
let updatedFilterOrder = Promise<[Int32]?>(nil)
|
||||
|
||||
let preferences = context.account.postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.chatListFilterSettings])
|
||||
|
Binary file not shown.
@ -14,6 +14,14 @@ import SolidRoundedButtonComponent
|
||||
import Markdown
|
||||
import SceneKit
|
||||
|
||||
private func deg2rad(_ number: Float) -> Float {
|
||||
return number * .pi / 180
|
||||
}
|
||||
|
||||
private func rad2deg(_ number: Float) -> Float {
|
||||
return number * 180.0 / .pi
|
||||
}
|
||||
|
||||
private class StarComponent: Component {
|
||||
static func ==(lhs: StarComponent, rhs: StarComponent) -> Bool {
|
||||
return true
|
||||
@ -41,18 +49,97 @@ private class StarComponent: Component {
|
||||
self.sceneView = SCNView(frame: frame)
|
||||
self.sceneView.backgroundColor = .clear
|
||||
self.sceneView.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
||||
self.sceneView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.sceneView)
|
||||
|
||||
self.setup()
|
||||
|
||||
let panGestureRecoginzer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
self.addGestureRecognizer(panGestureRecoginzer)
|
||||
|
||||
let tapGestureRecoginzer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapGestureRecoginzer)
|
||||
|
||||
self.disablesInteractiveModalDismiss = true
|
||||
self.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
|
||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||
return
|
||||
}
|
||||
|
||||
var left = true
|
||||
if let view = gesture.view {
|
||||
let point = gesture.location(in: view)
|
||||
if point.x > view.frame.width / 2.0 {
|
||||
left = false
|
||||
}
|
||||
}
|
||||
|
||||
let initial = node.rotation
|
||||
let target = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: left ? -0.6 : 0.6)
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "rotation")
|
||||
animation.fromValue = NSValue(scnVector4: initial)
|
||||
animation.toValue = NSValue(scnVector4: target)
|
||||
animation.duration = 0.25
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
animation.fillMode = .forwards
|
||||
node.addAnimation(animation, forKey: "rotate")
|
||||
|
||||
node.rotation = target
|
||||
|
||||
Queue.mainQueue().after(0.25) {
|
||||
node.rotation = initial
|
||||
let springAnimation = CASpringAnimation(keyPath: "rotation")
|
||||
springAnimation.fromValue = NSValue(scnVector4: target)
|
||||
springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0))
|
||||
springAnimation.mass = 1.0
|
||||
springAnimation.stiffness = 21.0
|
||||
springAnimation.damping = 5.8
|
||||
springAnimation.duration = springAnimation.settlingDuration * 0.8
|
||||
node.addAnimation(springAnimation, forKey: "rotate")
|
||||
}
|
||||
}
|
||||
|
||||
private var previousAngle: Float = 0.0
|
||||
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
self.previousAngle = 0.0
|
||||
case .changed:
|
||||
let translation = gesture.translation(in: gesture.view)
|
||||
let anglePan = deg2rad(Float(translation.x))
|
||||
|
||||
self.previousAngle = anglePan
|
||||
node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: self.previousAngle)
|
||||
case .ended:
|
||||
let velocity = gesture.velocity(in: gesture.view)
|
||||
|
||||
var small = false
|
||||
if (self.previousAngle < .pi / 2 && self.previousAngle > -.pi / 2) && abs(velocity.x) < 200 {
|
||||
small = true
|
||||
}
|
||||
|
||||
self.playAppearanceAnimation(velocity: velocity.x, small: small)
|
||||
node.rotation = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
guard let scene = SCNScene(named: "star.scn") else {
|
||||
return
|
||||
@ -77,11 +164,11 @@ private class StarComponent: Component {
|
||||
self.setupGradientAnimation()
|
||||
self.setupShineAnimation()
|
||||
|
||||
self.playAppearanceAnimation()
|
||||
self.playAppearanceAnimation(boom: true)
|
||||
}
|
||||
|
||||
private func setupGradientAnimation() {
|
||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: true) else {
|
||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||
return
|
||||
}
|
||||
guard let initial = node.geometry?.materials.first?.diffuse.contentsTransform else {
|
||||
@ -124,21 +211,37 @@ private class StarComponent: Component {
|
||||
node.geometry?.materials.first?.emission.addAnimation(group, forKey: "shimmer")
|
||||
}
|
||||
|
||||
private func playAppearanceAnimation() {
|
||||
guard let scene = self.sceneView.scene, let starNode = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||
private func playAppearanceAnimation(velocity: CGFloat? = nil, small: Bool = false, boom: Bool = false) {
|
||||
guard let scene = self.sceneView.scene, let node = scene.rootNode.childNode(withName: "star", recursively: false) else {
|
||||
return
|
||||
}
|
||||
|
||||
if boom, let node = scene.rootNode.childNode(withName: "swirl", recursively: false), let particles = scene.rootNode.childNode(withName: "particles", recursively: false) {
|
||||
node.physicsField?.isActive = true
|
||||
Queue.mainQueue().after(1.0) {
|
||||
node.physicsField?.isActive = false
|
||||
particles.particleSystems?.first?.birthRate = 0.8
|
||||
}
|
||||
}
|
||||
|
||||
let from = node.rotation
|
||||
var toValue: Float = small ? 0.0 : .pi * 2.0
|
||||
if let velocity = velocity, !small && abs(velocity) > 200 && velocity < 0.0 {
|
||||
toValue *= -1
|
||||
}
|
||||
let to = SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: toValue)
|
||||
let distance = rad2deg(to.w - from.w)
|
||||
|
||||
let springAnimation = CASpringAnimation(keyPath: "rotation")
|
||||
springAnimation.fromValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: 0.0))
|
||||
springAnimation.toValue = NSValue(scnVector4: SCNVector4(x: 0.0, y: 1.0, z: 0.0, w: .pi * 2.0))
|
||||
springAnimation.fromValue = NSValue(scnVector4: from)
|
||||
springAnimation.toValue = NSValue(scnVector4: to)
|
||||
springAnimation.mass = 1.0
|
||||
springAnimation.stiffness = 21.0
|
||||
springAnimation.damping = 5.8
|
||||
springAnimation.duration = 1.5
|
||||
springAnimation.initialVelocity = 1.7
|
||||
springAnimation.duration = springAnimation.settlingDuration * 0.75
|
||||
springAnimation.initialVelocity = velocity.flatMap { abs($0 / CGFloat(distance)) } ?? 1.7
|
||||
|
||||
starNode.addAnimation(springAnimation, forKey: "rotate")
|
||||
node.addAnimation(springAnimation, forKey: "rotate")
|
||||
}
|
||||
|
||||
func update(component: StarComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
@ -631,7 +734,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
.position(CGPoint(x: fade.size.width / 2.0, y: fade.size.height / 2.0))
|
||||
)
|
||||
|
||||
size.height += 183.0
|
||||
size.height += 183.0 + 10.0
|
||||
|
||||
let textColor = theme.list.itemPrimaryTextColor
|
||||
let titleColor = theme.list.itemPrimaryTextColor
|
||||
@ -1053,7 +1156,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
let topPanelAlpha: CGFloat
|
||||
let titleOffset: CGFloat
|
||||
let titleScale: CGFloat
|
||||
let titleOffsetDelta = 150.0 - environment.navigationHeight / 2.0
|
||||
let titleOffsetDelta = 160.0 - environment.navigationHeight / 2.0
|
||||
|
||||
if let topContentOffset = state.topContentOffset {
|
||||
topPanelAlpha = min(30.0, max(0.0, topContentOffset - 64.0)) / 30.0
|
||||
@ -1066,7 +1169,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
context.add(star
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 18.0 - titleOffset * titleScale))
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: star.size.height / 2.0 - 10.0 - titleOffset * titleScale))
|
||||
.scale(titleScale)
|
||||
)
|
||||
|
||||
@ -1080,7 +1183,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
|
||||
)
|
||||
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(150.0 - titleOffset, environment.navigationHeight / 2.0)))
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: max(160.0 - titleOffset, environment.navigationHeight / 2.0)))
|
||||
.scale(titleScale)
|
||||
)
|
||||
|
||||
|
@ -16,6 +16,410 @@ import BundleIconComponent
|
||||
import SolidRoundedButtonComponent
|
||||
import Markdown
|
||||
|
||||
private class PremiumLimitAnimationComponent: Component {
|
||||
private let iconName: String
|
||||
private let inactiveColor: UIColor
|
||||
private let activeColors: [UIColor]
|
||||
private let textColor: UIColor
|
||||
|
||||
init(
|
||||
iconName: String,
|
||||
inactiveColor: UIColor,
|
||||
activeColors: [UIColor],
|
||||
textColor: UIColor
|
||||
) {
|
||||
self.iconName = iconName
|
||||
self.inactiveColor = inactiveColor
|
||||
self.activeColors = activeColors
|
||||
self.textColor = textColor
|
||||
}
|
||||
|
||||
static func ==(lhs: PremiumLimitAnimationComponent, rhs: PremiumLimitAnimationComponent) -> Bool {
|
||||
if lhs.iconName != rhs.iconName {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveColor != rhs.inactiveColor {
|
||||
return false
|
||||
}
|
||||
if lhs.activeColors != rhs.activeColors {
|
||||
return false
|
||||
}
|
||||
if lhs.textColor != rhs.textColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let container: SimpleLayer
|
||||
private let inactiveBackground: SimpleLayer
|
||||
|
||||
private let activeContainer: SimpleLayer
|
||||
private let activeBackground: SimpleLayer
|
||||
|
||||
private let badgeView: UIView
|
||||
private let badgeMaskView: UIView
|
||||
private let badgeMaskBackgroundView: UIView
|
||||
private let badgeMaskArrowView: UIImageView
|
||||
private let badgeForeground: SimpleLayer
|
||||
private let badgeIcon: UIImageView
|
||||
private let badgeCountLabel: RollingLabel
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.container = SimpleLayer()
|
||||
self.container.masksToBounds = true
|
||||
self.container.cornerRadius = 6.0
|
||||
|
||||
self.inactiveBackground = SimpleLayer()
|
||||
|
||||
self.activeContainer = SimpleLayer()
|
||||
self.activeContainer.masksToBounds = true
|
||||
|
||||
self.activeBackground = SimpleLayer()
|
||||
|
||||
self.badgeView = UIView()
|
||||
self.badgeView.layer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
|
||||
|
||||
self.badgeMaskBackgroundView = UIView()
|
||||
self.badgeMaskBackgroundView.backgroundColor = .white
|
||||
self.badgeMaskBackgroundView.layer.cornerRadius = 24.0
|
||||
|
||||
self.badgeMaskArrowView = UIImageView()
|
||||
self.badgeMaskArrowView.image = generateImage(CGSize(width: 44.0, height: 12.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.scaleBy(x: 3.76, y: 3.76)
|
||||
context.translateBy(x: -9.3, y: -12.7)
|
||||
try? drawSvgPath(context, path: "M6.4,0.0 C2.9,0.0 0.0,2.84 0.0,6.35 C0.0,9.86 2.9,12.7 6.4,12.7 H9.302 H11.3 C11.7,12.7 12.1,12.87 12.4,13.17 L14.4,15.13 C14.8,15.54 15.5,15.54 15.9,15.13 L17.8,13.17 C18.1,12.87 18.5,12.7 18.9,12.7 H20.9 H23.6 C27.1,12.7 29.9,9.86 29.9,6.35 C29.9,2.84 27.1,0.0 23.6,0.0 Z ")
|
||||
})
|
||||
|
||||
self.badgeMaskView = UIView()
|
||||
self.badgeMaskView.addSubview(self.badgeMaskBackgroundView)
|
||||
self.badgeMaskView.addSubview(self.badgeMaskArrowView)
|
||||
self.badgeView.mask = self.badgeMaskView
|
||||
|
||||
self.badgeForeground = SimpleLayer()
|
||||
|
||||
self.badgeIcon = UIImageView()
|
||||
self.badgeIcon.contentMode = .center
|
||||
|
||||
self.badgeCountLabel = RollingLabel()
|
||||
self.badgeCountLabel.font = Font.with(size: 24.0, design: .round, weight: .semibold, traits: [])
|
||||
self.badgeCountLabel.textColor = .white
|
||||
self.badgeCountLabel.text(num: 0)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.container)
|
||||
self.container.addSublayer(self.inactiveBackground)
|
||||
self.container.addSublayer(self.activeContainer)
|
||||
self.activeContainer.addSublayer(self.activeBackground)
|
||||
|
||||
self.addSubview(self.badgeView)
|
||||
self.badgeView.layer.addSublayer(self.badgeForeground)
|
||||
self.badgeView.addSubview(self.badgeIcon)
|
||||
self.badgeView.addSubview(self.badgeCountLabel)
|
||||
|
||||
self.isUserInteractionEnabled = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private var didPlayAppearanceAnimation = false
|
||||
func playAppearanceAnimation(availableSize: CGSize) {
|
||||
self.badgeView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.4, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
|
||||
|
||||
let now = self.badgeView.layer.convertTime(CACurrentMediaTime(), from: nil)
|
||||
|
||||
let positionAnimation = CABasicAnimation(keyPath: "position.x")
|
||||
positionAnimation.fromValue = NSValue(cgPoint: CGPoint(x: -availableSize.width / 2.0, y: 0.0))
|
||||
positionAnimation.toValue = NSValue(cgPoint: CGPoint())
|
||||
positionAnimation.isAdditive = true
|
||||
positionAnimation.duration = 0.5
|
||||
positionAnimation.fillMode = .forwards
|
||||
positionAnimation.beginTime = now
|
||||
|
||||
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
rotateAnimation.fromValue = 0.0 as NSNumber
|
||||
rotateAnimation.toValue = 0.2 as NSNumber
|
||||
rotateAnimation.isAdditive = true
|
||||
rotateAnimation.duration = 0.2
|
||||
rotateAnimation.beginTime = now + 0.5
|
||||
rotateAnimation.fillMode = .forwards
|
||||
rotateAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
|
||||
let returnAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
returnAnimation.fromValue = 0.2 as NSNumber
|
||||
returnAnimation.toValue = 0.0 as NSNumber
|
||||
returnAnimation.isAdditive = true
|
||||
returnAnimation.duration = 0.18
|
||||
returnAnimation.beginTime = now + 0.5 + 0.2
|
||||
returnAnimation.fillMode = .forwards
|
||||
returnAnimation.timingFunction = CAMediaTimingFunction(name: .easeIn)
|
||||
|
||||
self.badgeView.layer.add(positionAnimation, forKey: "appearance1")
|
||||
self.badgeView.layer.add(rotateAnimation, forKey: "appearance2")
|
||||
self.badgeView.layer.add(returnAnimation, forKey: "appearance3")
|
||||
|
||||
self.badgeCountLabel.text(num: 4)
|
||||
}
|
||||
|
||||
var previousAvailableSize: CGSize?
|
||||
func update(component: PremiumLimitAnimationComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||
self.inactiveBackground.backgroundColor = component.inactiveColor.cgColor
|
||||
self.activeBackground.backgroundColor = component.activeColors.last?.cgColor
|
||||
|
||||
self.badgeIcon.image = UIImage(bundleImageName: component.iconName)?.withRenderingMode(.alwaysTemplate)
|
||||
self.badgeIcon.tintColor = component.textColor
|
||||
|
||||
let lineHeight: CGFloat = 30.0
|
||||
let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - lineHeight), size: CGSize(width: availableSize.width, height: lineHeight))
|
||||
self.container.frame = containerFrame
|
||||
|
||||
self.inactiveBackground.frame = CGRect(origin: .zero, size: CGSize(width: containerFrame.width / 2.0 - 1.0, height: lineHeight))
|
||||
self.activeContainer.frame = CGRect(origin: CGPoint(x: containerFrame.width / 2.0 + 1.0, y: 0.0), size: CGSize(width: containerFrame.width / 2.0 - 1.0, height: lineHeight))
|
||||
|
||||
self.activeBackground.bounds = CGRect(origin: .zero, size: CGSize(width: containerFrame.width * 3.0 / 2.0, height: lineHeight))
|
||||
if self.activeBackground.animation(forKey: "movement") == nil {
|
||||
self.activeBackground.position = CGPoint(x: containerFrame.width * 3.0 / 4.0, y: lineHeight / 2.0)
|
||||
}
|
||||
|
||||
let badgeSize = CGSize(width: 82.0, height: 48.0 + 12.0)
|
||||
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeSize)
|
||||
self.badgeMaskBackgroundView.frame = CGRect(origin: .zero, size: CGSize(width: badgeSize.width, height: 48.0))
|
||||
self.badgeMaskArrowView.frame = CGRect(origin: CGPoint(x: (badgeSize.width - 44.0) / 2.0, y: badgeSize.height - 12.0), size: CGSize(width: 44.0, height: 12.0))
|
||||
|
||||
self.badgeView.bounds = CGRect(origin: .zero, size: badgeSize)
|
||||
self.badgeView.center = CGPoint(x: availableSize.width / 2.0, y: 82.0)
|
||||
if self.badgeForeground.animation(forKey: "movement") == nil {
|
||||
self.badgeForeground.position = CGPoint(x: badgeSize.width * 3.0 / 2.0, y: badgeSize.height / 2.0)
|
||||
}
|
||||
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: badgeSize.width * 3.0, height: badgeSize.height))
|
||||
|
||||
self.badgeIcon.frame = CGRect(x: 15.0, y: 9.0, width: 30.0, height: 30.0)
|
||||
|
||||
self.badgeCountLabel.frame = CGRect(x: badgeSize.width - 36.0, y: 10.0, width: 30.0, height: 48.0)
|
||||
|
||||
if !self.didPlayAppearanceAnimation {
|
||||
self.didPlayAppearanceAnimation = true
|
||||
self.playAppearanceAnimation(availableSize: availableSize)
|
||||
}
|
||||
|
||||
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 availableSize
|
||||
}
|
||||
|
||||
private func setupGradientAnimations() {
|
||||
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 = (self.activeBackground.frame.width - self.activeContainer.bounds.width) / 2.0
|
||||
let linePreviousValue = self.activeBackground.position.x
|
||||
var lineNewValue: CGFloat = lineOffset
|
||||
if lineOffset - linePreviousValue < self.activeBackground.frame.width * 0.25 {
|
||||
lineNewValue -= self.activeBackground.frame.width * 0.35
|
||||
}
|
||||
self.activeBackground.position = CGPoint(x: lineNewValue, y: self.activeBackground.bounds.size.height / 2.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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class PremiumLimitDisplayComponent: CombinedComponent {
|
||||
public let inactiveColor: UIColor
|
||||
public let activeColors: [UIColor]
|
||||
public let inactiveTitle: String
|
||||
public let inactiveTitleColor: UIColor
|
||||
public let activeTitle: String
|
||||
public let activeValue: String
|
||||
public let activeTitleColor: UIColor
|
||||
public let badgeIconName: String
|
||||
public let badgeValue: String
|
||||
|
||||
public init(
|
||||
inactiveColor: UIColor,
|
||||
activeColors: [UIColor],
|
||||
inactiveTitle: String,
|
||||
inactiveTitleColor: UIColor,
|
||||
activeTitle: String,
|
||||
activeValue: String,
|
||||
activeTitleColor: UIColor,
|
||||
badgeIconName: String,
|
||||
badgeValue: String
|
||||
) {
|
||||
self.inactiveColor = inactiveColor
|
||||
self.activeColors = activeColors
|
||||
self.inactiveTitle = inactiveTitle
|
||||
self.inactiveTitleColor = inactiveTitleColor
|
||||
self.activeTitle = activeTitle
|
||||
self.activeValue = activeValue
|
||||
self.activeTitleColor = activeTitleColor
|
||||
self.badgeIconName = badgeIconName
|
||||
self.badgeValue = badgeValue
|
||||
}
|
||||
|
||||
public static func ==(lhs: PremiumLimitDisplayComponent, rhs: PremiumLimitDisplayComponent) -> Bool {
|
||||
if lhs.inactiveColor != rhs.inactiveColor {
|
||||
return false
|
||||
}
|
||||
if lhs.activeColors != rhs.activeColors {
|
||||
return false
|
||||
}
|
||||
if lhs.inactiveTitle != rhs.inactiveTitle {
|
||||
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.badgeIconName != rhs.badgeIconName {
|
||||
return false
|
||||
}
|
||||
if lhs.badgeValue != rhs.badgeValue {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public static var body: Body {
|
||||
let inactiveTitle = Child(Text.self)
|
||||
let activeTitle = Child(Text.self)
|
||||
let activeValue = Child(Text.self)
|
||||
let animation = Child(PremiumLimitAnimationComponent.self)
|
||||
|
||||
return { context in
|
||||
let component = context.component
|
||||
|
||||
let height: CGFloat = 120.0
|
||||
let lineHeight: CGFloat = 30.0
|
||||
|
||||
let inactiveTitle = inactiveTitle.update(
|
||||
component: Text(
|
||||
text: component.inactiveTitle,
|
||||
font: Font.semibold(15.0),
|
||||
color: component.inactiveTitleColor
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let activeTitle = activeTitle.update(
|
||||
component: Text(
|
||||
text: component.activeTitle,
|
||||
font: Font.semibold(15.0),
|
||||
color: component.activeTitleColor
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let activeValue = activeValue.update(
|
||||
component: Text(
|
||||
text: component.activeValue,
|
||||
font: Font.semibold(15.0),
|
||||
color: component.activeTitleColor
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
let animation = animation.update(
|
||||
component: PremiumLimitAnimationComponent(
|
||||
iconName: component.badgeIconName,
|
||||
inactiveColor: component.inactiveColor,
|
||||
activeColors: component.activeColors,
|
||||
textColor: component.activeTitleColor
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width, height: height),
|
||||
transition: context.transition
|
||||
)
|
||||
|
||||
context.add(animation
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: height / 2.0))
|
||||
)
|
||||
|
||||
context.add(inactiveTitle
|
||||
.position(CGPoint(x: inactiveTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
|
||||
)
|
||||
|
||||
context.add(activeTitle
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 + 1.0 + activeTitle.size.width / 2.0 + 12.0, y: height - lineHeight / 2.0))
|
||||
)
|
||||
|
||||
context.add(activeValue
|
||||
.position(CGPoint(x: context.availableSize.width - activeValue.size.width / 2.0 - 12.0, y: height - lineHeight / 2.0))
|
||||
)
|
||||
|
||||
return CGSize(width: context.availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LimitSheetContent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
@ -78,13 +482,9 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let badgeBackground = Child(RoundedRectangle.self)
|
||||
let badgeIcon = Child(BundleIconComponent.self)
|
||||
let badgeText = Child(MultilineTextComponent.self)
|
||||
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let text = Child(MultilineTextComponent.self)
|
||||
|
||||
let limit = Child(PremiumLimitDisplayComponent.self)
|
||||
let button = Child(SolidRoundedButtonComponent.self)
|
||||
|
||||
return { context in
|
||||
@ -100,68 +500,40 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
let textSideInset: CGFloat = 24.0 + environment.safeInsets.left
|
||||
|
||||
let iconName: String
|
||||
let badgeString: String
|
||||
let badgeValue: String
|
||||
let string: String
|
||||
let premiumValue: String
|
||||
switch subject {
|
||||
case .folders:
|
||||
let limit = state.limits.maxFoldersCount
|
||||
let premiumLimit = state.premiumLimits.maxFoldersCount
|
||||
iconName = "Premium/Folder"
|
||||
badgeString = "\(limit)"
|
||||
badgeValue = "\(limit)"
|
||||
string = strings.Premium_MaxFoldersCountText("\(limit)", "\(premiumLimit)").string
|
||||
premiumValue = "\(premiumLimit)"
|
||||
case .chatsInFolder:
|
||||
let limit = state.limits.maxFolderChatsCount
|
||||
let premiumLimit = state.premiumLimits.maxFolderChatsCount
|
||||
iconName = "Premium/Chat"
|
||||
badgeString = "\(limit)"
|
||||
badgeValue = "\(limit)"
|
||||
string = strings.Premium_MaxChatsInFolderCountText("\(limit)", "\(premiumLimit)").string
|
||||
premiumValue = "\(premiumLimit)"
|
||||
case .pins:
|
||||
let limit = state.limits.maxPinnedChatCount
|
||||
let premiumLimit = state.premiumLimits.maxPinnedChatCount
|
||||
let limit = 4//state.limits.maxPinnedChatCount
|
||||
let premiumLimit = 6//state.premiumLimits.maxPinnedChatCount
|
||||
iconName = "Premium/Pin"
|
||||
badgeString = "\(limit)"
|
||||
badgeValue = "\(limit)"
|
||||
string = strings.Premium_MaxPinsText("\(limit)", "\(premiumLimit)").string
|
||||
premiumValue = "\(premiumLimit)"
|
||||
case .files:
|
||||
let limit = 2048 * 1024 * 1024 //state.limits.maxPinnedChatCount
|
||||
let premiumLimit = 4096 * 1024 * 1024 //state.premiumLimits.maxPinnedChatCount
|
||||
iconName = "Premium/File"
|
||||
badgeString = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
|
||||
badgeValue = dataSizeString(limit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
|
||||
string = strings.Premium_MaxFileSizeText(dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))).string
|
||||
premiumValue = dataSizeString(premiumLimit, formatting: DataSizeStringFormatting(strings: environment.strings, decimalSeparator: environment.dateTimeFormat.decimalSeparator))
|
||||
}
|
||||
|
||||
let badgeIcon = badgeIcon.update(
|
||||
component: BundleIconComponent(
|
||||
name: iconName,
|
||||
tintColor: .white
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let badgeText = badgeText.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: badgeString,
|
||||
font: Font.with(size: 24.0, design: .round, weight: .semibold, traits: []),
|
||||
textColor: .white,
|
||||
paragraphAlignment: .center
|
||||
)),
|
||||
horizontalAlignment: .center,
|
||||
maximumNumberOfLines: 1
|
||||
),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let badgeBackground = badgeBackground.update(
|
||||
component: RoundedRectangle(
|
||||
colors: [UIColor(rgb: 0xa34fcf), UIColor(rgb: 0xc8498a), UIColor(rgb: 0xff7a23)],
|
||||
cornerRadius: 23.5
|
||||
),
|
||||
availableSize: CGSize(width: badgeText.size.width + 67.0, height: 47.0),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let title = title.update(
|
||||
component: MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
@ -195,19 +567,45 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let limit = limit.update(
|
||||
component: PremiumLimitDisplayComponent(
|
||||
inactiveColor: UIColor(rgb: 0xE9E9EA),
|
||||
activeColors: [
|
||||
UIColor(rgb: 0x0077ff),
|
||||
UIColor(rgb: 0x6b93ff),
|
||||
UIColor(rgb: 0x8878ff),
|
||||
UIColor(rgb: 0xe46ace)
|
||||
],
|
||||
inactiveTitle: "Free",
|
||||
inactiveTitleColor: .black,
|
||||
activeTitle: "Premium",
|
||||
activeValue: premiumValue,
|
||||
activeTitleColor: .white,
|
||||
badgeIconName: iconName,
|
||||
badgeValue: badgeValue
|
||||
),
|
||||
availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
let button = button.update(
|
||||
component: SolidRoundedButtonComponent(
|
||||
title: strings.Premium_IncreaseLimit,
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: .black,
|
||||
backgroundColors: [UIColor(rgb: 0x407af0), UIColor(rgb: 0x9551e8), UIColor(rgb: 0xbf499a), UIColor(rgb: 0xf17b30)],
|
||||
backgroundColors: [
|
||||
UIColor(rgb: 0x0077ff),
|
||||
UIColor(rgb: 0x6b93ff),
|
||||
UIColor(rgb: 0x8878ff),
|
||||
UIColor(rgb: 0xe46ace)
|
||||
],
|
||||
foregroundColor: .white
|
||||
),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
gloss: true,
|
||||
iconName: "Premium/X2",
|
||||
iconPosition: .right,
|
||||
action: { [weak component] in
|
||||
@ -224,19 +622,8 @@ private final class LimitSheetContent: CombinedComponent {
|
||||
|
||||
let width = context.availableSize.width
|
||||
|
||||
let badgeFrame = CGRect(origin: CGPoint(x: floor((context.availableSize.width - badgeBackground.size.width) / 2.0), y: 33.0), size: badgeBackground.size)
|
||||
context.add(badgeBackground
|
||||
.position(CGPoint(x: badgeFrame.midX, y: badgeFrame.midY))
|
||||
)
|
||||
|
||||
let badgeIconFrame = CGRect(origin: CGPoint(x: badgeFrame.minX + 18.0, y: badgeFrame.minY + floor((badgeFrame.height - badgeIcon.size.height) / 2.0)), size: badgeIcon.size)
|
||||
context.add(badgeIcon
|
||||
.position(CGPoint(x: badgeIconFrame.midX, y: badgeIconFrame.midY))
|
||||
)
|
||||
|
||||
let badgeTextFrame = CGRect(origin: CGPoint(x: badgeFrame.maxX - badgeText.size.width - 15.0, y: badgeFrame.minY + floor((badgeFrame.height - badgeText.size.height) / 2.0)), size: badgeText.size)
|
||||
context.add(badgeText
|
||||
.position(CGPoint(x: badgeTextFrame.midX, y: badgeTextFrame.midY))
|
||||
context.add(limit
|
||||
.position(CGPoint(x: width / 2.0, y: limit.size.height / 2.0 + 44.0))
|
||||
)
|
||||
|
||||
context.add(title
|
||||
|
197
submodules/PremiumUI/Sources/RollingCountLabel.swift
Normal file
197
submodules/PremiumUI/Sources/RollingCountLabel.swift
Normal file
@ -0,0 +1,197 @@
|
||||
import UIKit
|
||||
|
||||
private extension UILabel {
|
||||
func textWidth() -> CGFloat {
|
||||
return UILabel.textWidth(label: self)
|
||||
}
|
||||
|
||||
class func textWidth(label: UILabel) -> CGFloat {
|
||||
return textWidth(label: label, text: label.text!)
|
||||
}
|
||||
|
||||
class func textWidth(label: UILabel, text: String) -> CGFloat {
|
||||
return textWidth(font: label.font, text: text)
|
||||
}
|
||||
|
||||
class func textWidth(font: UIFont, text: String) -> CGFloat {
|
||||
let myText = text as NSString
|
||||
|
||||
let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
||||
let labelSize = myText.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
|
||||
return ceil(labelSize.width)
|
||||
}
|
||||
}
|
||||
|
||||
open class RollingLabel: UILabel {
|
||||
private var fullText = ""
|
||||
|
||||
private var suffix: String = ""
|
||||
open var showSymbol = false
|
||||
private var scrollLayers: [CAScrollLayer] = []
|
||||
private var scrollLabels: [UILabel] = []
|
||||
private let duration = 1.12
|
||||
private let durationOffset = 0.2
|
||||
private let textsNotAnimated = [","]
|
||||
|
||||
public func text(num: Int) {
|
||||
self.configure(with: num)
|
||||
self.text = " "
|
||||
self.animate()
|
||||
}
|
||||
|
||||
public func setPrefix(prefix: String) {
|
||||
self.suffix = prefix
|
||||
}
|
||||
|
||||
private func configure(with number: Int) {
|
||||
fullText = String(number)
|
||||
|
||||
clean()
|
||||
setupSubviews()
|
||||
}
|
||||
|
||||
private func animate(ascending: Bool = true) {
|
||||
createAnimations(ascending: ascending)
|
||||
}
|
||||
|
||||
private func clean() {
|
||||
self.text = nil
|
||||
self.subviews.forEach { $0.removeFromSuperview() }
|
||||
self.layer.sublayers?.forEach { $0.removeFromSuperlayer() }
|
||||
scrollLayers.removeAll()
|
||||
scrollLabels.removeAll()
|
||||
}
|
||||
|
||||
private func setupSubviews() {
|
||||
let stringArray = fullText.map { String($0) }
|
||||
var x: CGFloat = 0
|
||||
let y: CGFloat = 0
|
||||
if self.textAlignment == .center {
|
||||
if showSymbol {
|
||||
self.text = "\(fullText) \(suffix)"
|
||||
} else {
|
||||
self.text = fullText
|
||||
}
|
||||
let w = UILabel.textWidth(font: self.font, text: self.text ?? "")
|
||||
self.text = "" // 초기화
|
||||
x = -(w / 2)
|
||||
} else if self.textAlignment == .right {
|
||||
if showSymbol {
|
||||
self.text = "\(fullText) \(suffix) "
|
||||
} else {
|
||||
self.text = fullText
|
||||
}
|
||||
let w = UILabel.textWidth(font: self.font, text: self.text ?? "")
|
||||
self.text = "" // 초기화
|
||||
x = -w
|
||||
}
|
||||
|
||||
if showSymbol {
|
||||
let wLabel = UILabel()
|
||||
wLabel.frame.origin = CGPoint(x: x, y: y)
|
||||
wLabel.textColor = textColor
|
||||
wLabel.font = font
|
||||
wLabel.text = "\(suffix) "
|
||||
wLabel.textAlignment = .center
|
||||
wLabel.sizeToFit()
|
||||
self.addSubview(wLabel)
|
||||
x += wLabel.bounds.width
|
||||
}
|
||||
|
||||
stringArray.enumerated().forEach { index, text in
|
||||
if textsNotAnimated.contains(text) {
|
||||
let label = UILabel()
|
||||
label.frame.origin = CGPoint(x: x, y: y)
|
||||
label.textColor = textColor
|
||||
label.font = font
|
||||
label.text = text
|
||||
label.textAlignment = .center
|
||||
label.sizeToFit()
|
||||
self.addSubview(label)
|
||||
|
||||
x += label.bounds.width
|
||||
} else {
|
||||
let label = UILabel()
|
||||
label.frame.origin = CGPoint(x: x, y: y)
|
||||
label.textColor = textColor
|
||||
label.font = font
|
||||
label.text = "0"
|
||||
label.textAlignment = .center
|
||||
label.sizeToFit()
|
||||
createScrollLayer(to: label, text: text)
|
||||
|
||||
x += label.bounds.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createScrollLayer(to label: UILabel, text: String) {
|
||||
let scrollLayer = CAScrollLayer()
|
||||
scrollLayer.frame = label.frame
|
||||
scrollLayers.append(scrollLayer)
|
||||
self.layer.addSublayer(scrollLayer)
|
||||
|
||||
createContentForLayer(scrollLayer: scrollLayer, text: text)
|
||||
}
|
||||
|
||||
private func createContentForLayer(scrollLayer: CAScrollLayer, text: String) {
|
||||
var textsForScroll: [String] = []
|
||||
|
||||
let max: Int
|
||||
var found = false
|
||||
if let val = Int(text) {
|
||||
max = val
|
||||
found = true
|
||||
} else {
|
||||
max = 9
|
||||
}
|
||||
|
||||
for i in 0...max {
|
||||
let str = String(i)
|
||||
textsForScroll.append(str)
|
||||
}
|
||||
if !found {
|
||||
textsForScroll.append(text)
|
||||
}
|
||||
|
||||
var height: CGFloat = 0
|
||||
for text in textsForScroll {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.textColor = textColor
|
||||
label.font = font
|
||||
label.textAlignment = .center
|
||||
label.frame = CGRect(x: 0, y: height, width: scrollLayer.frame.width, height: scrollLayer.frame.height)
|
||||
scrollLayer.addSublayer(label.layer)
|
||||
scrollLabels.append(label)
|
||||
|
||||
height = label.frame.maxY
|
||||
}
|
||||
}
|
||||
|
||||
private func createAnimations(ascending: Bool) {
|
||||
var offset: CFTimeInterval = 0.0
|
||||
|
||||
for scrollLayer in scrollLayers {
|
||||
let maxY = scrollLayer.sublayers?.last?.frame.origin.y ?? 0.0
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "sublayerTransform.translation.y")
|
||||
animation.duration = duration + offset
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||
|
||||
if ascending {
|
||||
animation.fromValue = maxY
|
||||
animation.toValue = 0
|
||||
} else {
|
||||
animation.fromValue = 0
|
||||
animation.toValue = maxY
|
||||
}
|
||||
|
||||
scrollLayer.scrollMode = .vertically
|
||||
scrollLayer.add(animation, forKey: nil)
|
||||
scrollLayer.scroll(to: CGPoint(x: 0, y: maxY))
|
||||
|
||||
offset += self.durationOffset
|
||||
}
|
||||
}
|
||||
}
|
@ -645,9 +645,7 @@ public final class SolidRoundedButtonView: UIView {
|
||||
let previousValue = buttonBackgroundAnimationView.center.x
|
||||
var newValue: CGFloat = offset
|
||||
if offset - previousValue < buttonBackgroundAnimationView.frame.width * 0.25 {
|
||||
newValue -= CGFloat.random(in: buttonBackgroundAnimationView.frame.width * 0.3 ..< buttonBackgroundAnimationView.frame.width * 0.4)
|
||||
} else {
|
||||
// newValue -= CGFloat.random(in: 0.0 ..< buttonBackgroundAnimationView.frame.width * 0.1)
|
||||
newValue -= buttonBackgroundAnimationView.frame.width * 0.35
|
||||
}
|
||||
buttonBackgroundAnimationView.center = CGPoint(x: newValue, y: buttonBackgroundAnimationView.bounds.size.height / 2.0)
|
||||
|
||||
@ -794,7 +792,10 @@ public final class SolidRoundedButtonView: UIView {
|
||||
}
|
||||
|
||||
if let buttonBackgroundAnimationView = self.buttonBackgroundAnimationView {
|
||||
transition.updateFrame(view: buttonBackgroundAnimationView, frame: CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height)))
|
||||
if buttonBackgroundAnimationView.layer.animation(forKey: "movement") == nil {
|
||||
buttonBackgroundAnimationView.center = CGPoint(x: buttonSize.width * 2.4 / 2.0, y: buttonSize.height / 2.0)
|
||||
}
|
||||
buttonBackgroundAnimationView.bounds = CGRect(origin: CGPoint(), size: CGSize(width: buttonSize.width * 2.4, height: buttonSize.height))
|
||||
self.setupGradientAnimations()
|
||||
}
|
||||
|
||||
|
@ -128,6 +128,37 @@ public extension TelegramEngine {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func subscribe<
|
||||
T0: TelegramEngineDataItem,
|
||||
T1: TelegramEngineDataItem,
|
||||
T2: TelegramEngineDataItem
|
||||
>(
|
||||
_ t0: T0,
|
||||
_ t1: T1,
|
||||
_ t2: T2
|
||||
) -> Signal<
|
||||
(
|
||||
T0.Result,
|
||||
T1.Result,
|
||||
T2.Result
|
||||
),
|
||||
NoError> {
|
||||
return self._subscribe(items: [
|
||||
t0 as! AnyPostboxViewDataItem,
|
||||
t1 as! AnyPostboxViewDataItem,
|
||||
t2 as! AnyPostboxViewDataItem
|
||||
])
|
||||
|> map { results -> (T0.Result, T1.Result, T2.Result) in
|
||||
return (
|
||||
results[0] as! T0.Result,
|
||||
results[1] as! T1.Result,
|
||||
results[2] as! T2.Result
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func get<
|
||||
T0: TelegramEngineDataItem,
|
||||
T1: TelegramEngineDataItem
|
||||
@ -142,5 +173,23 @@ public extension TelegramEngine {
|
||||
NoError> {
|
||||
return self.subscribe(t0, t1) |> take(1)
|
||||
}
|
||||
|
||||
public func get<
|
||||
T0: TelegramEngineDataItem,
|
||||
T1: TelegramEngineDataItem,
|
||||
T2: TelegramEngineDataItem
|
||||
>(
|
||||
_ t0: T0,
|
||||
_ t1: T1,
|
||||
_ t2: T2
|
||||
) -> Signal<
|
||||
(
|
||||
T0.Result,
|
||||
T1.Result,
|
||||
T2.Result
|
||||
),
|
||||
NoError> {
|
||||
return self.subscribe(t0, t1, t2) |> take(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Tmp.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Tmp2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.8 KiB |
@ -131,11 +131,11 @@ public final class TelegramRootController: NavigationController {
|
||||
self.rootTabController = tabBarController
|
||||
self.pushViewController(tabBarController, animated: false)
|
||||
|
||||
Queue.mainQueue().after(1.0) {
|
||||
// let screen = PremiumLimitScreen(context: self.context, subject: .pins, action: {})
|
||||
let screen = PremiumIntroScreen(context: self.context, action: {})
|
||||
self.chatListController?.push(screen)
|
||||
}
|
||||
// Queue.mainQueue().after(1.0) {
|
||||
//// let screen = PremiumLimitScreen(context: self.context, subject: .pins, action: {})
|
||||
// let screen = PremiumIntroScreen(context: self.context, action: {})
|
||||
// self.chatListController?.push(screen)
|
||||
// }
|
||||
}
|
||||
|
||||
public func updateRootControllers(showCallsTab: Bool) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user