mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-03 21:16:35 +00:00
Merge branch 'beta'
This commit is contained in:
commit
e16f2ff47f
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
12
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "badratingprofile.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/InlineRatingWarning.imageset/badratingprofile.pdf
vendored
Normal file
Binary file not shown.
12
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "badratingslider.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf
vendored
Normal file
BIN
submodules/TelegramUI/Images.xcassets/Peer Info/ProfileLevelWarningIcon.imageset/badratingslider.pdf
vendored
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user