Merge branch 'beta'

This commit is contained in:
Isaac 2025-08-05 17:44:17 +02:00
commit e16f2ff47f
16 changed files with 730 additions and 270 deletions

View File

@ -4613,44 +4613,46 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1)) let _ = (combineLatest(self.chatListDisplayNode.mainContainerNode.currentItemNode.contentsReady |> take(1), self.context.account.postbox.tailChatListView(groupId: .root, count: 16, summaryComponents: ChatListEntrySummaryComponents(components: [:])) |> take(1))
|> deliverOnMainQueue).startStandalone(next: { [weak self] _, chatListView in |> deliverOnMainQueue).startStandalone(next: { [weak self] _, chatListView in
guard let strongSelf = self else { Task { @MainActor in
return guard let strongSelf = self else {
} return
/*if let scrollToTop = strongSelf.scrollToTop {
scrollToTop()
}*/
let tabsIsEmpty: Bool
if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData {
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
} else {
tabsIsEmpty = true
}
let _ = tabsIsEmpty
//TODO:swap tabs
let displaySearchFilters = true
if let filterContainerNodeAndActivate = strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) {
let (filterContainerNode, activate) = filterContainerNodeAndActivate
if displaySearchFilters {
let searchTabsNode = SparseNode()
strongSelf.searchTabsNode = searchTabsNode
searchTabsNode.addSubnode(filterContainerNode)
} }
activate(filter != .downloads) /*if let scrollToTop = strongSelf.scrollToTop {
scrollToTop()
}*/
if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode { let tabsIsEmpty: Bool
searchContentNode.search(filter: filter, query: query) if let (resolvedItems, displayTabsAtBottom, _) = strongSelf.tabContainerData {
tabsIsEmpty = resolvedItems.count <= 1 || displayTabsAtBottom
} else {
tabsIsEmpty = true
} }
let _ = tabsIsEmpty
//TODO:swap tabs
let displaySearchFilters = true
if let filterContainerNodeAndActivate = await strongSelf.chatListDisplayNode.activateSearch(placeholderNode: searchContentNode.placeholderNode, displaySearchFilters: displaySearchFilters, hasDownloads: strongSelf.hasDownloads, initialFilter: filter, navigationController: strongSelf.navigationController as? NavigationController) {
let (filterContainerNode, activate) = filterContainerNodeAndActivate
if displaySearchFilters {
let searchTabsNode = SparseNode()
strongSelf.searchTabsNode = searchTabsNode
searchTabsNode.addSubnode(filterContainerNode)
}
activate(filter != .downloads)
if let searchContentNode = strongSelf.chatListDisplayNode.searchDisplayController?.contentNode as? ChatListSearchContainerNode {
searchContentNode.search(filter: filter, query: query)
}
}
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
strongSelf.setDisplayNavigationBar(false, transition: transition)
(strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring))
} }
let transition: ContainedViewLayoutTransition = .animated(duration: 0.4, curve: .spring)
strongSelf.setDisplayNavigationBar(false, transition: transition)
(strongSelf.parent as? TabBarController)?.updateIsTabBarHidden(true, transition: .animated(duration: 0.4, curve: .spring))
}) })
self.isSearchActive = true self.isSearchActive = true

View File

