This commit is contained in:
Ali 2023-06-29 23:32:01 +02:00
parent 9c366b0155
commit 030bbe2fff
10 changed files with 535 additions and 207 deletions

View File

@ -1,5 +1,7 @@
import UIKit import UIKit
@objc(Application) class Application: UIApplication { @objc(Application) class Application: UIApplication {
override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
}
} }

View File

@ -2560,7 +2560,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if let rawStorySubscriptions = self.rawStorySubscriptions { if let rawStorySubscriptions = self.rawStorySubscriptions {
var openCamera = false var openCamera = false
if let accountItem = rawStorySubscriptions.accountItem { if let accountItem = rawStorySubscriptions.accountItem {
openCamera = accountItem.storyCount == 0 openCamera = accountItem.storyCount == 0 && !accountItem.hasPending
} else { } else {
openCamera = true openCamera = true
} }
@ -5338,11 +5338,20 @@ private final class ChatListLocationContext {
peerStatus = .single(nil) peerStatus = .single(nil)
} }
let networkState: Signal<AccountNetworkState, NoError>
#if DEBUG && false
networkState = .single(AccountNetworkState.connecting(proxy: nil)) |> then(.single(AccountNetworkState.updating(proxy: nil)) |> delay(2.0, queue: .mainQueue())) |> then(.single(AccountNetworkState.online(proxy: nil)) |> delay(2.0, queue: .mainQueue())) |> then(.complete() |> delay(2.0, queue: .mainQueue())) |> restart
#elseif DEBUG && false
networkState = .single(AccountNetworkState.connecting(proxy: nil))
#else
networkState = context.account.networkState
#endif
switch location { switch location {
case .chatList: case .chatList:
if !hideNetworkActivityStatus { if !hideNetworkActivityStatus {
self.titleDisposable = combineLatest(queue: .mainQueue(), self.titleDisposable = combineLatest(queue: .mainQueue(),
context.account.networkState, networkState,
hasProxy, hasProxy,
passcode, passcode,
containerNode.currentItemState, containerNode.currentItemState,
@ -5354,6 +5363,7 @@ private final class ChatListLocationContext {
guard let self else { guard let self else {
return return
} }
self.updateChatList( self.updateChatList(
networkState: networkState, networkState: networkState,
proxy: proxy, proxy: proxy,

View File

@ -22,11 +22,11 @@ open class HierarchyTrackingLayer: CALayer {
override open func action(forKey event: String) -> CAAction? { override open func action(forKey event: String) -> CAAction? {
if event == kCAOnOrderIn { if event == kCAOnOrderIn {
self.didEnterHierarchy?()
self.isInHierarchy = true self.isInHierarchy = true
self.didEnterHierarchy?()
} else if event == kCAOnOrderOut { } else if event == kCAOnOrderOut {
self.didExitHierarchy?()
self.isInHierarchy = false self.isInHierarchy = false
self.didExitHierarchy?()
} }
return nullAction return nullAction
} }

View File

@ -677,12 +677,17 @@ public extension TelegramEngine {
hasMoreToken = "" hasMoreToken = ""
} }
var accountPendingItemCount = 0
if let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = view.value?.get(Stories.LocalState.self) {
accountPendingItemCount = localState.items.count
}
var accountItem: EngineStorySubscriptions.Item = EngineStorySubscriptions.Item( var accountItem: EngineStorySubscriptions.Item = EngineStorySubscriptions.Item(
peer: EnginePeer(accountPeer), peer: EnginePeer(accountPeer),
hasUnseen: false, hasUnseen: false,
hasUnseenCloseFriends: false, hasUnseenCloseFriends: false,
hasPending: false, hasPending: accountPendingItemCount != 0,
storyCount: 0, storyCount: accountPendingItemCount,
unseenCount: 0, unseenCount: 0,
lastTimestamp: 0 lastTimestamp: 0
) )
@ -698,7 +703,6 @@ public extension TelegramEngine {
var hasUnseen = false var hasUnseen = false
var hasUnseenCloseFriends = false var hasUnseenCloseFriends = false
var unseenCount = 0 var unseenCount = 0
var hasPending = false
if let peerState = peerState { if let peerState = peerState {
hasUnseen = peerState.maxReadId < lastEntry.id hasUnseen = peerState.maxReadId < lastEntry.id
@ -717,18 +721,12 @@ public extension TelegramEngine {
} }
} }
if let view = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = view.value?.get(Stories.LocalState.self) {
if !localState.items.isEmpty {
hasPending = true
}
}
let item = EngineStorySubscriptions.Item( let item = EngineStorySubscriptions.Item(
peer: EnginePeer(accountPeer), peer: EnginePeer(accountPeer),
hasUnseen: hasUnseen, hasUnseen: hasUnseen,
hasUnseenCloseFriends: hasUnseenCloseFriends, hasUnseenCloseFriends: hasUnseenCloseFriends,
hasPending: hasPending, hasPending: accountPendingItemCount != 0,
storyCount: itemsView.items.count, storyCount: itemsView.items.count + accountPendingItemCount,
unseenCount: unseenCount, unseenCount: unseenCount,
lastTimestamp: lastEntry.timestamp lastTimestamp: lastEntry.timestamp
) )

View File

@ -15,7 +15,7 @@ private struct StoryKey: Hashable {
public final class StoryContentContextImpl: StoryContentContext { public final class StoryContentContextImpl: StoryContentContext {
private final class PeerContext { private final class PeerContext {
private let context: AccountContext private let context: AccountContext
private let peerId: EnginePeer.Id let peerId: EnginePeer.Id
private(set) var sliceValue: StoryContentContextState.FocusedSlice? private(set) var sliceValue: StoryContentContextState.FocusedSlice?
fileprivate var nextItems: [EngineStoryItem] = [] fileprivate var nextItems: [EngineStoryItem] = []
@ -513,29 +513,26 @@ public final class StoryContentContextImpl: StoryContentContext {
} else { } else {
var startedWithUnseenValue = false var startedWithUnseenValue = false
if let (focusedPeerId, _) = self.focusedItem, focusedPeerId == self.context.account.peerId { var centralIndex: Int?
} else { if let (focusedPeerId, _) = self.focusedItem {
var centralIndex: Int? if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) {
if let (focusedPeerId, _) = self.focusedItem { centralIndex = index
if let index = storySubscriptions.items.firstIndex(where: { $0.peer.id == focusedPeerId }) {
centralIndex = index
}
} }
if centralIndex == nil { }
if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) { if centralIndex == nil {
centralIndex = index if let index = storySubscriptions.items.firstIndex(where: { $0.hasUnseen }) {
} centralIndex = index
} }
if centralIndex == nil { }
if !storySubscriptions.items.isEmpty { if centralIndex == nil {
centralIndex = 0 if !storySubscriptions.items.isEmpty {
} centralIndex = 0
} }
}
if let centralIndex { if let centralIndex {
if storySubscriptions.items[centralIndex].hasUnseen { if storySubscriptions.items[centralIndex].hasUnseen {
startedWithUnseenValue = true startedWithUnseenValue = true
}
} }
} }
@ -585,9 +582,11 @@ public final class StoryContentContextImpl: StoryContentContext {
} }
private func updatePeerContexts() { private func updatePeerContexts() {
if let currentState = self.currentState { if let currentState = self.currentState, let storySubscriptions = self.storySubscriptions, !storySubscriptions.items.contains(where: { $0.peer.id == currentState.centralPeerContext.peerId }) {
let _ = currentState self.currentState = nil
} else { }
if self.currentState == nil {
self.switchToFocusedPeerId() self.switchToFocusedPeerId()
} }
} }

View File

@ -981,9 +981,18 @@ public final class StoryItemSetContainerComponent: Component {
return true return true
} }
} else { } else {
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View { var canReply = true
inputPanelView.activateInput() if component.slice.peer.isService {
return false canReply = false
} else if case .unsupported = component.slice.item.storyItem.media {
canReply = false
}
if canReply {
if let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View {
inputPanelView.activateInput()
return false
}
} }
} }
return false return false

View File

@ -22,8 +22,8 @@ swift_library(
"//submodules/ContextUI", "//submodules/ContextUI",
"//submodules/TelegramUI/Components/Stories/StoryContainerScreen", "//submodules/TelegramUI/Components/Stories/StoryContainerScreen",
"//submodules/Components/MultilineTextComponent", "//submodules/Components/MultilineTextComponent",
"//submodules/ActivityIndicator",
"//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/EmojiStatusComponent",
"//submodules/Components/HierarchyTrackingLayer",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -240,10 +240,50 @@ public final class StoryPeerListComponent: Component {
} }
} }
private final class TitleAnimationState {
let duration: Double
let startTime: Double
let fromFraction: CGFloat
let toFraction: CGFloat
let imageView: UIImageView
init(
duration: Double,
startTime: Double,
fromFraction: CGFloat,
toFraction: CGFloat,
imageView: UIImageView
) {
self.duration = duration
self.startTime = startTime
self.fromFraction = fromFraction
self.toFraction = toFraction
self.imageView = imageView
}
func interpolatedFraction(at timestamp: Double, effectiveFromFraction: CGFloat, toFraction: CGFloat) -> CGFloat {
var rawProgress = CGFloat((timestamp - self.startTime) / self.duration)
rawProgress = max(0.0, min(1.0, rawProgress))
let progress = listViewAnimationCurveSystem(rawProgress)
return effectiveFromFraction * (1.0 - progress) + toFraction * progress
}
func isFinished(at timestamp: Double) -> Bool {
if timestamp > self.startTime + self.duration {
return true
} else {
return false
}
}
}
private final class AnimationState { private final class AnimationState {
let duration: Double let duration: Double
let fromIsUnlocked: Bool let fromIsUnlocked: Bool
let fromFraction: CGFloat let fromFraction: CGFloat
let fromTitleWidth: CGFloat
let fromActivityFraction: CGFloat
let startTime: Double let startTime: Double
let bounce: Bool let bounce: Bool
@ -251,12 +291,16 @@ public final class StoryPeerListComponent: Component {
duration: Double, duration: Double,
fromIsUnlocked: Bool, fromIsUnlocked: Bool,
fromFraction: CGFloat, fromFraction: CGFloat,
fromTitleWidth: CGFloat,
fromActivityFraction: CGFloat,
startTime: Double, startTime: Double,
bounce: Bool bounce: Bool
) { ) {
self.duration = duration self.duration = duration
self.fromIsUnlocked = fromIsUnlocked self.fromIsUnlocked = fromIsUnlocked
self.fromFraction = fromFraction self.fromFraction = fromFraction
self.fromTitleWidth = fromTitleWidth
self.fromActivityFraction = fromActivityFraction
self.startTime = startTime self.startTime = startTime
self.bounce = bounce self.bounce = bounce
} }
@ -278,6 +322,14 @@ public final class StoryPeerListComponent: Component {
} }
} }
private struct TitleState: Equatable {
var text: String
init(text: String) {
self.text = text
}
}
public final class View: UIView, UIScrollViewDelegate { public final class View: UIView, UIScrollViewDelegate {
private let collapsedButton: HighlightableButton private let collapsedButton: HighlightableButton
private let scrollView: ScrollView private let scrollView: ScrollView
@ -292,7 +344,13 @@ public final class StoryPeerListComponent: Component {
private var visibleCollapsableItems: [EnginePeer.Id: VisibleItem] = [:] private var visibleCollapsableItems: [EnginePeer.Id: VisibleItem] = [:]
private var titleIndicatorView: ComponentView<Empty>? private var titleIndicatorView: ComponentView<Empty>?
private let titleView = ComponentView<Empty>()
private let titleView: UIImageView
private var titleState: TitleState?
private var titleViewAnimation: TitleAnimationState?
private var disappearingTitleViews: [TitleAnimationState] = []
private var titleIconView: ComponentView<Empty>? private var titleIconView: ComponentView<Empty>?
private var component: StoryPeerListComponent? private var component: StoryPeerListComponent?
@ -308,6 +366,8 @@ public final class StoryPeerListComponent: Component {
private var animator: ConstantDisplayLinkAnimator? private var animator: ConstantDisplayLinkAnimator?
private var currentFraction: CGFloat = 0.0 private var currentFraction: CGFloat = 0.0
private var currentTitleWidth: CGFloat = 0.0
private var currentActivityFraction: CGFloat = 0.0
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.collapsedButton = HighlightableButton() self.collapsedButton = HighlightableButton()
@ -325,6 +385,9 @@ public final class StoryPeerListComponent: Component {
self.scrollContainerView = UIView() self.scrollContainerView = UIView()
self.titleView = UIImageView()
self.titleView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
super.init(frame: frame) super.init(frame: frame)
self.scrollView.delegate = self self.scrollView.delegate = self
@ -333,6 +396,7 @@ public final class StoryPeerListComponent: Component {
self.addSubview(self.scrollView) self.addSubview(self.scrollView)
self.addSubview(self.scrollContainerView) self.addSubview(self.scrollContainerView)
self.addSubview(self.collapsedButton) self.addSubview(self.collapsedButton)
self.addSubview(self.titleView)
self.collapsedButton.highligthedChanged = { [weak self] highlighted in self.collapsedButton.highligthedChanged = { [weak self] highlighted in
guard let self else { guard let self else {
@ -438,41 +502,10 @@ public final class StoryPeerListComponent: Component {
let titleIconSpacing: CGFloat = 4.0 let titleIconSpacing: CGFloat = 4.0
let titleIndicatorSpacing: CGFloat = 8.0 let titleIndicatorSpacing: CGFloat = 8.0
var titleContentWidth: CGFloat = 0.0 var realTitleContentWidth: CGFloat = 0.0
var titleIndicatorSize: CGSize? let titleSize = self.titleView.image?.size ?? CGSize()
if component.titleHasActivity { realTitleContentWidth += titleSize.width
let titleIndicatorView: ComponentView<Empty>
if let current = self.titleIndicatorView {
titleIndicatorView = current
} else {
titleIndicatorView = ComponentView()
self.titleIndicatorView = titleIndicatorView
}
let titleIndicatorSizeValue = titleIndicatorView.update(
transition: .immediate,
component: AnyComponent(TitleActivityIndicatorComponent(
color: component.theme.rootController.navigationBar.accentTextColor
)),
environment: {},
containerSize: CGSize(width: 22.0, height: 22.0)
)
titleIndicatorSize = titleIndicatorSizeValue
titleContentWidth += titleIndicatorSizeValue.width + titleIndicatorSpacing
} else {
if let titleIndicatorView = self.titleIndicatorView {
self.titleIndicatorView = nil
titleIndicatorView.view?.removeFromSuperview()
}
}
let titleSize = self.titleView.update(
transition: .immediate,
component: AnyComponent(Text(text: component.title, font: Font.semibold(17.0), color: component.theme.rootController.navigationBar.primaryTextColor)),
environment: {},
containerSize: CGSize(width: 200.0, height: 100.0)
)
titleContentWidth += titleSize.width
var titleIconSize: CGSize? var titleIconSize: CGSize?
if let peerStatus = component.titlePeerStatus { if let peerStatus = component.titlePeerStatus {
@ -523,12 +556,8 @@ public final class StoryPeerListComponent: Component {
titleIconSize = titleIconSizeValue titleIconSize = titleIconSizeValue
if let titleIconComponentView = titleIconView.view {
titleIconComponentView.isHidden = component.titleHasActivity
}
if !component.titleHasActivity { if !component.titleHasActivity {
titleContentWidth += titleIconSpacing + titleIconSizeValue.width realTitleContentWidth += titleIconSpacing + titleIconSizeValue.width
} }
} else { } else {
if let titleIconView = self.titleIconView { if let titleIconView = self.titleIconView {
@ -552,50 +581,14 @@ public final class StoryPeerListComponent: Component {
collapseStartIndex = 1 collapseStartIndex = 1
} }
let collapsedItemWidth: CGFloat = 24.0
let collapsedItemDistance: CGFloat = 14.0
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItems.count - collapseStartIndex, 3))
var collapsedContentWidth: CGFloat = 0.0
if collapsedItemCount > 0 {
collapsedContentWidth = 1.0 * collapsedItemWidth + (collapsedItemDistance) * max(0.0, collapsedItemCount - 1.0)
}
let collapseEndIndex = collapseStartIndex + max(0, Int(collapsedItemCount) - 1)
var collapsedContentOrigin: CGFloat
let collapsedItemOffsetY: CGFloat
let titleContentSpacing: CGFloat = 8.0
var combinedTitleContentWidth = titleContentWidth
if !combinedTitleContentWidth.isZero {
combinedTitleContentWidth += titleContentSpacing
}
let centralContentWidth: CGFloat
centralContentWidth = collapsedContentWidth + combinedTitleContentWidth
collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5)
if component.titleHasActivity {
collapsedContentOrigin -= (collapsedContentWidth + titleContentSpacing) * 0.5
}
collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0)
var collapsedContentOriginOffset: CGFloat = 0.0
if itemLayout.itemCount == 1 && collapsedContentWidth <= 0.1 {
collapsedContentOriginOffset += 4.0
}
collapsedContentOrigin -= collapsedContentOriginOffset
collapsedItemOffsetY = -59.0
struct CollapseState { struct CollapseState {
var globalFraction: CGFloat var globalFraction: CGFloat
var scaleFraction: CGFloat var scaleFraction: CGFloat
var minFraction: CGFloat var minFraction: CGFloat
var maxFraction: CGFloat var maxFraction: CGFloat
var sideAlphaFraction: CGFloat var sideAlphaFraction: CGFloat
var titleWidth: CGFloat
var activityFraction: CGFloat
} }
let targetExpandedFraction = component.collapseFraction let targetExpandedFraction = component.collapseFraction
@ -619,6 +612,17 @@ public final class StoryPeerListComponent: Component {
targetSideAlphaFraction = 0.0 targetSideAlphaFraction = 0.0
} }
let collapsedItemWidth: CGFloat = 24.0
let collapsedItemDistance: CGFloat = 14.0
let collapsedItemOffsetY: CGFloat = -60.0
let titleContentSpacing: CGFloat = 8.0
let collapsedItemCount: CGFloat = CGFloat(min(self.sortedItems.count - collapseStartIndex, 3))
let targetActivityFraction: CGFloat = component.titleHasActivity ? 1.0 : 0.0
let timestamp = CACurrentMediaTime()
let collapsedState: CollapseState let collapsedState: CollapseState
let expandBoundsFraction: CGFloat let expandBoundsFraction: CGFloat
if let animationState = self.animationState { if let animationState = self.animationState {
@ -646,20 +650,22 @@ public final class StoryPeerListComponent: Component {
effectiveFromSideAlphaFraction = 0.0 effectiveFromSideAlphaFraction = 0.0
} }
let timestamp = CACurrentMediaTime()
let animatedGlobalFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromFraction, toFraction: targetFraction) let animatedGlobalFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromFraction, toFraction: targetFraction)
let animatedScaleFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromScaleFraction, toFraction: targetScaleFraction) let animatedScaleFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromScaleFraction, toFraction: targetScaleFraction)
let animatedMinFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromMinFraction, toFraction: targetMinFraction) let animatedMinFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromMinFraction, toFraction: targetMinFraction)
let animatedMaxFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromMaxFraction, toFraction: targetMaxFraction) let animatedMaxFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromMaxFraction, toFraction: targetMaxFraction)
let animatedSideAlphaFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromSideAlphaFraction, toFraction: targetSideAlphaFraction) let animatedSideAlphaFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: effectiveFromSideAlphaFraction, toFraction: targetSideAlphaFraction)
let animatedTitleWidth = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromTitleWidth, toFraction: realTitleContentWidth)
let animatedActivityFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromActivityFraction, toFraction: targetActivityFraction)
collapsedState = CollapseState( collapsedState = CollapseState(
globalFraction: animatedGlobalFraction, globalFraction: animatedGlobalFraction,
scaleFraction: animatedScaleFraction, scaleFraction: animatedScaleFraction,
minFraction: animatedMinFraction, minFraction: animatedMinFraction,
maxFraction: animatedMaxFraction, maxFraction: animatedMaxFraction,
sideAlphaFraction: animatedSideAlphaFraction sideAlphaFraction: animatedSideAlphaFraction,
titleWidth: animatedTitleWidth,
activityFraction: animatedActivityFraction
) )
var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration) var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration)
@ -676,14 +682,36 @@ public final class StoryPeerListComponent: Component {
scaleFraction: targetScaleFraction, scaleFraction: targetScaleFraction,
minFraction: targetMinFraction, minFraction: targetMinFraction,
maxFraction: targetMaxFraction, maxFraction: targetMaxFraction,
sideAlphaFraction: targetSideAlphaFraction sideAlphaFraction: targetSideAlphaFraction,
titleWidth: realTitleContentWidth,
activityFraction: targetActivityFraction
) )
expandBoundsFraction = 0.0 expandBoundsFraction = 0.0
} }
self.currentFraction = collapsedState.globalFraction var targetCollapsedContentWidth: CGFloat = 0.0
if collapsedItemCount > 0 {
targetCollapsedContentWidth = 1.0 * collapsedItemWidth + (collapsedItemDistance) * max(0.0, collapsedItemCount - 1.0)
}
let activityCollapsedContentWidth: CGFloat = 16.0 + titleIndicatorSpacing
let collapsedContentWidth = activityCollapsedContentWidth * collapsedState.activityFraction + targetCollapsedContentWidth * (1.0 - collapsedState.activityFraction)
component.externalState.collapsedWidth = collapsedContentWidth let collapseEndIndex = collapseStartIndex + max(0, Int(collapsedItemCount) - 1)
var collapsedContentOrigin: CGFloat
let centralContentWidth: CGFloat = collapsedContentWidth + titleContentSpacing + collapsedState.titleWidth
collapsedContentOrigin = floor((itemLayout.containerSize.width - centralContentWidth) * 0.5)
collapsedContentOrigin = min(collapsedContentOrigin, component.maxTitleX - centralContentWidth - 4.0)
let collapsedContentOriginOffset: CGFloat = 0.0
collapsedContentOrigin -= collapsedContentOriginOffset
self.currentFraction = collapsedState.globalFraction
self.currentTitleWidth = collapsedState.titleWidth
self.currentActivityFraction = collapsedState.activityFraction
let effectiveVisibleBounds = self.scrollView.bounds let effectiveVisibleBounds = self.scrollView.bounds
let visibleBounds = effectiveVisibleBounds.insetBy(dx: -200.0, dy: 0.0) let visibleBounds = effectiveVisibleBounds.insetBy(dx: -200.0, dy: 0.0)
@ -698,6 +726,8 @@ public final class StoryPeerListComponent: Component {
} }
} }
let expandedItemWidth: CGFloat = 60.0
struct MeasuredItem { struct MeasuredItem {
var itemFrame: CGRect var itemFrame: CGRect
var itemScale: CGFloat var itemScale: CGFloat
@ -712,10 +742,8 @@ public final class StoryPeerListComponent: Component {
let collapsedItemX: CGFloat let collapsedItemX: CGFloat
if collapseIndex < collapseStartIndex { if collapseIndex < collapseStartIndex {
collapsedItemX = collapsedContentOrigin collapsedItemX = collapsedContentOrigin
} else if collapseIndex > collapseEndIndex {
collapsedItemX = collapsedContentOrigin + CGFloat(collapseEndIndex) * collapsedItemDistance - collapsedItemWidth * 0.5
} else { } else {
collapsedItemX = collapsedContentOrigin + CGFloat(collapseIndex - collapseStartIndex) * collapsedItemDistance collapsedItemX = collapsedContentOrigin + CGFloat(min(collapseIndex - collapseStartIndex, collapseEndIndex - collapseStartIndex)) * collapsedItemDistance * (1.0 - collapsedState.activityFraction) * (1.0 - collapsedState.maxFraction)
} }
let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedItemX, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height)) let collapsedItemFrame = CGRect(origin: CGPoint(x: collapsedItemX, y: regularItemFrame.minY + collapsedItemOffsetY), size: CGSize(width: collapsedItemWidth, height: regularItemFrame.height))
@ -730,12 +758,15 @@ public final class StoryPeerListComponent: Component {
collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0 collapsedMaxItemFrame.origin.y += collapsedState.minFraction * 10.0
} }
let minimizedItemScale: CGFloat = 24.0 / 52.0 let minimizedDefaultItemScale: CGFloat = 24.0 / 52.0
let minimizedItemScale = minimizedDefaultItemScale
let minimizedMaxItemScale: CGFloat = (24.0 + 4.0) / 52.0 let minimizedMaxItemScale: CGFloat = (24.0 + 4.0) / 52.0
let maximizedItemScale: CGFloat = 1.0 let maximizedItemScale: CGFloat = 1.0
let minItemScale = minimizedItemScale.interpolate(to: minimizedMaxItemScale, amount: collapsedState.minFraction) let minItemScale: CGFloat = minimizedItemScale.interpolate(to: minimizedMaxItemScale, amount: collapsedState.minFraction) * (1.0 - collapsedState.activityFraction) + 0.1 * collapsedState.activityFraction
let itemScale: CGFloat = minItemScale.interpolate(to: maximizedItemScale, amount: collapsedState.maxFraction) let itemScale: CGFloat = minItemScale.interpolate(to: maximizedItemScale, amount: collapsedState.maxFraction)
let itemFrame: CGRect let itemFrame: CGRect
@ -787,7 +818,8 @@ public final class StoryPeerListComponent: Component {
continue continue
} }
let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame) //let isReallyVisible = effectiveVisibleBounds.intersects(regularItemFrame)
//let _ = isReallyVisible
validIds.append(itemSet.peer.id) validIds.append(itemSet.peer.id)
@ -845,11 +877,7 @@ public final class StoryPeerListComponent: Component {
rightItemFrame = calculateItem(i + 1).itemFrame rightItemFrame = calculateItem(i + 1).itemFrame
} }
if effectiveFirstVisibleIndex == 0 && !component.titleHasActivity { itemAlpha = collapsedState.sideAlphaFraction * 1.0 + (1.0 - collapsedState.sideAlphaFraction) * (1.0 - collapsedState.activityFraction)
itemAlpha = 1.0
} else {
itemAlpha = collapsedState.sideAlphaFraction
}
} else { } else {
if itemLayout.itemCount == 1 { if itemLayout.itemCount == 1 {
itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0) itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0)
@ -879,9 +907,8 @@ public final class StoryPeerListComponent: Component {
hasUnseenCloseFriendsItems: hasUnseenCloseFriendsItems, hasUnseenCloseFriendsItems: hasUnseenCloseFriendsItems,
hasItems: hasItems, hasItems: hasItems,
ringAnimation: itemRingAnimation, ringAnimation: itemRingAnimation,
collapseFraction: isReallyVisible ? (1.0 - collapsedState.maxFraction) : 0.0,
scale: itemScale, scale: itemScale,
collapsedWidth: collapsedItemWidth, fullWidth: expandedItemWidth,
expandedAlphaFraction: collapsedState.sideAlphaFraction, expandedAlphaFraction: collapsedState.sideAlphaFraction,
leftNeighborDistance: leftNeighborDistance, leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance, rightNeighborDistance: rightNeighborDistance,
@ -967,11 +994,7 @@ public final class StoryPeerListComponent: Component {
var itemAlpha: CGFloat = 1.0 var itemAlpha: CGFloat = 1.0
var isCollapsable: Bool = false var isCollapsable: Bool = false
var itemScale = measuredItem.itemScale let itemScale = measuredItem.itemScale
if itemLayout.itemCount == 1 {
let singleScaleFactor = min(1.0, collapsedState.minFraction + collapsedState.maxFraction)
itemScale = 0.001 * (1.0 - singleScaleFactor) + itemScale * singleScaleFactor
}
if i >= collapseStartIndex && i <= collapseEndIndex { if i >= collapseStartIndex && i <= collapseEndIndex {
isCollapsable = true isCollapsable = true
@ -983,21 +1006,9 @@ public final class StoryPeerListComponent: Component {
rightItemFrame = calculateItem(collapseIndex + 1).itemFrame rightItemFrame = calculateItem(collapseIndex + 1).itemFrame
} }
if effectiveFirstVisibleIndex == 0 { itemAlpha = (1.0 - collapsedState.sideAlphaFraction) * (1.0 - collapsedState.activityFraction)
itemAlpha = 0.0
} else {
itemAlpha = 1.0 - collapsedState.sideAlphaFraction
}
} else { } else {
if itemLayout.itemCount == 1 { itemAlpha = collapsedState.sideAlphaFraction
itemAlpha = min(1.0, (collapsedState.minFraction + collapsedState.maxFraction) * 4.0)
} else {
itemAlpha = collapsedState.sideAlphaFraction
}
}
if component.titleHasActivity {
itemAlpha = 0.0
} }
var leftNeighborDistance: CGPoint? var leftNeighborDistance: CGPoint?
@ -1021,9 +1032,8 @@ public final class StoryPeerListComponent: Component {
hasUnseenCloseFriendsItems: hasUnseenCloseFriendsItems, hasUnseenCloseFriendsItems: hasUnseenCloseFriendsItems,
hasItems: hasItems, hasItems: hasItems,
ringAnimation: itemRingAnimation, ringAnimation: itemRingAnimation,
collapseFraction: 1.0 - collapsedState.maxFraction,
scale: itemScale, scale: itemScale,
collapsedWidth: collapsedItemWidth, fullWidth: expandedItemWidth,
expandedAlphaFraction: collapsedState.sideAlphaFraction, expandedAlphaFraction: collapsedState.sideAlphaFraction,
leftNeighborDistance: leftNeighborDistance, leftNeighborDistance: leftNeighborDistance,
rightNeighborDistance: rightNeighborDistance, rightNeighborDistance: rightNeighborDistance,
@ -1093,46 +1103,110 @@ public final class StoryPeerListComponent: Component {
let defaultCollapsedTitleOffset: CGFloat = 0.0 let defaultCollapsedTitleOffset: CGFloat = 0.0
var targetCollapsedTitleOffset: CGFloat = collapsedContentOrigin + collapsedContentOriginOffset + collapsedContentWidth + titleContentSpacing let targetCollapsedTitleOffset: CGFloat = collapsedContentOrigin + collapsedContentOriginOffset + collapsedContentWidth + titleContentSpacing
if itemLayout.itemCount == 1 && collapsedContentWidth <= 0.1 {
let singleScaleFactor = min(1.0, collapsedState.minFraction)
targetCollapsedTitleOffset += singleScaleFactor * 4.0
}
let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset let collapsedTitleOffset = targetCollapsedTitleOffset - defaultCollapsedTitleOffset
let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction) let titleMinContentOffset: CGFloat = collapsedTitleOffset.interpolate(to: collapsedTitleOffset + 12.0, amount: collapsedState.minFraction * (1.0 - collapsedState.activityFraction))
var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: floor((itemLayout.containerSize.width - titleContentWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction) var titleContentOffset: CGFloat = titleMinContentOffset.interpolate(to: floor((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: collapsedState.maxFraction * (1.0 - collapsedState.activityFraction))
var titleIndicatorSize: CGSize?
if collapsedState.activityFraction != 0.0 {
let collapsedItemMinX = collapsedContentOrigin - collapsedItemWidth * 0.5
let collapsedItemMaxX = collapsedContentOrigin + CGFloat(collapseEndIndex - collapseStartIndex) * collapsedItemDistance * (1.0 - collapsedState.activityFraction) * (1.0 - collapsedState.sideAlphaFraction) + collapsedItemWidth * 0.5
let collapsedContentWidth = max(collapsedItemWidth, collapsedItemMaxX - collapsedItemMinX)
let titleIndicatorView: ComponentView<Empty>
if let current = self.titleIndicatorView {
titleIndicatorView = current
} else {
titleIndicatorView = ComponentView()
self.titleIndicatorView = titleIndicatorView
}
let titleIndicatorSizeValue = titleIndicatorView.update(
transition: .immediate,
component: AnyComponent(TitleActivityIndicatorComponent(
color: component.theme.rootController.navigationBar.accentTextColor
)),
environment: {},
containerSize: CGSize(width: collapsedContentWidth - 2.0, height: collapsedItemWidth - 2.0)
)
titleIndicatorSize = titleIndicatorSizeValue
} else if let titleIndicatorView = self.titleIndicatorView {
self.titleIndicatorView = nil
titleIndicatorView.view?.removeFromSuperview()
}
if let titleIndicatorSize, let titleIndicatorView = self.titleIndicatorView?.view { if let titleIndicatorSize, let titleIndicatorView = self.titleIndicatorView?.view {
let titleIndicatorFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleIndicatorSize.height) * 0.5)), size: titleIndicatorSize) let titleIndicatorFrame = CGRect(origin: CGPoint(x: titleContentOffset - titleIndicatorSize.width - 9.0, y: collapsedItemOffsetY + 2.0 + floor((56.0 - titleIndicatorSize.height) * 0.5)), size: titleIndicatorSize)
if titleIndicatorView.superview == nil { if titleIndicatorView.superview == nil {
self.addSubview(titleIndicatorView) self.addSubview(titleIndicatorView)
} }
titleIndicatorView.frame = titleIndicatorFrame titleIndicatorView.center = titleIndicatorFrame.center
titleContentOffset += titleIndicatorSize.width + titleIndicatorSpacing titleIndicatorView.bounds = CGRect(origin: CGPoint(), size: titleIndicatorFrame.size)
var indicatorMinScale: CGFloat = collapsedState.sideAlphaFraction * 0.1 + (1.0 - collapsedState.sideAlphaFraction) * 1.0
if collapsedItemCount == 0 {
indicatorMinScale = 0.1
}
let indicatorScale: CGFloat = collapsedState.activityFraction * 1.0 + (1.0 - collapsedState.activityFraction) * indicatorMinScale
let indicatorAlpha: CGFloat = collapsedState.activityFraction
titleIndicatorView.layer.transform = CATransform3DMakeScale(indicatorScale, indicatorScale, 1.0)
titleIndicatorView.alpha = indicatorAlpha
} }
let titleFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleSize.height) * 0.5)), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 2.0 + floor((56.0 - titleSize.height) * 0.5)), size: titleSize)
if let titleComponentView = self.titleView.view { if let image = self.titleView.image {
if titleComponentView.superview == nil { self.titleView.center = CGPoint(x: titleFrame.minX, y: titleFrame.midY)
titleComponentView.isUserInteractionEnabled = false self.titleView.bounds = CGRect(origin: CGPoint(), size: image.size)
self.addSubview(titleComponentView)
let titleFraction: CGFloat
if let titleViewAnimation = self.titleViewAnimation {
titleFraction = titleViewAnimation.interpolatedFraction(at: timestamp, effectiveFromFraction: titleViewAnimation.fromFraction, toFraction: titleViewAnimation.toFraction)
} else {
titleFraction = 1.0
}
self.titleView.alpha = titleFraction
let titleScale: CGFloat = titleFraction * 1.0 + (1.0 - titleFraction) * 0.3
self.titleView.layer.transform = CATransform3DMakeScale(titleScale, titleScale, 1.0)
}
for disappearingTitleView in self.disappearingTitleViews {
if let image = disappearingTitleView.imageView.image {
disappearingTitleView.imageView.center = CGPoint(x: titleFrame.minX, y: titleFrame.midY)
disappearingTitleView.imageView.bounds = CGRect(origin: CGPoint(), size: image.size)
let titleFraction = disappearingTitleView.interpolatedFraction(at: timestamp, effectiveFromFraction: disappearingTitleView.fromFraction, toFraction: disappearingTitleView.toFraction)
disappearingTitleView.imageView.alpha = titleFraction
let titleScale: CGFloat = titleFraction * 1.0 + (1.0 - titleFraction) * 0.3
disappearingTitleView.imageView.layer.transform = CATransform3DMakeScale(titleScale, titleScale, 1.0)
} }
titleComponentView.frame = titleFrame
} }
titleContentOffset += titleSize.width
if let titleIconSize, let titleIconView = self.titleIconView?.view { if let titleIconSize, let titleIconView = self.titleIconView?.view {
titleContentOffset += titleIconSpacing titleContentOffset += titleIconSpacing
let titleIconFrame = CGRect(origin: CGPoint(x: titleContentOffset, y: collapsedItemOffsetY + 1.0 + floor((56.0 - titleIconSize.height) * 0.5)), size: titleIconSize) let titleIconFrame = CGRect(origin: CGPoint(x: titleContentOffset + titleIconSpacing + (collapsedState.titleWidth - (titleIconSpacing + titleIconSize.width)) * (1.0 - collapsedState.activityFraction), y: collapsedItemOffsetY + 2.0 + floor((56.0 - titleIconSize.height) * 0.5)), size: titleIconSize)
if titleIconView.superview == nil { if titleIconView.superview == nil {
self.addSubview(titleIconView) self.addSubview(titleIconView)
} }
titleIconView.frame = titleIconFrame titleIconView.center = titleIconFrame.center
titleIconView.bounds = CGRect(origin: CGPoint(), size: titleIconFrame.size)
let titleIconFraction = 1.0 - collapsedState.activityFraction
let titleIconAlpha: CGFloat = titleIconFraction
let titleIconScale: CGFloat = titleIconFraction * 1.0 + (1.0 - titleIconFraction) * 0.1
titleIconView.alpha = titleIconAlpha
titleIconView.layer.transform = CATransform3DMakeScale(titleIconScale, titleIconScale, 1.0)
} }
titleContentOffset += collapsedState.titleWidth
} }
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
@ -1179,7 +1253,10 @@ public final class StoryPeerListComponent: Component {
useAnimation = true useAnimation = true
} else if let animationHint, animationHint.allowAvatarsExpansionUpdated { } else if let animationHint, animationHint.allowAvatarsExpansionUpdated {
useAnimation = true useAnimation = true
} else if let previousComponent = self.component, (component.title != previousComponent.title || component.titleHasActivity != previousComponent.titleHasActivity) {
useAnimation = true
} }
if let animationHint, animationHint.disableAnimations { if let animationHint, animationHint.disableAnimations {
useAnimation = false useAnimation = false
self.animationState = nil self.animationState = nil
@ -1195,7 +1272,48 @@ public final class StoryPeerListComponent: Component {
} else { } else {
duration = 0.25 duration = 0.25
} }
self.animationState = AnimationState(duration: duration * UIView.animationDurationFactor(), fromIsUnlocked: previousComponent.unlocked, fromFraction: self.currentFraction, startTime: timestamp, bounce: animationHint?.bounce ?? true)
if useAnimation, let previousComponent = self.component, component.title != previousComponent.title, self.titleView.image != nil {
var fromFraction: CGFloat = 1.0
if let titleViewAnimation = self.titleViewAnimation {
fromFraction = titleViewAnimation.interpolatedFraction(
at: timestamp,
effectiveFromFraction: titleViewAnimation.fromFraction,
toFraction: titleViewAnimation.toFraction
)
}
if let previousImage = self.titleView.image {
let previousImageView = UIImageView(image: previousImage)
previousImageView.layer.anchorPoint = self.titleView.layer.anchorPoint
self.disappearingTitleViews.append(TitleAnimationState(
duration: duration,
startTime: timestamp,
fromFraction: fromFraction,
toFraction: 0.0,
imageView: previousImageView
))
self.insertSubview(previousImageView, belowSubview: self.titleView)
}
self.titleViewAnimation = TitleAnimationState(
duration: duration * UIView.animationDurationFactor(),
startTime: timestamp,
fromFraction: 0.0,
toFraction: 1.0,
imageView: self.titleView
)
}
self.animationState = AnimationState(
duration: duration * UIView.animationDurationFactor(),
fromIsUnlocked: previousComponent.unlocked,
fromFraction: self.currentFraction,
fromTitleWidth: self.currentTitleWidth,
fromActivityFraction: self.currentActivityFraction,
startTime: timestamp,
bounce: animationHint?.bounce ?? true
)
} }
if let animationState = self.animationState { if let animationState = self.animationState {
@ -1204,7 +1322,20 @@ public final class StoryPeerListComponent: Component {
} }
} }
if let _ = self.animationState { if let titleViewAnimation = self.titleViewAnimation {
if titleViewAnimation.isFinished(at: timestamp) {
self.titleViewAnimation = nil
}
}
for i in (0 ..< self.disappearingTitleViews.count).reversed() {
if self.disappearingTitleViews[i].isFinished(at: timestamp) {
self.disappearingTitleViews[i].imageView.removeFromSuperview()
self.disappearingTitleViews.remove(at: i)
}
}
if self.animationState != nil || self.titleViewAnimation != nil || !self.disappearingTitleViews.isEmpty {
if self.animator == nil { if self.animator == nil {
let animator = ConstantDisplayLinkAnimator(update: { [weak self] in let animator = ConstantDisplayLinkAnimator(update: { [weak self] in
guard let self else { guard let self else {
@ -1223,6 +1354,27 @@ public final class StoryPeerListComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
let updatedTitleState = TitleState(text: component.title)
if self.titleState != updatedTitleState {
self.titleState = updatedTitleState
let attributedText = NSAttributedString(string: updatedTitleState.text, attributes: [
NSAttributedString.Key.font: Font.semibold(17.0),
NSAttributedString.Key.foregroundColor: component.theme.rootController.navigationBar.primaryTextColor
])
var boundingRect = attributedText.boundingRect(with: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX - 30.0), height: 100.0), options: .usesLineFragmentOrigin, context: nil)
boundingRect.size.width = ceil(boundingRect.size.width)
boundingRect.size.height = ceil(boundingRect.size.height)
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: boundingRect.size))
let image = renderer.image { context in
UIGraphicsPushContext(context.cgContext)
attributedText.draw(at: CGPoint())
UIGraphicsPopContext()
}
self.titleView.image = image
}
if let storySubscriptions = component.storySubscriptions, let hasMoreToken = storySubscriptions.hasMoreToken { if let storySubscriptions = component.storySubscriptions, let hasMoreToken = storySubscriptions.hasMoreToken {
if self.requestedLoadMoreToken != hasMoreToken { if self.requestedLoadMoreToken != hasMoreToken {
self.requestedLoadMoreToken = hasMoreToken self.requestedLoadMoreToken = hasMoreToken

View File

@ -269,9 +269,8 @@ public final class StoryPeerListItemComponent: Component {
public let hasUnseenCloseFriendsItems: Bool public let hasUnseenCloseFriendsItems: Bool
public let hasItems: Bool public let hasItems: Bool
public let ringAnimation: RingAnimation? public let ringAnimation: RingAnimation?
public let collapseFraction: CGFloat
public let scale: CGFloat public let scale: CGFloat
public let collapsedWidth: CGFloat public let fullWidth: CGFloat
public let expandedAlphaFraction: CGFloat public let expandedAlphaFraction: CGFloat
public let leftNeighborDistance: CGPoint? public let leftNeighborDistance: CGPoint?
public let rightNeighborDistance: CGPoint? public let rightNeighborDistance: CGPoint?
@ -287,9 +286,8 @@ public final class StoryPeerListItemComponent: Component {
hasUnseenCloseFriendsItems: Bool, hasUnseenCloseFriendsItems: Bool,
hasItems: Bool, hasItems: Bool,
ringAnimation: RingAnimation?, ringAnimation: RingAnimation?,
collapseFraction: CGFloat,
scale: CGFloat, scale: CGFloat,
collapsedWidth: CGFloat, fullWidth: CGFloat,
expandedAlphaFraction: CGFloat, expandedAlphaFraction: CGFloat,
leftNeighborDistance: CGPoint?, leftNeighborDistance: CGPoint?,
rightNeighborDistance: CGPoint?, rightNeighborDistance: CGPoint?,
@ -304,9 +302,8 @@ public final class StoryPeerListItemComponent: Component {
self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems self.hasUnseenCloseFriendsItems = hasUnseenCloseFriendsItems
self.hasItems = hasItems self.hasItems = hasItems
self.ringAnimation = ringAnimation self.ringAnimation = ringAnimation
self.collapseFraction = collapseFraction
self.scale = scale self.scale = scale
self.collapsedWidth = collapsedWidth self.fullWidth = fullWidth
self.expandedAlphaFraction = expandedAlphaFraction self.expandedAlphaFraction = expandedAlphaFraction
self.leftNeighborDistance = leftNeighborDistance self.leftNeighborDistance = leftNeighborDistance
self.rightNeighborDistance = rightNeighborDistance self.rightNeighborDistance = rightNeighborDistance
@ -339,13 +336,10 @@ public final class StoryPeerListItemComponent: Component {
if lhs.ringAnimation != rhs.ringAnimation { if lhs.ringAnimation != rhs.ringAnimation {
return false return false
} }
if lhs.collapseFraction != rhs.collapseFraction {
return false
}
if lhs.scale != rhs.scale { if lhs.scale != rhs.scale {
return false return false
} }
if lhs.collapsedWidth != rhs.collapsedWidth { if lhs.fullWidth != rhs.fullWidth {
return false return false
} }
if lhs.expandedAlphaFraction != rhs.expandedAlphaFraction { if lhs.expandedAlphaFraction != rhs.expandedAlphaFraction {
@ -526,7 +520,7 @@ public final class StoryPeerListItemComponent: Component {
self.component = component self.component = component
self.componentState = state self.componentState = state
let effectiveWidth: CGFloat = (1.0 - component.collapseFraction) * availableSize.width + component.collapseFraction * component.collapsedWidth let effectiveWidth: CGFloat = component.scale * component.fullWidth
let effectiveScale: CGFloat = component.scale let effectiveScale: CGFloat = component.scale
@ -571,7 +565,7 @@ public final class StoryPeerListItemComponent: Component {
} else { } else {
baseLineWidth = 1.0 + UIScreenPixel baseLineWidth = 1.0 + UIScreenPixel
} }
let indicatorLineWidth: CGFloat = baseLineWidth * (1.0 - component.collapseFraction) + minimizedLineWidth * component.collapseFraction let indicatorLineWidth: CGFloat = baseLineWidth * component.scale + minimizedLineWidth * (1.0 - component.scale)
avatarNode.setPeer( avatarNode.setPeer(
context: component.context, context: component.context,
@ -643,7 +637,7 @@ public final class StoryPeerListItemComponent: Component {
let baseRadius: CGFloat = 30.0 let baseRadius: CGFloat = 30.0
let collapsedRadius: CGFloat = 32.0 let collapsedRadius: CGFloat = 32.0
let indicatorRadius: CGFloat = baseRadius * (1.0 - component.collapseFraction) + collapsedRadius * component.collapseFraction let indicatorRadius: CGFloat = baseRadius * component.scale + collapsedRadius * (1.0 - component.scale)
self.indicatorShapeLayer.lineWidth = indicatorLineWidth self.indicatorShapeLayer.lineWidth = indicatorLineWidth

View File

@ -2,7 +2,7 @@ import Foundation
import UIKit import UIKit
import Display import Display
import ComponentFlow import ComponentFlow
import ActivityIndicator import HierarchyTrackingLayer
public final class TitleActivityIndicatorComponent: Component { public final class TitleActivityIndicatorComponent: Component {
let color: UIColor let color: UIColor
@ -21,10 +21,38 @@ public final class TitleActivityIndicatorComponent: Component {
} }
public final class View: UIView { public final class View: UIView {
private var activityIndicator: ActivityIndicator? private let shapeLayer: SimpleShapeLayer
private let hierarchyTrackingLayer: HierarchyTrackingLayer
private var component: TitleActivityIndicatorComponent?
private var animator: ConstantDisplayLinkAnimator?
private var animationPhase: CGFloat = 0.0
public override init(frame: CGRect) { public override init(frame: CGRect) {
self.shapeLayer = SimpleShapeLayer()
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
super.init(frame: frame) super.init(frame: frame)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didExitHierarchy = { [weak self] in
guard let self else {
return
}
self.refreshAnimation()
}
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let self else {
return
}
self.refreshAnimation()
}
self.layer.addSublayer(self.shapeLayer)
self.shapeLayer.lineCap = .round
self.shapeLayer.lineWidth = 1.5
self.shapeLayer.fillColor = nil
} }
required public init?(coder: NSCoder) { required public init?(coder: NSCoder) {
@ -34,18 +62,154 @@ public final class TitleActivityIndicatorComponent: Component {
deinit { deinit {
} }
func update(component: TitleActivityIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize { private func refreshAnimation() {
let activityIndicator: ActivityIndicator if self.hierarchyTrackingLayer.isInHierarchy {
if let current = self.activityIndicator { if self.animator == nil {
activityIndicator = current let animationStartTime = CACurrentMediaTime()
self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in
guard let self else {
return
}
let duration: Double = 0.5
self.animationPhase = (CACurrentMediaTime() - animationStartTime).truncatingRemainder(dividingBy: duration) / duration
self.updateAnimation()
})
self.animator?.isPaused = false
}
} else { } else {
activityIndicator = ActivityIndicator(type: .custom(component.color, availableSize.width, 2.0, true)) if let animator = self.animator {
self.activityIndicator = activityIndicator self.animator = nil
self.addSubview(activityIndicator.view) animator.invalidate()
}
}
}
private func updateAnimation() {
let size = self.shapeLayer.bounds
let path = CGMutablePath()
let radius = size.height * 0.5
enum Segment {
case line(start: CGPoint, end: CGPoint)
case halfCircle(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat)
func length(radius: CGFloat) -> CGFloat {
switch self {
case let .line(start, end):
return abs(start.x - end.x)
case let .halfCircle(_, radius, startAngle, endAngle):
return (endAngle - startAngle) * radius
}
}
func addPath(into path: CGMutablePath, fromFraction: CGFloat, toFraction: CGFloat) {
switch self {
case let .line(start, end):
if fromFraction != 0.0 {
path.move(to: CGPoint(
x: start.x * (1.0 - fromFraction) + end.x * fromFraction,
y: start.y * (1.0 - fromFraction) + end.y * fromFraction
))
}
path.addLine(to: CGPoint(
x: start.x * (1.0 - toFraction) + end.x * toFraction,
y: start.y * (1.0 - toFraction) + end.y * toFraction
))
case let .halfCircle(center, radius, startAngle, endAngle):
path.addArc(center: center, radius: radius, startAngle: startAngle + fromFraction * (endAngle - startAngle), endAngle: startAngle + toFraction * (endAngle - startAngle), clockwise: false)
}
}
} }
activityIndicator.frame = CGRect(origin: CGPoint(), size: availableSize) let segments: [Segment] = [
activityIndicator.isHidden = false .halfCircle(center: CGPoint(x: size.width - radius, y: radius), radius: radius, startAngle: -CGFloat.pi * 0.5, endAngle: CGFloat.pi * 0.5),
.line(start: CGPoint(x: size.width - radius, y: size.height), end: CGPoint(x: radius, y: size.height)),
.halfCircle(center: CGPoint(x: radius, y: radius), radius: radius, startAngle: CGFloat.pi * 0.5, endAngle: CGFloat.pi * 1.5),
.line(start: CGPoint(x: radius, y: 0.0), end: CGPoint(x: size.width - radius, y: 0.0)),
]
var totalLength: CGFloat = 0.0
for segment in segments {
totalLength += segment.length(radius: radius)
}
let startOffset: CGFloat = self.animationPhase
let endOffset: CGFloat = startOffset + 0.8
var startLength = startOffset * totalLength
var startSegment: (Int, CGFloat)?
while startSegment == nil {
for i in 0 ..< segments.count {
let segment = segments[i]
let segmentLength = segment.length(radius: radius)
if segmentLength <= startLength {
startLength -= segmentLength
} else {
let subOffset = startLength
startSegment = (i, subOffset)
break
}
}
}
var isFirst = true
var pathLength = (endOffset - startOffset) * totalLength
if let (startIndex, startOffset) = startSegment {
var index = startIndex
while pathLength > 0.0 {
let segment = segments[index]
let segmentOffset: CGFloat = isFirst ? startOffset : 0.0
let segmentLength = segment.length(radius: radius)
let segmentSubLength = segmentLength - segmentOffset
if segmentSubLength > 0.0 {
//remainingLength <= segmentRemainingLength -> take remainingLength
//remainingLength > segmentRemainingLength -> take segmentRemainingLength
let pathPart: CGFloat
if pathLength <= segmentSubLength {
pathPart = pathLength
} else {
pathPart = segmentSubLength
}
pathLength -= pathPart
segment.addPath(into: path, fromFraction: segmentOffset / segmentLength, toFraction: (segmentOffset + pathPart) / segmentLength)
}
index = (index + 1) % segments.count
isFirst = false
}
}
/*for segment in segments {
segment.addPath(into: path, fromFraction: 0.0, toFraction: 1.0)
}*/
if let currentPath = self.shapeLayer.path {
if currentPath != path {
self.shapeLayer.path = path
}
} else {
self.shapeLayer.path = path
}
}
func update(component: TitleActivityIndicatorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let isFirstTime = self.component == nil
self.component = component
let _ = isFirstTime
transition.setFrame(layer: self.shapeLayer, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setShapeLayerPath(layer: self.shapeLayer, path: UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: availableSize), cornerRadius: availableSize.height * 0.5).cgPath)
self.shapeLayer.strokeColor = component.color.cgColor
self.refreshAnimation()
self.updateAnimation()
return availableSize return availableSize
} }