mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Story view list animations
This commit is contained in:
parent
c88558ead8
commit
b6fe5dcc40
@ -288,3 +288,247 @@ public final class ImmediateAnimatedCountLabelNode: AnimatedCountLabelNode {
|
|||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AnimatedCountLabelView: UIView {
|
||||||
|
public struct Layout {
|
||||||
|
public var size: CGSize
|
||||||
|
public var isTruncated: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Segment: Equatable {
|
||||||
|
case number(Int, NSAttributedString)
|
||||||
|
case text(Int, NSAttributedString)
|
||||||
|
|
||||||
|
public static func ==(lhs: Segment, rhs: Segment) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .number(number, text):
|
||||||
|
if case let .number(rhsNumber, rhsText) = rhs, number == rhsNumber, text.isEqual(to: rhsText) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .text(index, text):
|
||||||
|
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate enum ResolvedSegment: Equatable {
|
||||||
|
public enum Key: Hashable {
|
||||||
|
case number(Int)
|
||||||
|
case text(Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
case number(id: Int, value: Int, string: NSAttributedString)
|
||||||
|
case text(id: Int, string: NSAttributedString)
|
||||||
|
|
||||||
|
public static func ==(lhs: ResolvedSegment, rhs: ResolvedSegment) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .number(id, number, text):
|
||||||
|
if case let .number(rhsId, rhsNumber, rhsText) = rhs, id == rhsId, number == rhsNumber, text.isEqual(to: rhsText) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .text(index, text):
|
||||||
|
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var attributedText: NSAttributedString {
|
||||||
|
switch self {
|
||||||
|
case let .number(_, _, text):
|
||||||
|
return text
|
||||||
|
case let .text(_, text):
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var key: Key {
|
||||||
|
switch self {
|
||||||
|
case let .number(id, _, _):
|
||||||
|
return .number(id)
|
||||||
|
case let .text(index, _):
|
||||||
|
return .text(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate var resolvedSegments: [ResolvedSegment.Key: (ResolvedSegment, TextNode)] = [:]
|
||||||
|
|
||||||
|
public var reverseAnimationDirection: Bool = false
|
||||||
|
public var alwaysOneDirection: Bool = false
|
||||||
|
|
||||||
|
override public init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func update(size: CGSize, segments initialSegments: [Segment], transition: ContainedViewLayoutTransition) -> Layout {
|
||||||
|
var segmentLayouts: [ResolvedSegment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:]
|
||||||
|
let wasEmpty = self.resolvedSegments.isEmpty
|
||||||
|
for (segmentKey, segmentAndTextNode) in self.resolvedSegments {
|
||||||
|
segmentLayouts[segmentKey] = TextNode.asyncLayout(segmentAndTextNode.1)
|
||||||
|
}
|
||||||
|
let reverseAnimationDirection = self.reverseAnimationDirection
|
||||||
|
let alwaysOneDirection = self.alwaysOneDirection
|
||||||
|
|
||||||
|
var segments: [ResolvedSegment] = []
|
||||||
|
loop: for segment in initialSegments {
|
||||||
|
switch segment {
|
||||||
|
case let .number(value, string):
|
||||||
|
if string.string.isEmpty {
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
let attributes = string.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: 1))
|
||||||
|
|
||||||
|
var remainingValue = value
|
||||||
|
|
||||||
|
let insertPosition = segments.count
|
||||||
|
|
||||||
|
while true {
|
||||||
|
let digitValue = remainingValue % 10
|
||||||
|
|
||||||
|
segments.insert(.number(id: 1000 - segments.count, value: value, string: NSAttributedString(string: "\(digitValue)", attributes: attributes)), at: insertPosition)
|
||||||
|
remainingValue /= 10
|
||||||
|
if remainingValue == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case let .text(id, string):
|
||||||
|
segments.append(.text(id: id, string: string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for segment in segments {
|
||||||
|
if segmentLayouts[segment.key] == nil {
|
||||||
|
segmentLayouts[segment.key] = TextNode.asyncLayout(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentSize = CGSize()
|
||||||
|
var remainingSize = size
|
||||||
|
|
||||||
|
var calculatedSegments: [ResolvedSegment.Key: (TextNodeLayout, CGFloat, () -> TextNode)] = [:]
|
||||||
|
var isTruncated = false
|
||||||
|
|
||||||
|
var validKeys: [ResolvedSegment.Key] = []
|
||||||
|
|
||||||
|
for segment in segments {
|
||||||
|
validKeys.append(segment.key)
|
||||||
|
let (layout, apply) = segmentLayouts[segment.key]!(TextNodeLayoutArguments(attributedString: segment.attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: remainingSize, alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil))
|
||||||
|
var effectiveSegmentWidth = layout.size.width
|
||||||
|
if case .number = segment {
|
||||||
|
//effectiveSegmentWidth = ceil(effectiveSegmentWidth / 2.0) * 2.0
|
||||||
|
} else if segment.attributedText.string == " " {
|
||||||
|
effectiveSegmentWidth = max(effectiveSegmentWidth, 4.0)
|
||||||
|
}
|
||||||
|
calculatedSegments[segment.key] = (layout, effectiveSegmentWidth, apply)
|
||||||
|
contentSize.width += effectiveSegmentWidth
|
||||||
|
contentSize.height = max(contentSize.height, layout.size.height)
|
||||||
|
remainingSize.width = max(0.0, remainingSize.width - layout.size.width)
|
||||||
|
if layout.truncated {
|
||||||
|
isTruncated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var transition = transition
|
||||||
|
if wasEmpty {
|
||||||
|
transition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentOffset = CGPoint()
|
||||||
|
for segment in segments {
|
||||||
|
var animation: (CGFloat, Double)?
|
||||||
|
if let (currentSegment, currentTextNode) = self.resolvedSegments[segment.key] {
|
||||||
|
if case let .number(_, currentValue, currentString) = currentSegment, case let .number(_, updatedValue, updatedString) = segment, transition.isAnimated, !wasEmpty, currentValue != updatedValue, currentString.string != updatedString.string, let snapshot = currentTextNode.layer.snapshotContentTree() {
|
||||||
|
var fromAlpha: CGFloat = 1.0
|
||||||
|
if let presentation = currentTextNode.layer.presentation() {
|
||||||
|
fromAlpha = CGFloat(presentation.opacity)
|
||||||
|
}
|
||||||
|
var offsetY: CGFloat
|
||||||
|
if currentValue > updatedValue || alwaysOneDirection {
|
||||||
|
offsetY = -floor(currentTextNode.bounds.height * 0.6)
|
||||||
|
} else {
|
||||||
|
offsetY = floor(currentTextNode.bounds.height * 0.6)
|
||||||
|
}
|
||||||
|
if reverseAnimationDirection {
|
||||||
|
offsetY = -offsetY
|
||||||
|
}
|
||||||
|
animation = (-offsetY, 0.2)
|
||||||
|
snapshot.frame = currentTextNode.frame
|
||||||
|
self.layer.addSublayer(snapshot)
|
||||||
|
snapshot.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offsetY), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||||
|
snapshot.animateScale(from: 1.0, to: 0.3, duration: 0.2, removeOnCompletion: false)
|
||||||
|
snapshot.animateAlpha(from: fromAlpha, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshot] _ in
|
||||||
|
snapshot?.removeFromSuperlayer()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (layout, effectiveSegmentWidth, apply) = calculatedSegments[segment.key]!
|
||||||
|
let textNode = apply()
|
||||||
|
let textFrame = CGRect(origin: currentOffset, size: layout.size)
|
||||||
|
if textNode.frame.isEmpty {
|
||||||
|
textNode.frame = textFrame
|
||||||
|
if transition.isAnimated, !wasEmpty, animation == nil {
|
||||||
|
textNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||||
|
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
} else if textNode.frame != textFrame {
|
||||||
|
transition.updateFrameAdditive(node: textNode, frame: textFrame)
|
||||||
|
}
|
||||||
|
currentOffset.x += effectiveSegmentWidth
|
||||||
|
if let (_, currentTextNode) = self.resolvedSegments[segment.key] {
|
||||||
|
if currentTextNode !== textNode {
|
||||||
|
currentTextNode.removeFromSupernode()
|
||||||
|
self.addSubnode(textNode)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
textNode.displaysAsynchronously = false
|
||||||
|
textNode.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(textNode.view)
|
||||||
|
}
|
||||||
|
if let (offset, duration) = animation {
|
||||||
|
textNode.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, additive: true)
|
||||||
|
textNode.layer.animateScale(from: 0.3, to: 1.0, duration: duration)
|
||||||
|
textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||||
|
}
|
||||||
|
self.resolvedSegments[segment.key] = (segment, textNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeKeys: [ResolvedSegment.Key] = []
|
||||||
|
for key in self.resolvedSegments.keys {
|
||||||
|
if !validKeys.contains(key) {
|
||||||
|
removeKeys.append(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in removeKeys {
|
||||||
|
guard let (_, textNode) = self.resolvedSegments.removeValue(forKey: key) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if transition.isAnimated {
|
||||||
|
textNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||||
|
textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textNode] _ in
|
||||||
|
textNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
textNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Layout(size: contentSize, isTruncated: isTruncated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -32,14 +32,16 @@ final class StoryItemContentComponent: Component {
|
|||||||
let item: EngineStoryItem
|
let item: EngineStoryItem
|
||||||
let audioMode: StoryContentItem.AudioMode
|
let audioMode: StoryContentItem.AudioMode
|
||||||
let isVideoBuffering: Bool
|
let isVideoBuffering: Bool
|
||||||
|
let isCurrent: Bool
|
||||||
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool) {
|
|
||||||
|
init(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, item: EngineStoryItem, audioMode: StoryContentItem.AudioMode, isVideoBuffering: Bool, isCurrent: Bool) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.item = item
|
self.item = item
|
||||||
self.audioMode = audioMode
|
self.audioMode = audioMode
|
||||||
self.isVideoBuffering = isVideoBuffering
|
self.isVideoBuffering = isVideoBuffering
|
||||||
|
self.isCurrent = isCurrent
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool {
|
static func ==(lhs: StoryItemContentComponent, rhs: StoryItemContentComponent) -> Bool {
|
||||||
@ -57,6 +59,9 @@ final class StoryItemContentComponent: Component {
|
|||||||
}
|
}
|
||||||
if lhs.isVideoBuffering != rhs.isVideoBuffering {
|
if lhs.isVideoBuffering != rhs.isVideoBuffering {
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
|
if lhs.isCurrent != rhs.isCurrent {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -94,6 +99,9 @@ final class StoryItemContentComponent: Component {
|
|||||||
|
|
||||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||||
|
|
||||||
|
private var fetchPriorityResourceId: String?
|
||||||
|
private var currentFetchPriority: (isMain: Bool, disposable: Disposable)?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||||
self.imageView = StoryItemImageView()
|
self.imageView = StoryItemImageView()
|
||||||
@ -121,6 +129,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
self.priorityDisposable?.dispose()
|
self.priorityDisposable?.dispose()
|
||||||
self.currentProgressTimer?.invalidate()
|
self.currentProgressTimer?.invalidate()
|
||||||
self.videoProgressDisposable?.dispose()
|
self.videoProgressDisposable?.dispose()
|
||||||
|
self.currentFetchPriority?.disposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func performActionAfterImageContentLoaded(update: Bool) {
|
private func performActionAfterImageContentLoaded(update: Bool) {
|
||||||
@ -479,6 +488,27 @@ final class StoryItemContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fetchPriorityResourceId: String?
|
||||||
|
switch messageMedia {
|
||||||
|
case let .image(image):
|
||||||
|
if let representation = largestImageRepresentation(image.representations) {
|
||||||
|
fetchPriorityResourceId = representation.resource.id.stringRepresentation
|
||||||
|
}
|
||||||
|
case let .file(file):
|
||||||
|
fetchPriorityResourceId = file.resource.id.stringRepresentation
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.fetchPriorityResourceId != fetchPriorityResourceId || self.currentFetchPriority?.0 != component.isCurrent {
|
||||||
|
self.fetchPriorityResourceId = fetchPriorityResourceId
|
||||||
|
self.currentFetchPriority?.disposable.dispose()
|
||||||
|
|
||||||
|
if let fetchPriorityResourceId {
|
||||||
|
self.currentFetchPriority = (component.isCurrent, component.context.engine.resources.pushPriorityDownload(resourceId: fetchPriorityResourceId, priority: component.isCurrent ? 2 : 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if reloadMedia, let messageMedia, let peerReference {
|
if reloadMedia, let messageMedia, let peerReference {
|
||||||
self.priorityDisposable?.dispose()
|
self.priorityDisposable?.dispose()
|
||||||
self.priorityDisposable = nil
|
self.priorityDisposable = nil
|
||||||
|
@ -432,6 +432,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
private var initializedOffset: Bool = false
|
private var initializedOffset: Bool = false
|
||||||
|
|
||||||
private var viewListPanState: PanState?
|
private var viewListPanState: PanState?
|
||||||
|
private var isCompletingViewListPan: Bool = false
|
||||||
private var viewListSwipeRecognizer: InteractiveTransitionGestureRecognizer?
|
private var viewListSwipeRecognizer: InteractiveTransitionGestureRecognizer?
|
||||||
|
|
||||||
private var verticalPanState: PanState?
|
private var verticalPanState: PanState?
|
||||||
@ -838,6 +839,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let translation = recognizer.translation(in: self)
|
let translation = recognizer.translation(in: self)
|
||||||
let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width))
|
let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width))
|
||||||
self.viewListPanState = PanState(fraction: fraction)
|
self.viewListPanState = PanState(fraction: fraction)
|
||||||
|
self.isCompletingViewListPan = false
|
||||||
|
self.layer.removeAnimation(forKey: "isCompletingViewListPan")
|
||||||
self.state?.updated(transition: .immediate)
|
self.state?.updated(transition: .immediate)
|
||||||
}
|
}
|
||||||
case .changed:
|
case .changed:
|
||||||
@ -846,6 +849,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width))
|
let fraction: CGFloat = max(-1.0, min(1.0, translation.x / self.bounds.width))
|
||||||
viewListPanState.fraction = fraction
|
viewListPanState.fraction = fraction
|
||||||
self.viewListPanState = viewListPanState
|
self.viewListPanState = viewListPanState
|
||||||
|
self.isCompletingViewListPan = false
|
||||||
self.state?.updated(transition: .immediate)
|
self.state?.updated(transition: .immediate)
|
||||||
}
|
}
|
||||||
case .cancelled, .ended:
|
case .cancelled, .ended:
|
||||||
@ -868,8 +872,19 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !consumed {
|
if !consumed {
|
||||||
|
let transition = Transition(animation: .curve(duration: 0.4, curve: .spring))
|
||||||
self.viewListPanState = nil
|
self.viewListPanState = nil
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
self.isCompletingViewListPan = true
|
||||||
|
transition.attachAnimation(view: self, id: "isCompletingViewListPan", completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.isCompletingViewListPan {
|
||||||
|
self.isCompletingViewListPan = false
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.state?.updated(transition: transition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -1175,7 +1190,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
peer: component.slice.peer,
|
peer: component.slice.peer,
|
||||||
item: item.storyItem,
|
item: item.storyItem,
|
||||||
audioMode: component.audioMode,
|
audioMode: component.audioMode,
|
||||||
isVideoBuffering: visibleItem.isBuffering
|
isVideoBuffering: visibleItem.isBuffering,
|
||||||
|
isCurrent: index == centralIndex
|
||||||
)),
|
)),
|
||||||
environment: {
|
environment: {
|
||||||
itemEnvironment
|
itemEnvironment
|
||||||
@ -1287,16 +1303,14 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if component.slice.peer.id == component.context.account.peerId {
|
if component.slice.peer.id == component.context.account.peerId {
|
||||||
if let _ = component.slice.item.storyItem.views {
|
self.displayViewList = true
|
||||||
self.displayViewList = true
|
if component.verticalPanFraction == 0.0 {
|
||||||
if component.verticalPanFraction == 0.0 {
|
self.preparingToDisplayViewList = true
|
||||||
self.preparingToDisplayViewList = true
|
self.updateScrolling(transition: .immediate)
|
||||||
self.updateScrolling(transition: .immediate)
|
self.preparingToDisplayViewList = false
|
||||||
self.preparingToDisplayViewList = false
|
|
||||||
}
|
|
||||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||||
|
return true
|
||||||
} else {
|
} else {
|
||||||
var canReply = true
|
var canReply = true
|
||||||
if component.slice.peer.id == component.context.account.peerId {
|
if component.slice.peer.id == component.context.account.peerId {
|
||||||
@ -1777,10 +1791,36 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id {
|
if let animateNextNavigationId = self.animateNextNavigationId, animateNextNavigationId == component.slice.item.storyItem.id {
|
||||||
self.animateNextNavigationId = nil
|
self.animateNextNavigationId = nil
|
||||||
self.viewListPanState = nil
|
self.viewListPanState = nil
|
||||||
|
self.isCompletingViewListPan = true
|
||||||
itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring))
|
itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring))
|
||||||
|
itemsTransition.attachAnimation(view: self, id: "isCompletingViewListPan", completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.isCompletingViewListPan {
|
||||||
|
self.isCompletingViewListPan = false
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
|
})
|
||||||
resetScrollingOffsetWithItemTransition = true
|
resetScrollingOffsetWithItemTransition = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let awaitingSwitchToId = self.awaitingSwitchToId, awaitingSwitchToId.to == component.slice.item.storyItem.id {
|
||||||
|
self.awaitingSwitchToId = nil
|
||||||
|
self.viewListPanState = nil
|
||||||
|
self.isCompletingViewListPan = true
|
||||||
|
itemsTransition = transition.withAnimation(.curve(duration: 0.3, curve: .spring))
|
||||||
|
itemsTransition.attachAnimation(view: self, id: "isCompletingViewListPan", completion: { [weak self] completed in
|
||||||
|
guard let self, completed else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if self.isCompletingViewListPan {
|
||||||
|
self.isCompletingViewListPan = false
|
||||||
|
self.state?.updated(transition: .immediate)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/*if self.topContentGradientLayer.colors == nil {
|
/*if self.topContentGradientLayer.colors == nil {
|
||||||
var locations: [NSNumber] = []
|
var locations: [NSNumber] = []
|
||||||
var colors: [CGColor] = []
|
var colors: [CGColor] = []
|
||||||
@ -2114,15 +2154,29 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
var maximizedBottomContentHeight: CGFloat = 0.0
|
var maximizedBottomContentHeight: CGFloat = 0.0
|
||||||
var minimizedBottomContentFraction: CGFloat = 0.0
|
var minimizedBottomContentFraction: CGFloat = 0.0
|
||||||
|
|
||||||
|
let minimizedHeight = max(100.0, availableSize.height - (325.0 + 12.0))
|
||||||
|
let defaultHeight = 60.0 + component.safeInsets.bottom + 1.0
|
||||||
|
|
||||||
var validViewListIds: [Int32] = []
|
var validViewListIds: [Int32] = []
|
||||||
if component.slice.peer.id == component.context.account.peerId, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) {
|
if component.slice.peer.id == component.context.account.peerId, let currentIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == component.slice.item.storyItem.id }) {
|
||||||
var visibleViewListIds: [Int32] = [component.slice.item.storyItem.id]
|
var visibleViewListIds: [Int32] = [component.slice.item.storyItem.id]
|
||||||
if self.displayViewList {
|
if self.displayViewList, let viewListPanState = self.viewListPanState {
|
||||||
if currentIndex != 0 {
|
if currentIndex != 0 {
|
||||||
visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id)
|
if viewListPanState.fraction > 0.0 {
|
||||||
|
visibleViewListIds.append(component.slice.allItems[currentIndex - 1].storyItem.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if currentIndex != component.slice.allItems.count - 1 {
|
if currentIndex != component.slice.allItems.count - 1 {
|
||||||
visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id)
|
if viewListPanState.fraction < 0.0 {
|
||||||
|
visibleViewListIds.append(component.slice.allItems[currentIndex + 1].storyItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.viewListPanState != nil || self.isCompletingViewListPan {
|
||||||
|
for (id, _) in self.viewLists {
|
||||||
|
if !visibleViewListIds.contains(id) {
|
||||||
|
visibleViewListIds.append(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2134,6 +2188,20 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
var fixedAnimationOffset: CGFloat = 0.0
|
var fixedAnimationOffset: CGFloat = 0.0
|
||||||
var applyFixedAnimationOffsetIds: [Int32] = []
|
var applyFixedAnimationOffsetIds: [Int32] = []
|
||||||
|
|
||||||
|
let outerExpansionFraction: CGFloat
|
||||||
|
let outerExpansionDirection: Bool
|
||||||
|
if self.displayViewList {
|
||||||
|
if let verticalPanState = self.verticalPanState {
|
||||||
|
outerExpansionFraction = max(0.0, min(1.0, 1.0 - verticalPanState.fraction))
|
||||||
|
} else {
|
||||||
|
outerExpansionFraction = 1.0
|
||||||
|
}
|
||||||
|
outerExpansionDirection = false
|
||||||
|
} else {
|
||||||
|
outerExpansionFraction = component.verticalPanFraction
|
||||||
|
outerExpansionDirection = true
|
||||||
|
}
|
||||||
|
|
||||||
for id in visibleViewListIds {
|
for id in visibleViewListIds {
|
||||||
guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) else {
|
guard let itemIndex = component.slice.allItems.firstIndex(where: { $0.storyItem.id == id }) else {
|
||||||
continue
|
continue
|
||||||
@ -2154,23 +2222,6 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
applyFixedAnimationOffsetIds.append(id)
|
applyFixedAnimationOffsetIds.append(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
let outerExpansionFraction: CGFloat
|
|
||||||
let outerExpansionDirection: Bool
|
|
||||||
if self.displayViewList {
|
|
||||||
if let verticalPanState = self.verticalPanState {
|
|
||||||
outerExpansionFraction = max(0.0, min(1.0, 1.0 - verticalPanState.fraction))
|
|
||||||
} else {
|
|
||||||
outerExpansionFraction = 1.0
|
|
||||||
}
|
|
||||||
outerExpansionDirection = false
|
|
||||||
} else if let _ = item.storyItem.views {
|
|
||||||
outerExpansionFraction = component.verticalPanFraction
|
|
||||||
outerExpansionDirection = true
|
|
||||||
} else {
|
|
||||||
outerExpansionFraction = 0.0
|
|
||||||
outerExpansionDirection = true
|
|
||||||
}
|
|
||||||
|
|
||||||
viewList.view.parentState = state
|
viewList.view.parentState = state
|
||||||
let viewListSize = viewList.view.update(
|
let viewListSize = viewList.view.update(
|
||||||
transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint(
|
transition: viewListTransition.withUserData(PeerListItemComponent.TransitionHint(
|
||||||
@ -2419,10 +2470,10 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if id == component.slice.item.storyItem.id {
|
if id == component.slice.item.storyItem.id {
|
||||||
viewListInset = viewList.externalState.minimizedHeight * viewList.externalState.minimizationFraction + viewList.externalState.defaultHeight * (1.0 - viewList.externalState.minimizationFraction)
|
viewListInset = minimizedHeight * viewList.externalState.minimizationFraction + defaultHeight * (1.0 - viewList.externalState.minimizationFraction)
|
||||||
inputPanelBottomInset = viewListInset
|
inputPanelBottomInset = viewListInset
|
||||||
minimizedBottomContentHeight = viewList.externalState.minimizedHeight
|
minimizedBottomContentHeight = minimizedHeight
|
||||||
maximizedBottomContentHeight = viewList.externalState.defaultHeight
|
maximizedBottomContentHeight = defaultHeight
|
||||||
minimizedBottomContentFraction = viewList.externalState.minimizationFraction
|
minimizedBottomContentFraction = viewList.externalState.minimizationFraction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -630,6 +630,26 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
var applyState = false
|
var applyState = false
|
||||||
var firstTime = true
|
var firstTime = true
|
||||||
self.viewListDisposable = (viewList.state
|
self.viewListDisposable = (viewList.state
|
||||||
|
|> mapToSignal { state in
|
||||||
|
#if DEBUG && false
|
||||||
|
if !state.items.isEmpty {
|
||||||
|
let otherItems: [EngineStoryViewListContext.Item] = Array(state.items.reversed().prefix(3))
|
||||||
|
let otherState = EngineStoryViewListContext.State(
|
||||||
|
totalCount: 3,
|
||||||
|
items: otherItems,
|
||||||
|
loadMoreToken: state.loadMoreToken
|
||||||
|
)
|
||||||
|
return .single(state)
|
||||||
|
|> then(.single(otherState) |> delay(1.0, queue: .mainQueue()))
|
||||||
|
|> then(.complete() |> delay(1.0, queue: .mainQueue()))
|
||||||
|
|> restart
|
||||||
|
} else {
|
||||||
|
return .single(state)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
return .single(state)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] listState in
|
|> deliverOnMainQueue).start(next: { [weak self] listState in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -641,6 +661,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
self.ignoreScrolling = false
|
self.ignoreScrolling = false
|
||||||
}
|
}
|
||||||
self.viewListState = listState
|
self.viewListState = listState
|
||||||
|
|
||||||
if applyState {
|
if applyState {
|
||||||
self.state?.updated(transition: Transition.immediate.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: false)))
|
self.state?.updated(transition: Transition.immediate.withUserData(PeerListItemComponent.TransitionHint(synchronousLoad: false)))
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ swift_library(
|
|||||||
"//submodules/TelegramCore",
|
"//submodules/TelegramCore",
|
||||||
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
"//submodules/TelegramUI/Components/MoreHeaderButton",
|
||||||
"//submodules/SemanticStatusNode",
|
"//submodules/SemanticStatusNode",
|
||||||
|
"//submodules/AnimatedCountLabelNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -11,8 +11,17 @@ import MoreHeaderButton
|
|||||||
import SemanticStatusNode
|
import SemanticStatusNode
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
|
import AnimatedCountLabelNode
|
||||||
|
|
||||||
public final class StoryFooterPanelComponent: Component {
|
public final class StoryFooterPanelComponent: Component {
|
||||||
|
public final class AnimationHint {
|
||||||
|
public let synchronousLoad: Bool
|
||||||
|
|
||||||
|
public init(synchronousLoad: Bool) {
|
||||||
|
self.synchronousLoad = synchronousLoad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public let context: AccountContext
|
public let context: AccountContext
|
||||||
public let strings: PresentationStrings
|
public let strings: PresentationStrings
|
||||||
public let storyItem: EngineStoryItem?
|
public let storyItem: EngineStoryItem?
|
||||||
@ -63,8 +72,8 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
|
|
||||||
public final class View: UIView {
|
public final class View: UIView {
|
||||||
private let viewStatsButton: HighlightTrackingButton
|
private let viewStatsButton: HighlightTrackingButton
|
||||||
private let viewStatsText = ComponentView<Empty>()
|
private let viewStatsText: AnimatedCountLabelView
|
||||||
private let viewStatsExpandedText = ComponentView<Empty>()
|
private let viewStatsExpandedText: AnimatedCountLabelView
|
||||||
private let deleteButton = ComponentView<Empty>()
|
private let deleteButton = ComponentView<Empty>()
|
||||||
|
|
||||||
private var statusButton: HighlightableButton?
|
private var statusButton: HighlightableButton?
|
||||||
@ -72,7 +81,7 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
private var uploadingText: ComponentView<Empty>?
|
private var uploadingText: ComponentView<Empty>?
|
||||||
|
|
||||||
private let avatarsContext: AnimatedAvatarSetContext
|
private let avatarsContext: AnimatedAvatarSetContext
|
||||||
private let avatarsNode: AnimatedAvatarSetNode
|
private let avatarsView: AnimatedAvatarSetView
|
||||||
|
|
||||||
private var component: StoryFooterPanelComponent?
|
private var component: StoryFooterPanelComponent?
|
||||||
private weak var state: EmptyComponentState?
|
private weak var state: EmptyComponentState?
|
||||||
@ -84,16 +93,18 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.viewStatsButton = HighlightTrackingButton()
|
self.viewStatsButton = HighlightTrackingButton()
|
||||||
|
self.viewStatsText = AnimatedCountLabelView(frame: CGRect())
|
||||||
|
self.viewStatsExpandedText = AnimatedCountLabelView(frame: CGRect())
|
||||||
|
|
||||||
self.avatarsContext = AnimatedAvatarSetContext()
|
self.avatarsContext = AnimatedAvatarSetContext()
|
||||||
self.avatarsNode = AnimatedAvatarSetNode()
|
self.avatarsView = AnimatedAvatarSetView()
|
||||||
|
|
||||||
self.externalContainerView = UIView()
|
self.externalContainerView = UIView()
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.avatarsNode.view.isUserInteractionEnabled = false
|
self.avatarsView.isUserInteractionEnabled = false
|
||||||
self.externalContainerView.addSubview(self.avatarsNode.view)
|
self.externalContainerView.addSubview(self.avatarsView)
|
||||||
self.addSubview(self.viewStatsButton)
|
self.addSubview(self.viewStatsButton)
|
||||||
|
|
||||||
self.viewStatsButton.highligthedChanged = { [weak self] highlighted in
|
self.viewStatsButton.highligthedChanged = { [weak self] highlighted in
|
||||||
@ -101,11 +112,11 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if highlighted {
|
if highlighted {
|
||||||
self.avatarsNode.view.alpha = 0.7
|
self.avatarsView.alpha = 0.7
|
||||||
self.viewStatsText.view?.alpha = 0.7
|
self.viewStatsText.alpha = 0.7
|
||||||
} else {
|
} else {
|
||||||
self.avatarsNode.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
|
self.avatarsView.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
|
||||||
self.viewStatsText.view?.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
|
self.viewStatsText.layer.animateAlpha(from: 0.7, to: 1.0, duration: 0.2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside)
|
self.viewStatsButton.addTarget(self, action: #selector(self.viewStatsPressed), for: .touchUpInside)
|
||||||
@ -137,6 +148,13 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
func update(component: StoryFooterPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let isFirstTime = self.component == nil
|
||||||
|
|
||||||
|
var synchronousLoad = true
|
||||||
|
if let hint = transition.userData(AnimationHint.self) {
|
||||||
|
synchronousLoad = hint.synchronousLoad
|
||||||
|
}
|
||||||
|
|
||||||
if self.component?.storyItem?.id != component.storyItem?.id || self.component?.storyItem?.isPending != component.storyItem?.isPending {
|
if self.component?.storyItem?.id != component.storyItem?.id || self.component?.storyItem?.isPending != component.storyItem?.isPending {
|
||||||
self.uploadProgressDisposable?.dispose()
|
self.uploadProgressDisposable?.dispose()
|
||||||
self.uploadProgress = 0.0
|
self.uploadProgress = 0.0
|
||||||
@ -249,12 +267,12 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
peers = Array(seenPeers.prefix(3))
|
peers = Array(seenPeers.prefix(3))
|
||||||
}
|
}
|
||||||
let avatarsContent = self.avatarsContext.update(peers: peers, animated: false)
|
let avatarsContent = self.avatarsContext.update(peers: peers, animated: false)
|
||||||
let avatarsSize = self.avatarsNode.update(context: component.context, content: avatarsContent, itemSize: CGSize(width: 30.0, height: 30.0), animated: false, synchronousLoad: true)
|
let avatarsSize = self.avatarsView.update(context: component.context, content: avatarsContent, itemSize: CGSize(width: 30.0, height: 30.0), animation: isFirstTime ? ListViewItemUpdateAnimation.None : ListViewItemUpdateAnimation.System(duration: 0.25, transition: ControlledTransition(duration: 0.25, curve: .easeInOut, interactive: false)), synchronousLoad: synchronousLoad)
|
||||||
|
|
||||||
let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize)
|
let avatarsNodeFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize)
|
||||||
self.avatarsNode.position = avatarsNodeFrame.center
|
self.avatarsView.center = avatarsNodeFrame.center
|
||||||
self.avatarsNode.bounds = CGRect(origin: CGPoint(), size: avatarsNodeFrame.size)
|
self.avatarsView.bounds = CGRect(origin: CGPoint(), size: avatarsNodeFrame.size)
|
||||||
transition.setAlpha(view: self.avatarsNode.view, alpha: avatarsAlpha)
|
transition.setAlpha(view: self.avatarsView, alpha: avatarsAlpha)
|
||||||
if !avatarsSize.width.isZero {
|
if !avatarsSize.width.isZero {
|
||||||
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
leftOffset = avatarsNodeFrame.maxX + avatarSpacing
|
||||||
}
|
}
|
||||||
@ -270,21 +288,28 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
viewsText = component.strings.Story_Footer_Views(Int32(viewCount))
|
viewsText = component.strings.Story_Footer_Views(Int32(viewCount))
|
||||||
}
|
}
|
||||||
|
let _ = viewsText
|
||||||
|
|
||||||
self.viewStatsButton.isEnabled = viewCount != 0
|
self.viewStatsButton.isEnabled = viewCount != 0
|
||||||
|
|
||||||
let viewStatsTextSize = self.viewStatsText.update(
|
//TODO:localize
|
||||||
transition: .immediate,
|
var regularSegments: [AnimatedCountLabelView.Segment] = []
|
||||||
component: AnyComponent(Text(text: viewsText, font: Font.regular(15.0), color: .white)),
|
if viewCount != 0 {
|
||||||
environment: {},
|
regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white)))
|
||||||
containerSize: CGSize(width: availableSize.width, height: size.height)
|
}
|
||||||
)
|
regularSegments.append(.text(1, NSAttributedString(string: " Views", font: Font.regular(15.0), textColor: .white)))
|
||||||
let viewStatsExpandedTextSize = self.viewStatsExpandedText.update(
|
|
||||||
transition: .immediate,
|
var expandedSegments: [AnimatedCountLabelView.Segment] = []
|
||||||
component: AnyComponent(Text(text: viewsText, font: Font.semibold(17.0), color: .white)),
|
if viewCount != 0 {
|
||||||
environment: {},
|
expandedSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.semibold(17.0), textColor: .white)))
|
||||||
containerSize: CGSize(width: availableSize.width, height: size.height)
|
}
|
||||||
)
|
expandedSegments.append(.text(1, NSAttributedString(string: " Views", font: Font.semibold(17.0), textColor: .white)))
|
||||||
|
|
||||||
|
let viewStatsTextLayout = self.viewStatsText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut))
|
||||||
|
let expandedViewStatsTextLayout = self.viewStatsExpandedText.update(size: CGSize(width: availableSize.width, height: size.height), segments: expandedSegments, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut))
|
||||||
|
|
||||||
|
let viewStatsTextSize = viewStatsTextLayout.size
|
||||||
|
let viewStatsExpandedTextSize = expandedViewStatsTextLayout.size
|
||||||
|
|
||||||
let viewStatsCollapsedFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
|
let viewStatsCollapsedFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - viewStatsTextSize.height) * 0.5)), size: viewStatsTextSize)
|
||||||
let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: 3.0 + floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize)
|
let viewStatsExpandedFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - viewStatsExpandedTextSize.width) * 0.5), y: 3.0 + floor((size.height - viewStatsExpandedTextSize.height) * 0.5)), size: viewStatsExpandedTextSize)
|
||||||
@ -293,7 +318,9 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction)
|
let viewStatsTextCenter = viewStatsCollapsedFrame.center.interpolate(to: viewStatsExpandedFrame.center, amount: component.expandFraction)
|
||||||
|
|
||||||
let viewStatsTextFrame = viewStatsCollapsedFrame.size.centered(around: viewStatsTextCenter)
|
let viewStatsTextFrame = viewStatsCollapsedFrame.size.centered(around: viewStatsTextCenter)
|
||||||
if let viewStatsTextView = self.viewStatsText.view {
|
do {
|
||||||
|
let viewStatsTextView = self.viewStatsText
|
||||||
|
|
||||||
if viewStatsTextView.superview == nil {
|
if viewStatsTextView.superview == nil {
|
||||||
viewStatsTextView.isUserInteractionEnabled = false
|
viewStatsTextView.isUserInteractionEnabled = false
|
||||||
self.externalContainerView.addSubview(viewStatsTextView)
|
self.externalContainerView.addSubview(viewStatsTextView)
|
||||||
@ -305,7 +332,9 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let viewStatsExpandedTextFrame = viewStatsExpandedFrame.size.centered(around: viewStatsTextCenter)
|
let viewStatsExpandedTextFrame = viewStatsExpandedFrame.size.centered(around: viewStatsTextCenter)
|
||||||
if let viewStatsExpandedTextView = self.viewStatsExpandedText.view {
|
do {
|
||||||
|
let viewStatsExpandedTextView = self.viewStatsExpandedText
|
||||||
|
|
||||||
if viewStatsExpandedTextView.superview == nil {
|
if viewStatsExpandedTextView.superview == nil {
|
||||||
viewStatsExpandedTextView.isUserInteractionEnabled = false
|
viewStatsExpandedTextView.isUserInteractionEnabled = false
|
||||||
self.addSubview(viewStatsExpandedTextView)
|
self.addSubview(viewStatsExpandedTextView)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user