mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-07 14:53:35 +00:00
Added CollectionIndexNode
This commit is contained in:
parent
c8a8570465
commit
d71707c31b
@ -76,6 +76,7 @@
|
||||
D03E7DF91C96C5F200C07816 /* NSWeakReference.m in Sources */ = {isa = PBXBuildFile; fileRef = D03E7DF71C96C5F200C07816 /* NSWeakReference.m */; };
|
||||
D03E7DFF1C96F7B400C07816 /* StatusBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E7DFE1C96F7B400C07816 /* StatusBarManager.swift */; };
|
||||
D03E7E011C974AB300C07816 /* DisplayLinkDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E7E001C974AB300C07816 /* DisplayLinkDispatcher.swift */; };
|
||||
D04554AA21BDB93E007A6DD9 /* CollectionIndexNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */; };
|
||||
D04C468E1F4C97BE00D30FE1 /* PageControlNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04C468D1F4C97BE00D30FE1 /* PageControlNode.swift */; };
|
||||
D05174B31EAA833200A1BF36 /* CASeeThroughTracingLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = D05174B11EAA833200A1BF36 /* CASeeThroughTracingLayer.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
D05174B41EAA833200A1BF36 /* CASeeThroughTracingLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = D05174B21EAA833200A1BF36 /* CASeeThroughTracingLayer.m */; };
|
||||
@ -254,6 +255,7 @@
|
||||
D03E7DF71C96C5F200C07816 /* NSWeakReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSWeakReference.m; sourceTree = "<group>"; };
|
||||
D03E7DFE1C96F7B400C07816 /* StatusBarManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarManager.swift; sourceTree = "<group>"; };
|
||||
D03E7E001C974AB300C07816 /* DisplayLinkDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayLinkDispatcher.swift; sourceTree = "<group>"; };
|
||||
D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionIndexNode.swift; sourceTree = "<group>"; };
|
||||
D04C468D1F4C97BE00D30FE1 /* PageControlNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControlNode.swift; sourceTree = "<group>"; };
|
||||
D05174B11EAA833200A1BF36 /* CASeeThroughTracingLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CASeeThroughTracingLayer.h; sourceTree = "<group>"; };
|
||||
D05174B21EAA833200A1BF36 /* CASeeThroughTracingLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CASeeThroughTracingLayer.m; sourceTree = "<group>"; };
|
||||
@ -508,6 +510,7 @@
|
||||
D0FA08C32048803C00DD23FC /* TextNode.swift */,
|
||||
D0FA08C5204880C900DD23FC /* ImmediateTextNode.swift */,
|
||||
D0CA3F892073F7650042D2B6 /* LinkHighlightingNode.swift */,
|
||||
D04554A921BDB93E007A6DD9 /* CollectionIndexNode.swift */,
|
||||
);
|
||||
name = Nodes;
|
||||
sourceTree = "<group>";
|
||||
@ -1012,6 +1015,7 @@
|
||||
D0CA3F8A2073F7650042D2B6 /* LinkHighlightingNode.swift in Sources */,
|
||||
D0C2DFCB1CC4431D0044FF83 /* ListViewAnimation.swift in Sources */,
|
||||
D0BE93191E8ED71100DCC1E6 /* NativeWindowHostView.swift in Sources */,
|
||||
D04554AA21BDB93E007A6DD9 /* CollectionIndexNode.swift in Sources */,
|
||||
D05CC3251B695B0700E235A3 /* NavigationBarProxy.m in Sources */,
|
||||
D05174B41EAA833200A1BF36 /* CASeeThroughTracingLayer.m in Sources */,
|
||||
D03AA4D9202D8E5E0056C405 /* GlobalOverlayPresentationContext.swift in Sources */,
|
||||
|
||||
141
Display/CollectionIndexNode.swift
Normal file
141
Display/CollectionIndexNode.swift
Normal file
@ -0,0 +1,141 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
private let titleFont = Font.bold(11.0)
|
||||
|
||||
public final class CollectionIndexNode: ASDisplayNode {
|
||||
public static let searchIndex: String = "_$search$_"
|
||||
|
||||
private var currentSize: CGSize?
|
||||
private var currentSections: [String] = []
|
||||
private var currentColor: UIColor?
|
||||
private var titleNodes: [String: (node: ImmediateTextNode, size: CGSize)] = [:]
|
||||
|
||||
private var currentSelectedIndex: String?
|
||||
public var indexSelected: ((String) -> Void)?
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))))
|
||||
}
|
||||
|
||||
public func update(size: CGSize, color: UIColor, sections: [String], transition: ContainedViewLayoutTransition) {
|
||||
if self.currentColor == nil || !color.isEqual(self.currentColor) {
|
||||
self.currentColor = color
|
||||
for (title, nodeAndSize) in self.titleNodes {
|
||||
nodeAndSize.node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color)
|
||||
let _ = nodeAndSize.node.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
}
|
||||
}
|
||||
|
||||
if self.currentSize == size && self.currentSections == sections {
|
||||
return
|
||||
}
|
||||
|
||||
self.currentSize = size
|
||||
self.currentSections = sections
|
||||
|
||||
let itemHeight: CGFloat = 15.0
|
||||
let verticalInset: CGFloat = 10.0
|
||||
let maxHeight = size.height - verticalInset * 2.0
|
||||
|
||||
let maxItemCount = min(sections.count, Int(floor(maxHeight / itemHeight)))
|
||||
let skipCount = Int(ceil(CGFloat(sections.count) / CGFloat(maxItemCount)))
|
||||
let actualCount: CGFloat = ceil(CGFloat(sections.count) / CGFloat(skipCount))
|
||||
|
||||
let totalHeight = actualCount * itemHeight
|
||||
let verticalOrigin = verticalInset + floor((maxHeight - totalHeight) / 2.0)
|
||||
|
||||
var validTitles = Set<String>()
|
||||
|
||||
var index = 0
|
||||
var displayIndex = 0
|
||||
while index < sections.count {
|
||||
let title = sections[index]
|
||||
let nodeAndSize: (node: ImmediateTextNode, size: CGSize)
|
||||
var animate = false
|
||||
if let current = self.titleNodes[title] {
|
||||
animate = true
|
||||
nodeAndSize = current
|
||||
} else {
|
||||
let node = ImmediateTextNode()
|
||||
node.attributedText = NSAttributedString(string: title, font: titleFont, textColor: color)
|
||||
let nodeSize = node.updateLayout(CGSize(width: 100.0, height: 100.0))
|
||||
nodeAndSize = (node, nodeSize)
|
||||
self.addSubnode(node)
|
||||
self.titleNodes[title] = nodeAndSize
|
||||
}
|
||||
validTitles.insert(title)
|
||||
let previousPosition = nodeAndSize.node.position
|
||||
nodeAndSize.node.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeAndSize.size.width) / 2.0), y: verticalOrigin + itemHeight * CGFloat(displayIndex) + floor((itemHeight - nodeAndSize.size.height) / 2.0)), size: nodeAndSize.size)
|
||||
if animate {
|
||||
transition.animatePosition(node: nodeAndSize.node, from: previousPosition)
|
||||
}
|
||||
|
||||
index += skipCount
|
||||
displayIndex += 1
|
||||
}
|
||||
|
||||
var removeTitles: [String] = []
|
||||
for title in self.titleNodes.keys {
|
||||
if !validTitles.contains(title) {
|
||||
removeTitles.append(title)
|
||||
}
|
||||
}
|
||||
|
||||
for title in removeTitles {
|
||||
self.titleNodes.removeValue(forKey: title)?.node.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.bounds.insetBy(dx: -5.0, dy: 0.0).contains(point) {
|
||||
return self.view
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
var locationTitleAndPosition: (String, CGFloat)?
|
||||
let location = recognizer.location(in: self.view)
|
||||
for (title, nodeAndSize) in self.titleNodes {
|
||||
let nodeFrame = nodeAndSize.node.frame
|
||||
if location.y >= nodeFrame.minY - 5.0 && location.y <= nodeFrame.maxY + 5.0 {
|
||||
if let currentTitleAndPosition = locationTitleAndPosition {
|
||||
let distance = abs(nodeFrame.midY - location.y)
|
||||
let previousDistance = abs(currentTitleAndPosition.1 - location.y)
|
||||
if distance < previousDistance {
|
||||
locationTitleAndPosition = (title, nodeFrame.midY)
|
||||
}
|
||||
} else {
|
||||
locationTitleAndPosition = (title, nodeFrame.midY)
|
||||
}
|
||||
}
|
||||
}
|
||||
let locationTitle = locationTitleAndPosition?.0
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
self.currentSelectedIndex = locationTitle
|
||||
if let locationTitle = locationTitle {
|
||||
self.indexSelected?(locationTitle)
|
||||
}
|
||||
case .changed:
|
||||
if locationTitle != self.currentSelectedIndex {
|
||||
self.currentSelectedIndex = locationTitle
|
||||
if let locationTitle = locationTitle {
|
||||
self.indexSelected?(locationTitle)
|
||||
}
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
self.currentSelectedIndex = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,16 +137,16 @@ public extension ContainedViewLayoutTransition {
|
||||
|
||||
func animatePosition(node: ASDisplayNode, from position: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animatePosition(from: position, to: node.position, duration: duration, timingFunction: curve.timingFunction, completion: { result in
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
completion(true)
|
||||
}
|
||||
})
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animatePosition(from: position, to: node.position, duration: duration, timingFunction: curve.timingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -227,6 +227,12 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
public var indicatorStyle: UIScrollViewIndicatorStyle = .default {
|
||||
didSet {
|
||||
self.scrollView.indicatorStyle = self.indicatorStyle
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var opaqueState: Any?
|
||||
|
||||
public override init() {
|
||||
@ -755,6 +761,9 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
private func applyPresentaionLayoutTransition(_ presentationLayoutTransition: GridNodePresentationLayoutTransition, removedNodes: [GridItemNode], updateLayoutTransition: ContainedViewLayoutTransition?, customScrollToItem: Bool, itemTransition: ContainedViewLayoutTransition, synchronousLoads: Bool, updatingLayout: Bool, completion: (GridNodeDisplayedItemRange) -> Void) {
|
||||
let boundsTransition: ContainedViewLayoutTransition = updateLayoutTransition ?? .immediate
|
||||
|
||||
var addedNodes = false
|
||||
let verticalIndicator = self.scrollView.subviews.last as? UIImageView
|
||||
|
||||
var previousItemFrames: [WrappedGridItemNode: CGRect]?
|
||||
var saveItemFrames = false
|
||||
switch presentationLayoutTransition.transition {
|
||||
@ -782,7 +791,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
previousItemFrames = itemFrames
|
||||
}
|
||||
|
||||
applyingContentOffset = true
|
||||
self.applyingContentOffset = true
|
||||
|
||||
let previousBounds = self.bounds
|
||||
self.scrollView.contentSize = presentationLayoutTransition.layout.contentSize
|
||||
@ -819,6 +828,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
let itemNode = self.items[item.index].node(layout: presentationLayoutTransition.layout.layout, synchronousLoad: synchronousLoads)
|
||||
itemNode.frame = item.frame
|
||||
self.addItemNode(index: item.index, itemNode: itemNode, lowestSectionNode: lowestSectionNode)
|
||||
addedNodes = true
|
||||
itemNode.updateLayout(item: self.items[item.index], size: item.frame.size, isVisible: bounds.intersects(item.frame), synchronousLoads: synchronousLoads)
|
||||
}
|
||||
}
|
||||
@ -845,6 +855,7 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
let sectionNode = section.section.node()
|
||||
sectionNode.frame = sectionFrame
|
||||
self.addSectionNode(section: wrappedSection, sectionNode: sectionNode)
|
||||
addedNodes = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1100,6 +1111,12 @@ open class GridNode: GridNodeScroller, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if addedNodes {
|
||||
if let verticalIndicator = verticalIndicator, self.scrollView.subviews.last !== verticalIndicator {
|
||||
verticalIndicator.superview?.bringSubview(toFront: verticalIndicator)
|
||||
}
|
||||
}
|
||||
|
||||
if let presentationLayoutUpdated = self.presentationLayoutUpdated {
|
||||
presentationLayoutUpdated(GridNodeCurrentPresentationLayout(layout: presentationLayoutTransition.layout.layout, contentOffset: presentationLayoutTransition.layout.contentOffset, contentSize: presentationLayoutTransition.layout.contentSize), updateLayoutTransition ?? presentationLayoutTransition.transition)
|
||||
}
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
public struct ImmediateTextNodeLayoutInfo {
|
||||
public let size: CGSize
|
||||
public let truncated: Bool
|
||||
}
|
||||
|
||||
public class ImmediateTextNode: TextNode {
|
||||
public var attributedText: NSAttributedString?
|
||||
public var textAlignment: NSTextAlignment = .natural
|
||||
@ -31,6 +36,13 @@ public class ImmediateTextNode: TextNode {
|
||||
return layout.size
|
||||
}
|
||||
|
||||
public func updateLayoutInfo(_ constrainedSize: CGSize) -> ImmediateTextNodeLayoutInfo {
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: nil, insets: self.insets))
|
||||
let _ = apply()
|
||||
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
|
||||
@ -97,11 +97,12 @@ public final class TextNodeLayout: NSObject {
|
||||
fileprivate let cutout: TextNodeCutout?
|
||||
fileprivate let insets: UIEdgeInsets
|
||||
public let size: CGSize
|
||||
public let truncated: Bool
|
||||
fileprivate let firstLineOffset: CGFloat
|
||||
fileprivate let lines: [TextNodeLine]
|
||||
public let hasRTL: Bool
|
||||
|
||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) {
|
||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], backgroundColor: UIColor?) {
|
||||
self.attributedString = attributedString
|
||||
self.maximumNumberOfLines = maximumNumberOfLines
|
||||
self.truncationType = truncationType
|
||||
@ -111,6 +112,7 @@ public final class TextNodeLayout: NSObject {
|
||||
self.cutout = cutout
|
||||
self.insets = insets
|
||||
self.size = size
|
||||
self.truncated = truncated
|
||||
self.firstLineOffset = firstLineOffset
|
||||
self.lines = lines
|
||||
self.backgroundColor = backgroundColor
|
||||
@ -327,7 +329,7 @@ public class TextNode: ASDisplayNode {
|
||||
var maybeTypesetter: CTTypesetter?
|
||||
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
|
||||
if maybeTypesetter == nil {
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
|
||||
}
|
||||
|
||||
let typesetter = maybeTypesetter!
|
||||
@ -364,6 +366,7 @@ public class TextNode: ASDisplayNode {
|
||||
|
||||
let firstLineOffset = floorToScreenPixels(fontDescent)
|
||||
|
||||
var truncated = false
|
||||
var first = true
|
||||
while true {
|
||||
var lineConstrainedWidth = constrainedSize.width
|
||||
@ -419,6 +422,7 @@ public class TextNode: ASDisplayNode {
|
||||
let truncationToken = CTLineCreateWithAttributedString(truncatedTokenString)
|
||||
|
||||
coreTextLine = CTLineCreateTruncatedLine(originalLine, Double(constrainedSize.width), truncationType, truncationToken) ?? truncationToken
|
||||
truncated = true
|
||||
}
|
||||
|
||||
let lineWidth = min(constrainedSize.width, ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))))
|
||||
@ -485,9 +489,9 @@ public class TextNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, backgroundColor: backgroundColor)
|
||||
} else {
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, alignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], backgroundColor: backgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user