@ -20,6 +20,7 @@ import ComponentFlow
import ChatFolderLinkPreviewScreen import ChatFolderLinkPreviewScreen
import ChatListHeaderComponent import ChatListHeaderComponent
import StoryPeerListComponent import StoryPeerListComponent
import TelegramNotices
public enum ChatListContainerNodeFilter: Equatable { public enum ChatListContainerNodeFilter: Equatable {
case all case all
@ -1649,7 +1650,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
} }
} }
func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) -> (ASDisplayNode, (Bool) -> Void)? { @MainActor
func activateSearch(placeholderNode: SearchBarPlaceholderNode, displaySearchFilters: Bool, hasDownloads: Bool, initialFilter: ChatListSearchFilter, navigationController: NavigationController?) async -> (ASDisplayNode, (Bool) -> Void)? {
guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else { guard let (containerLayout, _, _, cleanNavigationBarHeight, _) = self.containerLayout, self.searchDisplayController == nil else {
return nil return nil
} }
@ -1688,6 +1690,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
self?.controller?.openAdInfo(node: node, adPeer: adPeer) self?.controller?.openAdInfo(node: node, adPeer: adPeer)
} }
let searchTips = await ApplicationSpecificNotice.getGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).get()
contentNode.displayGlobalPostsNewBadge = searchTips < 3
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: contentNode, cancel: { [weak self] in self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, mode: .list, contentNode: contentNode, cancel: { [weak self] in
if let requestDeactivateSearch = self?.requestDeactivateSearch { if let requestDeactivateSearch = self?.requestDeactivateSearch {
requestDeactivateSearch() requestDeactivateSearch()

View File

@ -131,6 +131,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
private var forumPeer: EnginePeer? private var forumPeer: EnginePeer?
private var hasPublicPostsTab = false private var hasPublicPostsTab = false
private var showPublicPostsTab = false private var showPublicPostsTab = false
public var displayGlobalPostsNewBadge = false
private var shareStatusDisposable: MetaDisposable? private var shareStatusDisposable: MetaDisposable?
@ -715,7 +716,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter) filters = defaultAvailableSearchPanes(isForum: isForum, hasDownloads: !isForum && self.hasDownloads, hasPublicPosts: self.showPublicPostsTab).map(\.filter)
} }
self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: true, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition) self.filterContainerNode.update(size: CGSize(width: layout.size.width - 40.0, height: 38.0), sideInset: layout.safeInsets.left - 20.0, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: self.displayGlobalPostsNewBadge, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: transition)
} }
} }
@ -786,7 +787,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
} }
let overflowInset: CGFloat = 20.0 let overflowInset: CGFloat = 20.0
self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: true, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring)) self.filterContainerNode.update(size: CGSize(width: layout.size.width - overflowInset * 2.0, height: 38.0), sideInset: layout.safeInsets.left - overflowInset, filters: filters.map { .filter($0) }, displayGlobalPostsNewBadge: self.displayGlobalPostsNewBadge, selectedFilter: self.selectedFilter?.id, transitionFraction: self.transitionFraction, presentationData: self.presentationData, transition: .animated(duration: 0.4, curve: .spring))
if isFirstTime { if isFirstTime {
self.filterContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.filterContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)

View File

@ -2061,7 +2061,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
} }
var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false)) var defaultFoundRemoteMessagesSignal: Signal<([FoundRemoteMessages], Bool), NoError> = .single(([FoundRemoteMessages(messages: [], readCounters: [:], threadsData: [:], totalCount: 0)], false))
if key == .globalPosts { if key == .globalPosts, let data = context.currentAppConfiguration.with({ $0 }).data, let value = data["ios_load_empty_global_posts"] as? Double, value != 0.0 {
let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50) let searchSignal = context.engine.messages.searchMessages(location: .general(scope: .globalPosts(allowPaidStars: nil), tags: nil, minDate: nil, maxDate: nil), query: "", state: nil, limit: 50)
|> map { resultData -> ChatListSearchMessagesResult in |> map { resultData -> ChatListSearchMessagesResult in
let (result, updatedState) = resultData let (result, updatedState) = resultData

View File

@ -9,6 +9,7 @@ import AccountContext
import ContextUI import ContextUI
import AnimationCache import AnimationCache
import MultiAnimationRenderer import MultiAnimationRenderer
import TelegramNotices
protocol ChatListSearchPaneNode: ASDisplayNode { protocol ChatListSearchPaneNode: ASDisplayNode {
var isReady: Signal<Bool, NoError> { get } var isReady: Signal<Bool, NoError> { get }
@ -238,6 +239,11 @@ final class ChatListSearchPaneContainerNode: ASDisplayNode, ASGestureRecognizerD
} }
return return
} }
if key == .globalPosts {
let _ = ApplicationSpecificNotice.incrementGlobalPostsSearch(accountManager: self.context.sharedContext.accountManager).startStandalone()
}
#if DEBUG #if DEBUG
#else #else
self.isAdjacentLoadingEnabled = true self.isAdjacentLoadingEnabled = true

View File

@ -4,6 +4,23 @@ import Display
import ComponentFlow import ComponentFlow
import TelegramPresentationData import TelegramPresentationData
extension ComponentTransition {
func animateBlur(layer: CALayer, from: CGFloat, to: CGFloat, delay: Double = 0.0, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
if let blurFilter = CALayer.blur() {
blurFilter.setValue(to as NSNumber, forKey: "inputRadius")
layer.filters = [blurFilter]
layer.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, delay: delay, removeOnCompletion: removeOnCompletion, completion: { [weak layer] _ in
guard let layer else {
return
}
if to == 0.0 && removeOnCompletion {
layer.filters = nil
}
})
}
}
}
public final class AnimatedTextComponent: Component { public final class AnimatedTextComponent: Component {
public struct Item: Equatable { public struct Item: Equatable {
public enum Content: Equatable { public enum Content: Equatable {
@ -27,19 +44,25 @@ public final class AnimatedTextComponent: Component {
public let items: [Item] public let items: [Item]
public let noDelay: Bool public let noDelay: Bool
public let animateScale: Bool public let animateScale: Bool
public let preferredDirectionIsDown: Bool
public let blur: Bool
public init( public init(
font: UIFont, font: UIFont,
color: UIColor, color: UIColor,
items: [Item], items: [Item],
noDelay: Bool = false, noDelay: Bool = false,
animateScale: Bool = true animateScale: Bool = true,
preferredDirectionIsDown: Bool = false,
blur: Bool = false
) { ) {
self.font = font self.font = font
self.color = color self.color = color
self.items = items self.items = items
self.noDelay = noDelay self.noDelay = noDelay
self.animateScale = animateScale self.animateScale = animateScale
self.preferredDirectionIsDown = preferredDirectionIsDown
self.blur = blur
} }
public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool { public static func ==(lhs: AnimatedTextComponent, rhs: AnimatedTextComponent) -> Bool {
@ -58,6 +81,12 @@ public final class AnimatedTextComponent: Component {
if lhs.animateScale != rhs.animateScale { if lhs.animateScale != rhs.animateScale {
return false return false
} }
if lhs.preferredDirectionIsDown != rhs.preferredDirectionIsDown {
return false
}
if lhs.blur != rhs.blur {
return false
}
return true return true
} }
@ -88,8 +117,14 @@ public final class AnimatedTextComponent: Component {
var size = CGSize() var size = CGSize()
let delayNorm: CGFloat = 0.002 let delayNorm: CGFloat = 0.002
var offsetNorm: CGFloat = 0.4
let transitionBlurRadius: CGFloat = 6.0
var firstDelayWidth: CGFloat? var firstDelayWidth: CGFloat?
if component.preferredDirectionIsDown {
firstDelayWidth = 0.0
offsetNorm = 0.8
}
var validKeys: [CharacterKey] = [] var validKeys: [CharacterKey] = []
for item in component.items { for item in component.items {
@ -119,6 +154,57 @@ public final class AnimatedTextComponent: Component {
index += 1 index += 1
validKeys.append(characterKey) validKeys.append(characterKey)
}
}
var outLastDelayWidth: CGFloat?
var outFirstDelayWidth: CGFloat?
if component.preferredDirectionIsDown {
for (key, characterView) in self.characters {
if !validKeys.contains(key), let characterView = characterView.view {
if let outFirstDelayWidthValue = outFirstDelayWidth {
outFirstDelayWidth = max(outFirstDelayWidthValue, characterView.frame.center.x)
} else {
outFirstDelayWidth = characterView.frame.center.x
}
if let outLastDelayWidthValue = outLastDelayWidth {
outLastDelayWidth = min(outLastDelayWidthValue, characterView.frame.center.x)
} else {
outLastDelayWidth = characterView.frame.center.x
}
}
}
}
if outLastDelayWidth != nil {
firstDelayWidth = outLastDelayWidth
}
for item in component.items {
var itemText: [String] = []
switch item.content {
case let .text(text):
if item.isUnbreakable {
itemText = [text]
} else {
itemText = text.map(String.init)
}
case let .number(value, minDigits):
var valueText: String = "\(value)"
while valueText.count < minDigits {
valueText.insert("0", at: valueText.startIndex)
}
if item.isUnbreakable {
itemText = [valueText]
} else {
itemText = valueText.map(String.init)
}
}
var index = 0
for character in itemText {
let characterKey = CharacterKey(itemId: item.id, index: index, value: character)
index += 1
var characterTransition = transition var characterTransition = transition
let characterView: ComponentView<Empty> let characterView: ComponentView<Empty>
@ -155,7 +241,11 @@ public final class AnimatedTextComponent: Component {
} else { } else {
var delayWidth: Double = 0.0 var delayWidth: Double = 0.0
if let firstDelayWidth { if let firstDelayWidth {
delayWidth = size.width - firstDelayWidth if characterFrame.midX > characterComponentView.frame.midX {
delayWidth = 0.0
} else {
delayWidth = abs(size.width - firstDelayWidth)
}
} else { } else {
firstDelayWidth = size.width firstDelayWidth = size.width
} }
@ -170,7 +260,7 @@ public final class AnimatedTextComponent: Component {
if animateIn, !transition.animation.isImmediate { if animateIn, !transition.animation.isImmediate {
var delayWidth: Double = 0.0 var delayWidth: Double = 0.0
if !component.noDelay { if !component.noDelay || component.preferredDirectionIsDown {
if let firstDelayWidth { if let firstDelayWidth {
delayWidth = size.width - firstDelayWidth delayWidth = size.width - firstDelayWidth
} else { } else {
@ -181,7 +271,10 @@ public final class AnimatedTextComponent: Component {
if component.animateScale { if component.animateScale {
characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring) characterComponentView.layer.animateScale(from: 0.001, to: 1.0, duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring)
} }
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * 0.5), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true) if component.blur {
ComponentTransition.easeInOut(duration: 0.2).animateBlur(layer: characterComponentView.layer, from: transitionBlurRadius, to: 0.0, delay: delayNorm * delayWidth)
}
characterComponentView.layer.animatePosition(from: CGPoint(x: 0.0, y: characterSize.height * offsetNorm), to: CGPoint(), duration: 0.4, delay: delayNorm * delayWidth, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth) characterComponentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18, delay: delayNorm * delayWidth)
} }
} }
@ -194,8 +287,6 @@ public final class AnimatedTextComponent: Component {
let outScaleTransition: ComponentTransition = .spring(duration: 0.4) let outScaleTransition: ComponentTransition = .spring(duration: 0.4)
let outAlphaTransition: ComponentTransition = .easeInOut(duration: 0.18) let outAlphaTransition: ComponentTransition = .easeInOut(duration: 0.18)
var outFirstDelayWidth: CGFloat?
var removedKeys: [CharacterKey] = [] var removedKeys: [CharacterKey] = []
for (key, characterView) in self.characters { for (key, characterView) in self.characters {
if !validKeys.contains(key) { if !validKeys.contains(key) {
@ -205,18 +296,28 @@ public final class AnimatedTextComponent: Component {
if !transition.animation.isImmediate { if !transition.animation.isImmediate {
var delayWidth: Double = 0.0 var delayWidth: Double = 0.0
if let outFirstDelayWidth { if let outFirstDelayWidth {
delayWidth = characterComponentView.frame.minX - outFirstDelayWidth delayWidth = abs(characterComponentView.frame.midX - outFirstDelayWidth)
} else { } else {
outFirstDelayWidth = characterComponentView.frame.minX outFirstDelayWidth = characterComponentView.frame.midX
} }
delayWidth = max(0.0, delayWidth)
if component.animateScale { if component.animateScale {
outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth) outScaleTransition.setScale(view: characterComponentView, scale: 0.01, delay: delayNorm * delayWidth)
} }
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: characterComponentView.center.y - characterComponentView.bounds.height * 0.4), delay: delayNorm * delayWidth) let targetY: CGFloat
if component.preferredDirectionIsDown {
targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm
} else {
targetY = characterComponentView.center.y - characterComponentView.bounds.height * offsetNorm
}
outScaleTransition.setPosition(view: characterComponentView, position: CGPoint(x: characterComponentView.center.x, y: targetY), delay: delayNorm * delayWidth)
outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in outAlphaTransition.setAlpha(view: characterComponentView, alpha: 0.0, delay: delayNorm * delayWidth, completion: { [weak characterComponentView] _ in
characterComponentView?.removeFromSuperview() characterComponentView?.removeFromSuperview()
}) })
if component.blur {
outAlphaTransition.animateBlur(layer: characterComponentView.layer, from: 0.0, to: transitionBlurRadius, delay: delayNorm * delayWidth, removeOnCompletion: false)
}
} else { } else {
characterComponentView.removeFromSuperview() characterComponentView.removeFromSuperview()
} }

View File

@ -338,6 +338,10 @@ public final class PeerInfoRatingComponent: Component {
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
if level < 0 {
return
}
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_outer", withExtension: "svg"), let data = try? Data(contentsOf: url) { if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_outer", withExtension: "svg"), let data = try? Data(contentsOf: url) {
if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.borderColor) { if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.borderColor) {
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)
@ -360,6 +364,13 @@ public final class PeerInfoRatingComponent: Component {
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
if level < 0 {
if let image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/InlineRatingWarning"), color: component.backgroundColor) {
image.draw(in: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - image.size.width) * 0.5), y: floorToScreenPixels((size.height - image.size.height) * 0.5)), size: image.size))
}
return
}
if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_inner", withExtension: "svg"), let data = try? Data(contentsOf: url) { if let url = Bundle.main.url(forResource: "profile_level\(levelIndex)_inner", withExtension: "svg"), let data = try? Data(contentsOf: url) {
if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.backgroundColor) { if let image = generateTintedImage(image: drawSvgImage(data, size, nil, nil, 0.0, false), color: component.backgroundColor) {
image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0) image.draw(in: CGRect(origin: CGPoint(x: 0.0, y: backgroundOffsetsY[levelIndex] ?? 0.0), size: size), blendMode: .normal, alpha: 1.0)

View File

@ -781,6 +781,13 @@ final class PeerInfoHeaderNode: ASDisplayNode {
self.avatarClippingNode.clipsToBounds = true self.avatarClippingNode.clipsToBounds = true
} }
let accentRatingBackgroundColor: UIColor
if let currentStarRating = self.currentStarRating, currentStarRating.level < 0 {
accentRatingBackgroundColor = UIColor(rgb: 0xFF3B30)
} else {
accentRatingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor
}
let ratingBackgroundColor: UIColor let ratingBackgroundColor: UIColor
let ratingBorderColor: UIColor let ratingBorderColor: UIColor
let ratingForegroundColor: UIColor let ratingForegroundColor: UIColor
@ -796,7 +803,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
headerButtonBackgroundColor = collapsedHeaderButtonBackgroundColor headerButtonBackgroundColor = collapsedHeaderButtonBackgroundColor
ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor ratingBackgroundColor = accentRatingBackgroundColor
ratingBorderColor = .clear ratingBorderColor = .clear
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
} else if self.isAvatarExpanded { } else if self.isAvatarExpanded {
@ -833,7 +840,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, patternColorValue, _) = status.content { if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, patternColorValue, _) = status.content {
let _ = innerColor let _ = innerColor
ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction) ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(accentRatingBackgroundColor, alpha: effectiveTransitionFraction)
let innerColor = UIColor(rgb: UInt32(bitPattern: innerColor)) let innerColor = UIColor(rgb: UInt32(bitPattern: innerColor))
let outerColor = UIColor(rgb: UInt32(bitPattern: outerColor)) let outerColor = UIColor(rgb: UInt32(bitPattern: outerColor))
@ -855,29 +862,10 @@ final class PeerInfoHeaderNode: ASDisplayNode {
ratingBorderColor = patternColor.withAlphaComponent(0.1).blendOver(background: backgroundColor).mixedWith(.clear, alpha: effectiveTransitionFraction) ratingBorderColor = patternColor.withAlphaComponent(0.1).blendOver(background: backgroundColor).mixedWith(.clear, alpha: effectiveTransitionFraction)
ratingForegroundColor = ratingBorderColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction) ratingForegroundColor = ratingBorderColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction)
} else { } else {
ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor ratingBackgroundColor = accentRatingBackgroundColor
ratingBorderColor = UIColor.clear ratingBorderColor = UIColor.clear
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
} }
/*if let status = peer?.emojiStatus, case let .starGift(_, _, _, _, _, innerColor, outerColor, _, _) = status.content {
let _ = outerColor
let mainColor = UIColor(rgb: UInt32(bitPattern: innerColor))
ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction)
ratingForegroundColor = mainColor.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 0.9).mixedWith(UIColor.clear, alpha: effectiveTransitionFraction)
ratingBorderColor = ratingForegroundColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction)
} else if let profileColor = peer?.profileColor {
let backgroundColors = self.context.peerNameColors.getProfile(profileColor, dark: presentationData.theme.overallDarkAppearance)
ratingBackgroundColor = UIColor(white: 1.0, alpha: 1.0).mixedWith(presentationData.theme.list.itemCheckColors.fillColor, alpha: effectiveTransitionFraction)
ratingForegroundColor = backgroundColors.main.withMultiplied(hue: 1.0, saturation: 1.1, brightness: 0.9).mixedWith(UIColor.clear, alpha: effectiveTransitionFraction)
ratingBorderColor = ratingForegroundColor.mixedWith(presentationData.theme.list.itemCheckColors.foregroundColor, alpha: effectiveTransitionFraction)
} else {
ratingBackgroundColor = presentationData.theme.list.itemCheckColors.fillColor
ratingBorderColor = UIColor.clear
ratingForegroundColor = presentationData.theme.list.itemCheckColors.foregroundColor
}*/
} }
do { do {
@ -1986,20 +1974,30 @@ final class PeerInfoHeaderNode: ASDisplayNode {
if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating { if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating {
self.currentStarRating = starRating self.currentStarRating = starRating
self.currentPendingStarRating = cachedData.pendingStarRating self.currentPendingStarRating = cachedData.pendingStarRating
#if DEBUG
if let _ = starRating.nextLevelStars {
self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level, currentLevelStars: starRating.currentLevelStars, stars: starRating.stars + 234, nextLevelStars: starRating.nextLevelStars), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level + 1, currentLevelStars: starRating.nextLevelStars!, stars: starRating.nextLevelStars! + starRating.nextLevelStars! / 2 + starRating.nextLevelStars! / 4, nextLevelStars: starRating.nextLevelStars! * 2), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
}
#endif
} else { } else {
self.currentStarRating = nil self.currentStarRating = nil
self.currentPendingStarRating = nil self.currentPendingStarRating = nil
} }
if let cachedData = cachedData as? CachedUserData, let starRating = cachedData.starRating { #if DEBUG
//if "".isEmpty { if "".isEmpty {
let starRating: TelegramStarRating
if self.context.account.peerId.id._internalGetInt64Value() == 654152421 {
starRating = TelegramStarRating(level: -1, currentLevelStars: -1, stars: -100, nextLevelStars: 0)
} else {
starRating = TelegramStarRating(level: 2, currentLevelStars: 1000, stars: 2000, nextLevelStars: 3000)
}
self.currentStarRating = starRating
if let _ = starRating.nextLevelStars {
//self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level, currentLevelStars: starRating.currentLevelStars, stars: starRating.stars + 234, nextLevelStars: starRating.nextLevelStars), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
self.currentPendingStarRating = TelegramStarPendingRating(rating: TelegramStarRating(level: starRating.level + 2, currentLevelStars: starRating.nextLevelStars!, stars: max(500, starRating.nextLevelStars! + starRating.nextLevelStars! / 2 - starRating.nextLevelStars! / 4), nextLevelStars: max(1000, starRating.nextLevelStars! * 2)), timestamp: Int32(Date().timeIntervalSince1970) + 60 * 60 * 24 * 3)
}
}
#endif
if let starRating = self.currentStarRating {
let subtitleRating: ComponentView<Empty> let subtitleRating: ComponentView<Empty>
var subtitleRatingTransition = ComponentTransition(transition) var subtitleRatingTransition = ComponentTransition(transition)
if let current = self.subtitleRating { if let current = self.subtitleRating {
@ -2018,7 +2016,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
foregroundColor: ratingForegroundColor, foregroundColor: ratingForegroundColor,
level: Int(starRating.level), level: Int(starRating.level),
action: { [weak self] in action: { [weak self] in
guard let self, let peer, let currentStarRating = self.currentStarRating else { guard let self, let peer = self.peer, let currentStarRating = self.currentStarRating else {
return return
} }
self.controller?.push(ProfileLevelInfoScreen( self.controller?.push(ProfileLevelInfoScreen(

View File

@ -17,6 +17,7 @@ import PlainButtonComponent
import Markdown import Markdown
import PremiumUI import PremiumUI
import LottieComponent import LottieComponent
import AnimatedTextComponent
private final class ProfileLevelInfoScreenComponent: Component { private final class ProfileLevelInfoScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -376,10 +377,38 @@ private final class ProfileLevelInfoScreenComponent: Component {
descriptionTextString = environment.strings.ProfileLevelInfo_OtherDescription(component.peer.compactDisplayTitle).string descriptionTextString = environment.strings.ProfileLevelInfo_OtherDescription(component.peer.compactDisplayTitle).string
} }
//TODO:localize
var titleItems: [AnimatedTextComponent.Item] = []
if self.isPreviewingPendingRating {
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(0),
isUnbreakable: false,
content: .text("Future ")
))
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(1),
isUnbreakable: true,
content: .text("Rating")
))
} else {
titleItems.append(AnimatedTextComponent.Item(
id: AnyHashable(1),
isUnbreakable: true,
content: .text("Rating")
))
}
let _ = titleString
let titleSize = self.title.update( let titleSize = self.title.update(
transition: .immediate, transition: transition,
component: AnyComponent(MultilineTextComponent( component: AnyComponent(AnimatedTextComponent(
text: .plain(NSAttributedString(string: titleString, font: Font.semibold(17.0), textColor: environment.theme.list.itemPrimaryTextColor)) font: Font.semibold(17.0),
color: environment.theme.list.itemPrimaryTextColor,
items: titleItems,
noDelay: true,
animateScale: false,
preferredDirectionIsDown: true,
blur: true
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0) containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
@ -389,7 +418,7 @@ private final class ProfileLevelInfoScreenComponent: Component {
if titleView.superview == nil { if titleView.superview == nil {
self.navigationBarContainer.addSubview(titleView) self.navigationBarContainer.addSubview(titleView)
} }
titleView.frame = titleFrame transition.setFrame(view: titleView, frame: titleFrame)
} }
contentHeight += 56.0 contentHeight += 56.0
@ -433,47 +462,25 @@ private final class ProfileLevelInfoScreenComponent: Component {
if let nextLevelStars = component.starRating.nextLevelStars { if let nextLevelStars = component.starRating.nextLevelStars {
badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))" badgeTextSuffix = " / \(starCountString(Int64(nextLevelStars), decimalSeparator: "."))"
} }
if let nextLevelStars = component.starRating.nextLevelStars { if component.starRating.stars < 0 {
levelFraction = 0.5
} else if let nextLevelStars = component.starRating.nextLevelStars {
levelFraction = Double(component.starRating.stars - component.starRating.currentLevelStars) / Double(nextLevelStars - component.starRating.currentLevelStars) levelFraction = Double(component.starRating.stars - component.starRating.currentLevelStars) / Double(nextLevelStars - component.starRating.currentLevelStars)
} else if component.starRating.stars > 0 {
levelFraction = 1.0
} else { } else {
levelFraction = 0.0 levelFraction = 1.0
} }
} }
levelFraction = max(0.0, levelFraction) levelFraction = max(0.0, levelFraction)
/*let levelInfoSize = self.levelInfo.update( //TODO:localize
transition: .immediate,
component: AnyComponent(PremiumLimitDisplayComponent(
inactiveColor: environment.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5),
activeColors: gradientColors,
inactiveTitle: environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)),
inactiveValue: "",
inactiveTitleColor: environment.theme.list.itemPrimaryTextColor,
activeTitle: "",
activeValue: nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
activeTitleColor: .white,
badgeIconName: "Peer Info/ProfileLevelProgressIcon",
badgeText: badgeText,
badgeTextSuffix: badgeTextSuffix,
badgePosition: levelFraction,
badgeGraphPosition: levelFraction,
invertProgress: true,
isPremiumDisabled: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 200.0)
)*/
let _ = levelFraction
let levelInfoSize = self.levelInfo.update( let levelInfoSize = self.levelInfo.update(
transition: isChangingPreview ? ComponentTransition.immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)) : .immediate, transition: isChangingPreview ? ComponentTransition.immediate.withUserData(ProfileLevelRatingBarComponent.TransitionHint(animate: true)) : .immediate,
component: AnyComponent(ProfileLevelRatingBarComponent( component: AnyComponent(ProfileLevelRatingBarComponent(
theme: environment.theme, theme: environment.theme,
value: levelFraction, value: levelFraction,
leftLabel: environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)), leftLabel: currentLevel < 0 ? "" : environment.strings.ProfileLevelInfo_LevelIndex(Int32(currentLevel)),
rightLabel: nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "", rightLabel: currentLevel < 0 ? "Negative rating" : nextLevel.flatMap { environment.strings.ProfileLevelInfo_LevelIndex(Int32($0)) } ?? "",
badgeValue: badgeText, badgeValue: badgeText,
badgeTotal: badgeTextSuffix, badgeTotal: badgeTextSuffix,
level: Int(currentLevel) level: Int(currentLevel)
@ -491,12 +498,21 @@ private final class ProfileLevelInfoScreenComponent: Component {
contentHeight += 129.0 contentHeight += 129.0
if let secondaryDescriptionTextString { if let secondaryDescriptionTextString {
let changingPreviewAnimationOffset: CGFloat = self.isPreviewingPendingRating ? -100.0 : 100.0
let transitionBlurRadius: CGFloat = 10.0
if isChangingPreview, let secondaryDescriptionTextView = self.secondaryDescriptionText?.view { if isChangingPreview, let secondaryDescriptionTextView = self.secondaryDescriptionText?.view {
self.secondaryDescriptionText = nil self.secondaryDescriptionText = nil
transition.setTransform(view: secondaryDescriptionTextView, transform: CATransform3DMakeScale(0.9, 0.9, 1.0)) transition.setTransform(view: secondaryDescriptionTextView, transform: CATransform3DMakeTranslation(changingPreviewAnimationOffset, 0.0, 0.0))
alphaTransition.setAlpha(view: secondaryDescriptionTextView, alpha: 0.0, completion: { [weak secondaryDescriptionTextView] _ in alphaTransition.setAlpha(view: secondaryDescriptionTextView, alpha: 0.0, completion: { [weak secondaryDescriptionTextView] _ in
secondaryDescriptionTextView?.removeFromSuperview() secondaryDescriptionTextView?.removeFromSuperview()
}) })
if let blurFilter = CALayer.blur() {
blurFilter.setValue(transitionBlurRadius as NSNumber, forKey: "inputRadius")
secondaryDescriptionTextView.layer.filters = [blurFilter]
secondaryDescriptionTextView.layer.animate(from: 0.0 as NSNumber, to: transitionBlurRadius as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false)
}
} }
contentHeight -= 8.0 contentHeight -= 8.0
@ -510,9 +526,16 @@ private final class ProfileLevelInfoScreenComponent: Component {
self.secondaryDescriptionText = secondaryDescriptionText self.secondaryDescriptionText = secondaryDescriptionText
} }
let secondaryTextColor: UIColor
if currentLevel < 0 {
secondaryTextColor = UIColor(rgb: 0xFF3B30)
} else {
secondaryTextColor = environment.theme.list.itemSecondaryTextColor
}
let secondaryDescriptionAttributedString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(secondaryDescriptionTextString, attributes: MarkdownAttributes( let secondaryDescriptionAttributedString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(secondaryDescriptionTextString, attributes: MarkdownAttributes(
body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemSecondaryTextColor), body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: secondaryTextColor),
bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.itemSecondaryTextColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: secondaryTextColor),
link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor),
linkAttribute: { url in linkAttribute: { url in
return ("URL", url) return ("URL", url)
@ -563,8 +586,16 @@ private final class ProfileLevelInfoScreenComponent: Component {
if secondaryDescriptionTextView.superview == nil { if secondaryDescriptionTextView.superview == nil {
self.scrollContentView.addSubview(secondaryDescriptionTextView) self.scrollContentView.addSubview(secondaryDescriptionTextView)
if isChangingPreview { if isChangingPreview {
transition.animateScale(view: secondaryDescriptionTextView, from: 0.9, to: 1.0) transition.animatePosition(view: secondaryDescriptionTextView, from: CGPoint(x: -changingPreviewAnimationOffset, y: 0.0), to: CGPoint(), additive: true)
alphaTransition.animateAlpha(view: secondaryDescriptionTextView, from: 0.0, to: 1.0) alphaTransition.animateAlpha(view: secondaryDescriptionTextView, from: 0.0, to: 1.0)
if let blurFilter = CALayer.blur() {
blurFilter.setValue(transitionBlurRadius as NSNumber, forKey: "inputRadius")
secondaryDescriptionTextView.layer.filters = [blurFilter]
secondaryDescriptionTextView.layer.animate(from: transitionBlurRadius as NSNumber, to: 0.0 as NSNumber, keyPath: "filters.gaussianBlur.inputRadius", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.3, removeOnCompletion: false, completion: { [weak secondaryDescriptionTextView] _ in
secondaryDescriptionTextView?.layer.filters = nil
})
}
} }
} }
secondaryDescriptionTextTransition.setPosition(view: secondaryDescriptionTextView, position: secondaryDescriptionTextFrame.center) secondaryDescriptionTextTransition.setPosition(view: secondaryDescriptionTextView, position: secondaryDescriptionTextFrame.center)

View File

@ -47,11 +47,12 @@ final class ProfileLevelRatingBarBadge: Component {
final class View: UIView { final class View: UIView {
private let badgeView: UIView private let badgeView: UIView
private let badgeMaskView: UIView private let badgeMaskView: UIView
private let badgeShapeLayer = SimpleShapeLayer() private let badgeShapeView: UIImageView
private let badgeShapeAnimation = ComponentView<Empty>() private let badgeShapeAnimation = ComponentView<Empty>()
private let badgeForeground: SimpleLayer private let badgeForeground: SimpleLayer
let badgeIcon: UIImageView private let badgeIcon: UIImageView
private var disappearingBadgeIcon: UIImageView?
private let badgeLabel = ComponentView<Empty>() private let badgeLabel = ComponentView<Empty>()
private let suffixLabel = ComponentView<Empty>() private let suffixLabel = ComponentView<Empty>()
@ -68,11 +69,10 @@ final class ProfileLevelRatingBarBadge: Component {
self.badgeView.alpha = 0.0 self.badgeView.alpha = 0.0
self.badgeView.layer.anchorPoint = CGPoint() self.badgeView.layer.anchorPoint = CGPoint()
self.badgeShapeLayer.fillColor = UIColor.white.cgColor self.badgeShapeView = UIImageView()
self.badgeShapeLayer.transform = CATransform3DMakeScale(1.0, -1.0, 1.0)
self.badgeMaskView = UIView() self.badgeMaskView = UIView()
self.badgeMaskView.layer.addSublayer(self.badgeShapeLayer) self.badgeMaskView.addSubview(self.badgeShapeView)
self.badgeView.mask = self.badgeMaskView self.badgeView.mask = self.badgeMaskView
self.badgeForeground = SimpleLayer() self.badgeForeground = SimpleLayer()
@ -108,17 +108,49 @@ final class ProfileLevelRatingBarBadge: Component {
self.isUpdating = false self.isUpdating = false
} }
if self.component == nil { var labelsTransition = transition
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate) if let hint = transition.userData(TransitionHint.self), hint.animateText {
labelsTransition = .spring(duration: 0.5)
}
let previousComponent = self.component
if previousComponent == nil || (previousComponent?.title == "") != (component.title == "") {
if !labelsTransition.animation.isImmediate, self.badgeIcon.image != nil {
if let disappearingBadgeIcon = self.disappearingBadgeIcon {
self.disappearingBadgeIcon = nil
disappearingBadgeIcon.removeFromSuperview()
}
let disappearingBadgeIcon = UIImageView()
disappearingBadgeIcon.contentMode = self.badgeIcon.contentMode
disappearingBadgeIcon.frame = self.badgeIcon.frame
disappearingBadgeIcon.image = self.badgeIcon.image
disappearingBadgeIcon.tintColor = self.badgeIcon.tintColor
self.badgeView.insertSubview(disappearingBadgeIcon, aboveSubview: self.badgeIcon)
labelsTransition.setScale(view: disappearingBadgeIcon, scale: 0.001)
labelsTransition.setAlpha(view: disappearingBadgeIcon, alpha: 0.0, completion: { [weak self, weak disappearingBadgeIcon] _ in
guard let self, let disappearingBadgeIcon else {
return
}
disappearingBadgeIcon.removeFromSuperview()
if self.disappearingBadgeIcon === disappearingBadgeIcon {
self.disappearingBadgeIcon = nil
}
})
labelsTransition.animateScale(view: self.badgeIcon, from: 0.001, to: 1.0)
labelsTransition.animateAlpha(view: self.badgeIcon, from: 0.0, to: 1.0)
}
if component.title.isEmpty {
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelWarningIcon")?.withRenderingMode(.alwaysTemplate)
} else {
self.badgeIcon.image = UIImage(bundleImageName: "Peer Info/ProfileLevelProgressIcon")?.withRenderingMode(.alwaysTemplate)
}
} }
self.component = component self.component = component
self.badgeIcon.tintColor = component.theme.list.itemCheckColors.foregroundColor self.badgeIcon.tintColor = component.theme.list.itemCheckColors.foregroundColor
self.disappearingBadgeIcon?.tintColor = component.theme.list.itemCheckColors.foregroundColor
var labelsTransition = transition
if let hint = transition.userData(TransitionHint.self), hint.animateText {
labelsTransition = .spring(duration: 0.4)
}
let badgeLabelSize = self.badgeLabel.update( let badgeLabelSize = self.badgeLabel.update(
transition: labelsTransition, transition: labelsTransition,
@ -150,7 +182,12 @@ final class ProfileLevelRatingBarBadge: Component {
containerSize: CGSize(width: 300.0, height: 100.0) containerSize: CGSize(width: 300.0, height: 100.0)
) )
var badgeWidth: CGFloat = badgeLabelSize.width + 3.0 + 60.0 var badgeWidth: CGFloat = 0.0
if !component.title.isEmpty {
badgeWidth += badgeLabelSize.width + 3.0 + 60.0
} else {
badgeWidth += 78.0
}
if component.suffix != nil { if component.suffix != nil {
badgeWidth += badgeSuffixSize.width + badgeSuffixSpacing badgeWidth += badgeSuffixSize.width + badgeSuffixSpacing
} }
@ -158,13 +195,21 @@ final class ProfileLevelRatingBarBadge: Component {
let badgeSize = CGSize(width: badgeWidth, height: 48.0) let badgeSize = CGSize(width: badgeWidth, height: 48.0)
let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0) let badgeFullSize = CGSize(width: badgeWidth, height: 48.0 + 12.0)
self.badgeMaskView.frame = CGRect(origin: .zero, size: badgeFullSize) 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) self.badgeView.bounds = CGRect(origin: .zero, size: badgeFullSize)
self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 600.0, height: badgeFullSize.height + 10.0)) self.badgeForeground.bounds = CGRect(origin: CGPoint(), size: CGSize(width: 600.0, height: badgeFullSize.height + 10.0))
self.badgeIcon.frame = CGRect(x: 13.0, y: 8.0, width: 30.0, height: 30.0) let badgeIconFrame: CGRect
if !component.title.isEmpty {
badgeIconFrame = CGRect(x: 13.0, y: 8.0, width: 30.0, height: 30.0)
} else {
badgeIconFrame = CGRect(x: floor((badgeWidth - 30.0) * 0.5), y: 8.0, width: 30.0, height: 30.0)
}
labelsTransition.setFrame(view: self.badgeIcon, frame: badgeIconFrame)
if let disappearingBadgeIcon = self.disappearingBadgeIcon {
labelsTransition.setFrame(view: disappearingBadgeIcon, frame: badgeIconFrame)
}
self.badgeView.alpha = 1.0 self.badgeView.alpha = 1.0
@ -194,77 +239,63 @@ final class ProfileLevelRatingBarBadge: Component {
if self.previousAvailableSize != availableSize { if self.previousAvailableSize != availableSize {
self.previousAvailableSize = availableSize self.previousAvailableSize = availableSize
let activeColors: [UIColor] = [
component.theme.list.itemCheckColors.fillColor,
component.theme.list.itemCheckColors.fillColor
]
var locations: [CGFloat] = []
let delta = 1.0 / CGFloat(activeColors.count - 1)
for i in 0 ..< activeColors.count {
locations.append(delta * CGFloat(i))
}
let gradient = generateGradientImage(size: CGSize(width: 200.0, height: 60.0), colors: activeColors, locations: locations, direction: .horizontal)
self.badgeForeground.contentsGravity = .resizeAspectFill
self.badgeForeground.contents = gradient?.cgImage
self.setupGradientAnimations()
} }
return size return size
} }
func adjustTail(size: CGSize, overflowWidth: CGFloat, transition: ComponentTransition) { func updateColors(background: UIColor) {
guard let component else { self.badgeForeground.backgroundColor = background.cgColor
return }
func adjustTail(size: CGSize, tailOffset: CGFloat, transition: ComponentTransition) {
if self.badgeShapeView.image == nil {
self.badgeShapeView.image = generateStretchableFilledCircleImage(diameter: 48.0, color: UIColor.white)
} }
let _ = component self.badgeShapeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: 48.0))
var tailPosition = size.width * 0.5 let badgeShapeSize = CGSize(width: 78, height: 60)
tailPosition += overflowWidth
tailPosition = max(36.0, min(size.width - 36.0, tailPosition))
let tailPositionFraction = tailPosition / size.width
transition.setShapeLayerPath(layer: self.badgeShapeLayer, path: generateRoundedRectWithTailPath(rectSize: size, tailPosition: tailPositionFraction, transformTail: false).cgPath)
let badgeShapeSize = CGSize(width: 128, height: 128)
let _ = self.badgeShapeAnimation.update( let _ = self.badgeShapeAnimation.update(
transition: .immediate, transition: .immediate,
component: AnyComponent(LottieComponent( component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "badge_with_tail"), content: LottieComponent.AppBundleContent(name: "badge_with_tail"),
color: .red,//component.theme.list.itemCheckColors.fillColor, color: .white,
placeholderColor: nil, placeholderColor: nil,
startingPosition: .begin, startingPosition: .begin,
size: badgeShapeSize, size: badgeShapeSize,
renderingScale: nil, renderingScale: UIScreenScale,
loop: false, loop: false,
playOnce: nil playOnce: nil
)), )),
environment: {}, environment: {},
containerSize: badgeShapeSize containerSize: badgeShapeSize
) )
if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View, !"".isEmpty { if let badgeShapeAnimationView = self.badgeShapeAnimation.view as? LottieComponent.View {
if badgeShapeAnimationView.superview == nil { if badgeShapeAnimationView.superview == nil {
badgeShapeAnimationView.layer.anchorPoint = CGPoint() badgeShapeAnimationView.layer.anchorPoint = CGPoint()
self.addSubview(badgeShapeAnimationView) self.badgeMaskView.addSubview(badgeShapeAnimationView)
} }
let transition: ComponentTransition = .immediate var shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: badgeShapeSize)
let badgeShapeWidth = badgeShapeSize.width
let midFrame = 359 / 2
if tailOffset < badgeShapeWidth * 0.5 {
let frameIndex = Int(floor(CGFloat(midFrame) * tailOffset / (badgeShapeWidth * 0.5)))
badgeShapeAnimationView.setFrameIndex(index: frameIndex)
} else if tailOffset >= size.width - badgeShapeWidth * 0.5 {
let endOffset = tailOffset - (size.width - badgeShapeWidth * 0.5)
let frameIndex = midFrame + Int(floor(CGFloat(359 - midFrame) * endOffset / (badgeShapeWidth * 0.5)))
badgeShapeAnimationView.setFrameIndex(index: frameIndex)
shapeFrame.origin.x = size.width - badgeShapeWidth
} else {
badgeShapeAnimationView.setFrameIndex(index: midFrame)
shapeFrame.origin.x = tailOffset - badgeShapeWidth * 0.5
}
let shapeFrame = CGRect(origin: CGPoint(x: 0.0, y: -10.0), size: badgeShapeSize)
badgeShapeAnimationView.center = shapeFrame.origin badgeShapeAnimationView.center = shapeFrame.origin
badgeShapeAnimationView.bounds = CGRect(origin: CGPoint(), size: shapeFrame.size) badgeShapeAnimationView.bounds = CGRect(origin: CGPoint(), size: shapeFrame.size)
let scaleFactor: CGFloat = 144.0 / (946.0 / 4.0)
transition.setScale(view: badgeShapeAnimationView, scale: scaleFactor)
let badgeShapeWidth = floor(shapeFrame.width * scaleFactor)
let badgeShapeOffset = -overflowWidth / badgeShapeWidth
let _ = badgeShapeOffset
//badgeShapeAnimationView.setFrameIndex(index: 0)
} }
//let transition: ContainedViewLayoutTransition = .immediate //let transition: ContainedViewLayoutTransition = .immediate
@ -275,38 +306,6 @@ final class ProfileLevelRatingBarBadge: Component {
let transition: ContainedViewLayoutTransition = .immediate let transition: ContainedViewLayoutTransition = .immediate
transition.updateTransformRotation(view: self.badgeView, angle: angle) transition.updateTransformRotation(view: self.badgeView, angle: angle)
} }
private func setupGradientAnimations() {
guard let _ = self.component else {
return
}
if let _ = self.badgeForeground.animation(forKey: "movement") {
} else {
CATransaction.begin()
let badgePreviousValue = self.badgeForeground.position.x
let badgeNewValue: CGFloat
if self.badgeForeground.position.x == -300.0 {
badgeNewValue = 0.0
} else {
badgeNewValue = -300.0
}
self.badgeForeground.position = CGPoint(x: badgeNewValue, 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")
CATransaction.commit()
}
}
} }
func makeView() -> View { func makeView() -> View {

View File

@ -6,7 +6,6 @@ import ComponentFlow
import MultilineTextComponent import MultilineTextComponent
import BundleIconComponent import BundleIconComponent
import HierarchyTrackingLayer import HierarchyTrackingLayer
import AnimatedTextComponent
final class ProfileLevelRatingBarComponent: Component { final class ProfileLevelRatingBarComponent: Component {
final class TransitionHint { final class TransitionHint {
@ -69,20 +68,33 @@ final class ProfileLevelRatingBarComponent: Component {
} }
private final class AnimationState { private final class AnimationState {
enum Wraparound {
case left
case right
}
let fromLevel: Int
let toLevel: Int
let fromLeftLabelText: String
let fromRightLabelText: String
let fromValue: CGFloat let fromValue: CGFloat
let toValue: CGFloat let toValue: CGFloat
let fromBadgeSize: CGSize let fromBadgeSize: CGSize
let startTime: Double let startTime: Double
let duration: Double let duration: Double
let isWraparound: Bool let wraparound: Wraparound?
init(fromValue: CGFloat, toValue: CGFloat, fromBadgeSize: CGSize, startTime: Double, duration: Double, isWraparound: Bool) { init(fromLevel: Int, toLevel: Int, fromLeftLabelText: String, fromRightLabelText: String, fromValue: CGFloat, toValue: CGFloat, fromBadgeSize: CGSize, startTime: Double, duration: Double, wraparound: Wraparound?) {
self.fromLevel = fromLevel
self.toLevel = toLevel
self.fromLeftLabelText = fromLeftLabelText
self.fromRightLabelText = fromRightLabelText
self.fromValue = fromValue self.fromValue = fromValue
self.toValue = toValue self.toValue = toValue
self.fromBadgeSize = fromBadgeSize self.fromBadgeSize = fromBadgeSize
self.startTime = startTime self.startTime = startTime
self.duration = duration self.duration = duration
self.isWraparound = isWraparound self.wraparound = wraparound
} }
func timeFraction(at timestamp: Double) -> CGFloat { func timeFraction(at timestamp: Double) -> CGFloat {
@ -91,8 +103,30 @@ final class ProfileLevelRatingBarComponent: Component {
return fraction return fraction
} }
func stepFraction(at timestamp: Double) -> (step: Int, fraction: CGFloat) {
if self.wraparound != nil {
var t = self.timeFraction(at: timestamp)
t = bezierPoint(0.6, 0.0, 0.4, 1.0, t)
if t < 0.5 {
let vt = t / 0.5
return (0, vt)
} else {
let vt = (t - 0.5) / 0.5
return (1, vt)
}
} else {
let t = self.timeFraction(at: timestamp)
return (0, listViewAnimationCurveSystem(t))
}
}
func fraction(at timestamp: Double) -> CGFloat { func fraction(at timestamp: Double) -> CGFloat {
return listViewAnimationCurveSystem(self.timeFraction(at: timestamp)) let t = self.timeFraction(at: timestamp)
if self.wraparound != nil {
return listViewAnimationCurveEaseInOut(t)
} else {
return listViewAnimationCurveSystem(t)
}
} }
func value(at timestamp: Double) -> CGFloat { func value(at timestamp: Double) -> CGFloat {
@ -100,14 +134,12 @@ final class ProfileLevelRatingBarComponent: Component {
return (1.0 - fraction) * self.fromValue + fraction * self.toValue return (1.0 - fraction) * self.fromValue + fraction * self.toValue
} }
func wrapAroundValue(at timestamp: Double, topValue: CGFloat) -> CGFloat { func wrapAroundValue(at timestamp: Double, bottomValue: CGFloat, topValue: CGFloat) -> CGFloat {
let fraction = self.fraction(at: timestamp) let (step, fraction) = self.stepFraction(at: timestamp)
if fraction <= 0.5 { if step == 0 {
let halfFraction = fraction / 0.5 return (1.0 - fraction) * self.fromValue + fraction * topValue
return (1.0 - halfFraction) * self.fromValue + halfFraction * topValue
} else { } else {
let halfFraction = (fraction - 0.5) / 0.5 return (1.0 - fraction) * bottomValue + fraction * self.toValue
return halfFraction * self.toValue
} }
} }
@ -123,6 +155,7 @@ final class ProfileLevelRatingBarComponent: Component {
final class View: UIView { final class View: UIView {
private let barBackground: UIImageView private let barBackground: UIImageView
private let backgroundClippingContainer: UIView private let backgroundClippingContainer: UIView
private let foregroundBarClippingContainer: UIView
private let foregroundClippingContainer: UIView private let foregroundClippingContainer: UIView
private let barForeground: UIImageView private let barForeground: UIImageView
@ -139,13 +172,29 @@ final class ProfileLevelRatingBarComponent: Component {
private var hierarchyTracker: HierarchyTrackingLayer? private var hierarchyTracker: HierarchyTrackingLayer?
private var animationLink: SharedDisplayLinkDriver.Link? private var animationLink: SharedDisplayLinkDriver.Link?
private var badgePhysicsLink: SharedDisplayLinkDriver.Link?
private var animationState: AnimationState? private var animationState: AnimationState?
private var previousAnimationTimestamp: Double?
private var previousAnimationTimeFraction: CGFloat?
private var animationDeltaTime: Double?
private var animationIsMovingOverStep: Bool = false
private var badgeAngularSpeed: CGFloat = 0.0
private var badgeScale: CGFloat = 1.0
private var badgeAngle: CGFloat = 0.0
private var previousPhysicsTimestamp: Double?
private var testFraction: CGFloat?
private var startTestFraction: CGFloat?
override init(frame: CGRect) { override init(frame: CGRect) {
self.barBackground = UIImageView() self.barBackground = UIImageView()
self.backgroundClippingContainer = UIView() self.backgroundClippingContainer = UIView()
self.backgroundClippingContainer.clipsToBounds = true self.backgroundClippingContainer.clipsToBounds = true
self.foregroundBarClippingContainer = UIView()
self.foregroundBarClippingContainer.clipsToBounds = true
self.foregroundClippingContainer = UIView() self.foregroundClippingContainer = UIView()
self.foregroundClippingContainer.clipsToBounds = true self.foregroundClippingContainer.clipsToBounds = true
self.barForeground = UIImageView() self.barForeground = UIImageView()
@ -161,7 +210,28 @@ final class ProfileLevelRatingBarComponent: Component {
return return
} }
self.updateAnimations() self.updateAnimations()
if value {
if self.badgePhysicsLink == nil {
let badgePhysicsLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] _ in
guard let self else {
return
}
self.updateBadgePhysics()
})
self.badgePhysicsLink = badgePhysicsLink
}
} else {
if let badgePhysicsLink = self.badgePhysicsLink {
self.badgePhysicsLink = nil
badgePhysicsLink.invalidate()
}
}
} }
#if DEBUG
self.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.onPanGesture(_:))))
#endif
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -171,7 +241,40 @@ final class ProfileLevelRatingBarComponent: Component {
deinit { deinit {
} }
@objc private func onPanGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began, .changed:
if self.testFraction == nil {
self.testFraction = self.component?.value
}
if self.startTestFraction == nil {
if let testFraction = self.testFraction {
self.startTestFraction = testFraction
}
}
if let startTestFraction = self.startTestFraction {
let x = recognizer.translation(in: self).x
var value: CGFloat = startTestFraction + x / self.bounds.width
value = max(0.0, min(1.0, value))
self.testFraction = value
self.state?.updated(transition: .immediate, isLocal: true)
}
case .ended, .cancelled:
self.startTestFraction = nil
default:
break
}
}
private func updateAnimations() { private func updateAnimations() {
let timestamp = CACurrentMediaTime()
let deltaTime: CGFloat
if let previousAnimationTimestamp = self.previousAnimationTimestamp {
deltaTime = min(0.2, timestamp - previousAnimationTimestamp)
} else {
deltaTime = 1.0 / 60.0
}
if let hierarchyTracker = self.hierarchyTracker, hierarchyTracker.isInHierarchy { if let hierarchyTracker = self.hierarchyTracker, hierarchyTracker.isInHierarchy {
if self.animationState != nil { if self.animationState != nil {
if self.animationLink == nil { if self.animationLink == nil {
@ -194,15 +297,104 @@ final class ProfileLevelRatingBarComponent: Component {
} }
if let animationState = self.animationState { if let animationState = self.animationState {
if animationState.timeFraction(at: CACurrentMediaTime()) >= 1.0 { let timeFraction = animationState.timeFraction(at: timestamp)
if timeFraction >= 1.0 {
self.animationState = nil self.animationState = nil
self.updateAnimations() self.updateAnimations()
return
} else {
if let previousAnimationTimeFraction = self.previousAnimationTimeFraction {
if previousAnimationTimeFraction < 0.5 && timeFraction >= 0.5 {
self.animationIsMovingOverStep = true
}
}
self.previousAnimationTimeFraction = timeFraction
} }
} else {
self.previousAnimationTimeFraction = nil
} }
self.animationDeltaTime = Double(deltaTime)
if self.animationState != nil && !self.isUpdating { if self.animationState != nil && !self.isUpdating {
self.state?.updated(transition: .immediate, isLocal: true) self.state?.updated(transition: .immediate, isLocal: true)
} }
self.animationDeltaTime = nil
self.animationIsMovingOverStep = false
}
private func addBadgeDeltaX(value: CGFloat, deltaTime: CGFloat) {
var deltaTime = deltaTime
deltaTime /= UIView.animationDurationFactor()
let horizontalVelocity = value / deltaTime
var badgeAngle = self.badgeAngle
badgeAngle -= horizontalVelocity * 0.00005
let maxAngle: CGFloat = 0.1
if abs(badgeAngle) > maxAngle {
badgeAngle = badgeAngle < 0.0 ? -maxAngle : maxAngle
}
self.badgeAngle = badgeAngle
}
private func updateBadgePhysics() {
let timestamp = CACurrentMediaTime()
var deltaTime: CGFloat
if let previousPhysicsTimestamp = self.previousPhysicsTimestamp {
deltaTime = CGFloat(min(1.0 / 60.0, timestamp - previousPhysicsTimestamp))
} else {
deltaTime = CGFloat(1.0 / 60.0)
}
self.previousPhysicsTimestamp = timestamp
deltaTime /= UIView.animationDurationFactor()
let testSpringFriction: CGFloat = 18.5
let testSpringConstant: CGFloat = 243.0
let frictionConstant: CGFloat = testSpringFriction
let springConstant: CGFloat = testSpringConstant
let time: CGFloat = deltaTime
var badgeAngle = self.badgeAngle
// friction force = velocity * friction constant
let frictionForce = self.badgeAngularSpeed * frictionConstant
// spring force = (target point - current position) * spring constant
let springForce = -badgeAngle * springConstant
// force = spring force - friction force
let force = springForce - frictionForce
// velocity = current velocity + force * time / mass
self.badgeAngularSpeed = self.badgeAngularSpeed + force * time
// position = current position + velocity * time
badgeAngle = badgeAngle + self.badgeAngularSpeed * time
badgeAngle = badgeAngle.isNaN ? 0.0 : badgeAngle
let epsilon: CGFloat = 0.01
if abs(badgeAngle) < epsilon && abs(self.badgeAngularSpeed) < epsilon {
badgeAngle = 0.0
self.badgeAngularSpeed = 0.0
}
if abs(badgeAngle) > 0.22 {
badgeAngle = badgeAngle < 0.0 ? -0.22 : 0.22
}
if self.badgeAngle != badgeAngle {
self.badgeAngle = badgeAngle
self.updateBadgeTransform()
}
}
private func updateBadgeTransform() {
guard let badgeView = self.badge.view else {
return
}
var transform = CATransform3DIdentity
transform = CATransform3DScale(transform, self.badgeScale, self.badgeScale, 1.0)
transform = CATransform3DRotate(transform, self.badgeAngle, 0.0, 0.0, 1.0)
badgeView.layer.transform = transform
} }
func update(component: ProfileLevelRatingBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize { func update(component: ProfileLevelRatingBarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
@ -215,11 +407,31 @@ final class ProfileLevelRatingBarComponent: Component {
var labelsTransition = transition var labelsTransition = transition
if let previousComponent = self.component, let hint = transition.userData(TransitionHint.self), hint.animate { if let previousComponent = self.component, let hint = transition.userData(TransitionHint.self), hint.animate {
labelsTransition = .spring(duration: 0.4) labelsTransition = .spring(duration: 0.5)
var fromLevel = previousComponent.level
var fromLeftLabelText = previousComponent.leftLabel
var fromRightLabelText = previousComponent.rightLabel
let toLevel: Int = component.level
let fromValue: CGFloat let fromValue: CGFloat
if let animationState = self.animationState { if let animationState = self.animationState {
fromValue = animationState.value(at: CACurrentMediaTime()) if let wraparound = animationState.wraparound {
let wraparoundEnd: CGFloat
switch wraparound {
case .left:
wraparoundEnd = 0.0
case .right:
wraparoundEnd = 1.0
}
if animationState.stepFraction(at: CACurrentMediaTime()).step == 0 {
fromLevel = animationState.fromLevel
fromLeftLabelText = animationState.fromLeftLabelText
fromRightLabelText = animationState.fromRightLabelText
}
fromValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), bottomValue: 1.0 - wraparoundEnd, topValue: wraparoundEnd)
} else {
fromValue = animationState.value(at: CACurrentMediaTime())
}
} else { } else {
fromValue = previousComponent.value fromValue = previousComponent.value
} }
@ -229,13 +441,23 @@ final class ProfileLevelRatingBarComponent: Component {
} else { } else {
fromBadgeSize = CGSize() fromBadgeSize = CGSize()
} }
var wraparound: AnimationState.Wraparound?
var duration = 0.4
if previousComponent.level != component.level {
wraparound = component.level > previousComponent.level ? .right : .left
duration = 0.8
}
self.animationState = AnimationState( self.animationState = AnimationState(
fromLevel: fromLevel,
toLevel: toLevel,
fromLeftLabelText: fromLeftLabelText,
fromRightLabelText: fromRightLabelText,
fromValue: fromValue, fromValue: fromValue,
toValue: component.value, toValue: component.value,
fromBadgeSize: fromBadgeSize, fromBadgeSize: fromBadgeSize,
startTime: CACurrentMediaTime(), startTime: CACurrentMediaTime(),
duration: 0.4 * UIView.animationDurationFactor(), duration: duration * UIView.animationDurationFactor(),
isWraparound: false//previousComponent.level < component.level wraparound: wraparound
) )
self.updateAnimations() self.updateAnimations()
} }
@ -248,20 +470,19 @@ final class ProfileLevelRatingBarComponent: Component {
self.barForeground.image = self.barBackground.image self.barForeground.image = self.barBackground.image
} }
self.barBackground.tintColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5)
self.barForeground.tintColor = component.theme.list.itemCheckColors.fillColor
if self.barBackground.superview == nil { if self.barBackground.superview == nil {
self.addSubview(self.barBackground) self.addSubview(self.barBackground)
self.addSubview(self.backgroundClippingContainer) self.addSubview(self.backgroundClippingContainer)
self.addSubview(self.foregroundBarClippingContainer)
self.foregroundBarClippingContainer.addSubview(self.barForeground)
self.addSubview(self.foregroundClippingContainer) self.addSubview(self.foregroundClippingContainer)
self.foregroundClippingContainer.addSubview(self.barForeground)
} }
let progressValue: CGFloat let progressValue: CGFloat
if let animationState = self.animationState { if let testFraction = self.testFraction {
progressValue = animationState.value(at: CACurrentMediaTime()) progressValue = testFraction
} else { } else {
progressValue = component.value progressValue = component.value
} }
@ -269,18 +490,84 @@ final class ProfileLevelRatingBarComponent: Component {
let barBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - barHeight), size: CGSize(width: availableSize.width, height: barHeight)) let barBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - barHeight), size: CGSize(width: availableSize.width, height: barHeight))
transition.setFrame(view: self.barBackground, frame: barBackgroundFrame) transition.setFrame(view: self.barBackground, frame: barBackgroundFrame)
let barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height)) var barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
var barApparentForegroundFrame = barForegroundFrame var foregroundAlpha: CGFloat = 1.0
if let animationState = self.animationState, animationState.isWraparound { var foregroundContentsAlpha: CGFloat = 1.0
let progressValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), topValue: 1.0) var badgeScale: CGFloat = 1.0
barApparentForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height)) var currentIsNegativeRating: Bool = component.level < 0
var leftLabelText = component.leftLabel
var rightLabelText = component.rightLabel
if let animationState = self.animationState {
if let wraparound = animationState.wraparound {
let (step, progress) = animationState.stepFraction(at: CACurrentMediaTime())
if step == 0 {
currentIsNegativeRating = animationState.fromLevel < 0
leftLabelText = animationState.fromLeftLabelText
rightLabelText = animationState.fromRightLabelText
} else {
currentIsNegativeRating = animationState.toLevel < 0
}
let wraparoundEnd: CGFloat
switch wraparound {
case .left:
wraparoundEnd = 0.0
if step == 0 {
foregroundContentsAlpha = 1.0 * (1.0 - progress)
badgeScale = 1.0 * (1.0 - progress) + 0.3 * progress
} else {
foregroundAlpha = 1.0 * progress
foregroundContentsAlpha = foregroundAlpha
badgeScale = 1.0 * progress + 0.3 * (1.0 - progress)
}
case .right:
wraparoundEnd = 1.0
if step == 0 {
foregroundAlpha = 1.0 * (1.0 - progress)
foregroundContentsAlpha = foregroundAlpha
badgeScale = 1.0 * (1.0 - progress) + 0.3 * progress
} else {
foregroundContentsAlpha = 1.0 * progress
badgeScale = 1.0 * progress + 0.3 * (1.0 - progress)
}
}
let progressValue = animationState.wrapAroundValue(at: CACurrentMediaTime(), bottomValue: 1.0 - wraparoundEnd, topValue: wraparoundEnd)
barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
} else {
let progressValue = animationState.value(at: CACurrentMediaTime())
barForegroundFrame = CGRect(origin: barBackgroundFrame.origin, size: CGSize(width: floorToScreenPixels(progressValue * barBackgroundFrame.width), height: barBackgroundFrame.height))
}
} }
transition.setFrame(view: self.foregroundClippingContainer, frame: barApparentForegroundFrame)
let backgroundClippingFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barApparentForegroundFrame.width, y: barBackgroundFrame.minY), size: CGSize(width: barBackgroundFrame.width - barApparentForegroundFrame.width, height: barBackgroundFrame.height)) let badgeColor: UIColor
if currentIsNegativeRating {
badgeColor = UIColor(rgb: 0xFF3B30)
} else {
badgeColor = component.theme.list.itemCheckColors.fillColor
}
self.barBackground.tintColor = component.theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5)
self.barForeground.tintColor = badgeColor
var effectiveBarForegroundFrame = barForegroundFrame
if currentIsNegativeRating {
effectiveBarForegroundFrame.size.width = barBackgroundFrame.maxX - barForegroundFrame.maxX
effectiveBarForegroundFrame.origin.x = barBackgroundFrame.maxX - effectiveBarForegroundFrame.width
}
transition.setPosition(view: self.foregroundBarClippingContainer, position: effectiveBarForegroundFrame.center)
transition.setBounds(view: self.foregroundBarClippingContainer, bounds: CGRect(origin: CGPoint(x: effectiveBarForegroundFrame.minX - barForegroundFrame.minX, y: 0.0), size: effectiveBarForegroundFrame.size))
transition.setPosition(view: self.foregroundClippingContainer, position: effectiveBarForegroundFrame.center)
transition.setBounds(view: self.foregroundClippingContainer, bounds: CGRect(origin: CGPoint(x: effectiveBarForegroundFrame.minX - barForegroundFrame.minX, y: 0.0), size: effectiveBarForegroundFrame.size))
transition.setAlpha(view: self.foregroundBarClippingContainer, alpha: foregroundAlpha)
transition.setAlpha(view: self.foregroundClippingContainer, alpha: foregroundContentsAlpha)
let backgroundClippingFrame = CGRect(origin: CGPoint(x: barBackgroundFrame.minX + barForegroundFrame.width, y: barBackgroundFrame.minY), size: CGSize(width: barBackgroundFrame.width - barForegroundFrame.width, height: barBackgroundFrame.height))
transition.setPosition(view: self.backgroundClippingContainer, position: backgroundClippingFrame.center) transition.setPosition(view: self.backgroundClippingContainer, position: backgroundClippingFrame.center)
transition.setBounds(view: self.backgroundClippingContainer, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX - barBackgroundFrame.minX, y: 0.0), size: backgroundClippingFrame.size)) transition.setBounds(view: self.backgroundClippingContainer, bounds: CGRect(origin: CGPoint(x: backgroundClippingFrame.minX - barBackgroundFrame.minX, y: 0.0), size: backgroundClippingFrame.size))
transition.setAlpha(view: self.backgroundClippingContainer, alpha: foregroundContentsAlpha)
transition.setFrame(view: self.barForeground, frame: CGRect(origin: CGPoint(), size: barBackgroundFrame.size)) transition.setFrame(view: self.barForeground, frame: CGRect(origin: CGPoint(), size: barBackgroundFrame.size))
@ -288,52 +575,32 @@ final class ProfileLevelRatingBarComponent: Component {
let leftLabelSize = self.backgroundLeftLabel.update( let leftLabelSize = self.backgroundLeftLabel.update(
transition: labelsTransition, transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent( component: AnyComponent(MultilineTextComponent(
font: labelFont, text: .plain(NSAttributedString(string: leftLabelText, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor))
color: component.theme.list.itemPrimaryTextColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.leftLabel)
)]
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
) )
let _ = self.foregroundLeftLabel.update( let _ = self.foregroundLeftLabel.update(
transition: labelsTransition, transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent( component: AnyComponent(MultilineTextComponent(
font: labelFont, text: .plain(NSAttributedString(string: leftLabelText, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor))
color: component.theme.list.itemCheckColors.foregroundColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.leftLabel)
)]
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
) )
let rightLabelSize = self.backgroundRightLabel.update( let rightLabelSize = self.backgroundRightLabel.update(
transition: labelsTransition, transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent( component: AnyComponent(MultilineTextComponent(
font: labelFont, text: .plain(NSAttributedString(string: rightLabelText, font: labelFont, textColor: component.theme.list.itemPrimaryTextColor))
color: component.theme.list.itemPrimaryTextColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.rightLabel)
)]
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
) )
let _ = self.foregroundRightLabel.update( let _ = self.foregroundRightLabel.update(
transition: labelsTransition, transition: labelsTransition,
component: AnyComponent(AnimatedTextComponent( component: AnyComponent(MultilineTextComponent(
font: labelFont, text: .plain(NSAttributedString(string: rightLabelText, font: labelFont, textColor: component.theme.list.itemCheckColors.foregroundColor))
color: component.theme.list.itemCheckColors.foregroundColor,
items: [AnimatedTextComponent.Item(
id: AnyHashable(0),
content: .text(component.rightLabel)
)]
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0) containerSize: CGSize(width: barBackgroundFrame.width, height: 100.0)
@ -379,8 +646,8 @@ final class ProfileLevelRatingBarComponent: Component {
transition: transition.withUserData(ProfileLevelRatingBarBadge.TransitionHint(animateText: !labelsTransition.animation.isImmediate)), transition: transition.withUserData(ProfileLevelRatingBarBadge.TransitionHint(animateText: !labelsTransition.animation.isImmediate)),
component: AnyComponent(ProfileLevelRatingBarBadge( component: AnyComponent(ProfileLevelRatingBarBadge(
theme: component.theme, theme: component.theme,
title: "\(component.badgeValue)", title: component.level < 0 ? "" : "\(component.badgeValue)",
suffix: component.badgeTotal suffix: component.level < 0 ? nil : component.badgeTotal
)), )),
environment: {}, environment: {},
containerSize: CGSize(width: 200.0, height: 200.0) containerSize: CGSize(width: 200.0, height: 200.0)
@ -412,9 +679,24 @@ final class ProfileLevelRatingBarComponent: Component {
} }
badgeFrame.origin.x += badgeOverflowWidth badgeFrame.origin.x += badgeOverflowWidth
badgeView.frame = badgeFrame let badgeTailOffset = (barBackgroundFrame.minX + barForegroundFrame.width) - badgeFrame.minX
let badgePosition = CGPoint(x: badgeFrame.minX + badgeTailOffset, y: badgeFrame.maxY)
badgeView.adjustTail(size: apparentBadgeSize, overflowWidth: -badgeOverflowWidth, transition: transition) if let animationDeltaTime = self.animationDeltaTime, self.animationState != nil, !self.animationIsMovingOverStep {
let previousX = badgeView.center.x
self.addBadgeDeltaX(value: badgePosition.x - previousX, deltaTime: animationDeltaTime)
}
badgeView.center = badgePosition
badgeView.bounds = CGRect(origin: CGPoint(), size: badgeFrame.size)
transition.setAnchorPoint(layer: badgeView.layer, anchorPoint: CGPoint(x: max(0.0, min(1.0, badgeTailOffset / badgeFrame.width)), y: 1.0))
badgeView.updateColors(background: badgeColor)
badgeView.adjustTail(size: apparentBadgeSize, tailOffset: badgeTailOffset, transition: transition)
transition.setAlpha(view: badgeView, alpha: foregroundContentsAlpha)
self.badgeScale = badgeScale
self.updateBadgeTransform()
} }
return availableSize return availableSize

View File

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

View File

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

File diff suppressed because one or more lines are too long