mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Add 'submodules/Display/' from commit '7bd11013ea936e3d49d937550d599f5816d32560'
git-subtree-dir: submodules/Display git-subtree-mainline: 9bc996374ffdad37aef175427db72731c9551dcf git-subtree-split: 7bd11013ea936e3d49d937550d599f5816d32560
This commit is contained in:
commit
8f5a4f7dc1
25
submodules/Display/.gitignore
vendored
Normal file
25
submodules/Display/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
fastlane/README.md
|
||||
fastlane/report.xml
|
||||
fastlane/test_output/*
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
.DS_Store
|
||||
*.dSYM
|
||||
*.dSYM.zip
|
||||
*.ipa
|
||||
*/xcuserdata/*
|
||||
Display.xcodeproj/*
|
0
submodules/Display/.gitmodules
vendored
Normal file
0
submodules/Display/.gitmodules
vendored
Normal file
53
submodules/Display/BUCK
Normal file
53
submodules/Display/BUCK
Normal file
@ -0,0 +1,53 @@
|
||||
load('//tools:buck_utils.bzl', 'config_with_updated_linker_flags', 'configs_with_config', 'combined_config')
|
||||
load('//tools:buck_defs.bzl', 'SHARED_CONFIGS', 'EXTENSION_LIB_SPECIFIC_CONFIG')
|
||||
|
||||
apple_library(
|
||||
name = 'DisplayPrivate',
|
||||
srcs = glob([
|
||||
'Display/*.m',
|
||||
]),
|
||||
headers = glob([
|
||||
'Display/*.h',
|
||||
]),
|
||||
header_namespace = 'DisplayPrivate',
|
||||
exported_headers = glob([
|
||||
'Display/*.h',
|
||||
], exclude = ['Display/Display.h']),
|
||||
modular = True,
|
||||
configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])),
|
||||
compiler_flags = ['-w'],
|
||||
preprocessor_flags = ['-fobjc-arc'],
|
||||
visibility = ['//submodules/Display:Display'],
|
||||
deps = [
|
||||
'//submodules/AsyncDisplayKit:AsyncDisplayKit',
|
||||
],
|
||||
frameworks = [
|
||||
'$SDKROOT/System/Library/Frameworks/Foundation.framework',
|
||||
'$SDKROOT/System/Library/Frameworks/UIKit.framework',
|
||||
],
|
||||
)
|
||||
|
||||
apple_library(
|
||||
name = 'Display',
|
||||
srcs = glob([
|
||||
'Display/*.swift',
|
||||
]),
|
||||
configs = configs_with_config(combined_config([SHARED_CONFIGS, EXTENSION_LIB_SPECIFIC_CONFIG])),
|
||||
swift_compiler_flags = [
|
||||
'-suppress-warnings',
|
||||
'-application-extension',
|
||||
],
|
||||
visibility = ['PUBLIC'],
|
||||
deps = [
|
||||
':DisplayPrivate',
|
||||
'//submodules/AsyncDisplayKit:AsyncDisplayKit',
|
||||
'//submodules/SSignalKit:SwiftSignalKit',
|
||||
],
|
||||
frameworks = [
|
||||
'$SDKROOT/System/Library/Frameworks/Foundation.framework',
|
||||
'$SDKROOT/System/Library/Frameworks/UIKit.framework',
|
||||
'$SDKROOT/System/Library/Frameworks/QuartzCore.framework',
|
||||
'$SDKROOT/System/Library/Frameworks/CoreText.framework',
|
||||
'$SDKROOT/System/Library/Frameworks/CoreGraphics.framework',
|
||||
],
|
||||
)
|
69
submodules/Display/Display/ASTransformLayerNode.swift
Normal file
69
submodules/Display/Display/ASTransformLayerNode.swift
Normal file
@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
class ASTransformLayer: CATransformLayer {
|
||||
override var contents: Any? {
|
||||
get {
|
||||
return nil
|
||||
} set(value) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override var backgroundColor: CGColor? {
|
||||
get {
|
||||
return nil
|
||||
} set(value) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override func setNeedsLayout() {
|
||||
}
|
||||
|
||||
override func layoutSublayers() {
|
||||
}
|
||||
}
|
||||
|
||||
class ASTransformView: UIView {
|
||||
override class var layerClass: AnyClass {
|
||||
return ASTransformLayer.self
|
||||
}
|
||||
}
|
||||
|
||||
open class ASTransformLayerNode: ASDisplayNode {
|
||||
public override init() {
|
||||
super.init()
|
||||
self.setLayerBlock({
|
||||
return ASTransformLayer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open class ASTransformViewNode: ASDisplayNode {
|
||||
public override init() {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return ASTransformView()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
open class ASTransformNode: ASDisplayNode {
|
||||
public init(layerBacked: Bool = true) {
|
||||
if layerBacked {
|
||||
super.init()
|
||||
self.setLayerBlock({
|
||||
return ASTransformLayer()
|
||||
})
|
||||
} else {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return ASTransformView()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
21
submodules/Display/Display/Accessibility.swift
Normal file
21
submodules/Display/Display/Accessibility.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public func addAccessibilityChildren(of node: ASDisplayNode, container: Any, to list: inout [Any]) {
|
||||
if node.isAccessibilityElement {
|
||||
let element = UIAccessibilityElement(accessibilityContainer: container)
|
||||
element.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view)
|
||||
element.accessibilityLabel = node.accessibilityLabel
|
||||
element.accessibilityValue = node.accessibilityValue
|
||||
element.accessibilityTraits = node.accessibilityTraits
|
||||
element.accessibilityHint = node.accessibilityHint
|
||||
element.accessibilityIdentifier = node.accessibilityIdentifier
|
||||
|
||||
//node.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(node.bounds, node.view)
|
||||
list.append(element)
|
||||
} else if let accessibilityElements = node.accessibilityElements {
|
||||
list.append(contentsOf: accessibilityElements)
|
||||
}
|
||||
}
|
||||
|
21
submodules/Display/Display/AccessibilityAreaNode.swift
Normal file
21
submodules/Display/Display/AccessibilityAreaNode.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public final class AccessibilityAreaNode: ASDisplayNode {
|
||||
public var activate: (() -> Bool)?
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.isAccessibilityElement = true
|
||||
}
|
||||
|
||||
override public func accessibilityActivate() -> Bool {
|
||||
return self.activate?() ?? false
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return nil
|
||||
}
|
||||
}
|
138
submodules/Display/Display/ActionSheetButtonItem.swift
Normal file
138
submodules/Display/Display/ActionSheetButtonItem.swift
Normal file
@ -0,0 +1,138 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public enum ActionSheetButtonColor {
|
||||
case accent
|
||||
case destructive
|
||||
case disabled
|
||||
}
|
||||
|
||||
|
||||
public enum ActionSheetButtonFont {
|
||||
case `default`
|
||||
case bold
|
||||
}
|
||||
|
||||
public class ActionSheetButtonItem: ActionSheetItem {
|
||||
public let title: String
|
||||
public let color: ActionSheetButtonColor
|
||||
public let font: ActionSheetButtonFont
|
||||
public let enabled: Bool
|
||||
public let action: () -> Void
|
||||
|
||||
public init(title: String, color: ActionSheetButtonColor = .accent, font: ActionSheetButtonFont = .default, enabled: Bool = true, action: @escaping () -> Void) {
|
||||
self.title = title
|
||||
self.color = color
|
||||
self.font = font
|
||||
self.enabled = enabled
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
||||
let node = ActionSheetButtonNode(theme: theme)
|
||||
node.setItem(self)
|
||||
return node
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ActionSheetItemNode) {
|
||||
guard let node = node as? ActionSheetButtonNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
node.setItem(self)
|
||||
}
|
||||
}
|
||||
|
||||
public class ActionSheetButtonNode: ActionSheetItemNode {
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
public static let defaultFont: UIFont = Font.regular(20.0)
|
||||
public static let boldFont: UIFont = Font.medium(20.0)
|
||||
|
||||
private var item: ActionSheetButtonItem?
|
||||
|
||||
private let button: HighlightTrackingButton
|
||||
private let label: ASTextNode
|
||||
|
||||
override public init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.button = HighlightTrackingButton()
|
||||
|
||||
self.label = ASTextNode()
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.label.maximumNumberOfLines = 1
|
||||
self.label.displaysAsynchronously = false
|
||||
self.label.truncationMode = .byTruncatingTail
|
||||
|
||||
super.init(theme: theme)
|
||||
|
||||
self.view.addSubview(self.button)
|
||||
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.addSubnode(self.label)
|
||||
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setItem(_ item: ActionSheetButtonItem) {
|
||||
self.item = item
|
||||
|
||||
let textColor: UIColor
|
||||
let textFont: UIFont
|
||||
switch item.color {
|
||||
case .accent:
|
||||
textColor = self.theme.standardActionTextColor
|
||||
case .destructive:
|
||||
textColor = self.theme.destructiveActionTextColor
|
||||
case .disabled:
|
||||
textColor = self.theme.disabledActionTextColor
|
||||
}
|
||||
switch item.font {
|
||||
case .default:
|
||||
textFont = ActionSheetButtonNode.defaultFont
|
||||
case .bold:
|
||||
textFont = ActionSheetButtonNode.boldFont
|
||||
}
|
||||
self.label.attributedText = NSAttributedString(string: item.title, font: textFont, textColor: textColor)
|
||||
|
||||
self.button.isEnabled = item.enabled
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: constrainedSize.width, height: 57.0)
|
||||
}
|
||||
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 10.0), height: size.height))
|
||||
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
if let item = self.item {
|
||||
item.action()
|
||||
}
|
||||
}
|
||||
}
|
147
submodules/Display/Display/ActionSheetCheckboxItem.swift
Normal file
147
submodules/Display/Display/ActionSheetCheckboxItem.swift
Normal file
@ -0,0 +1,147 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public enum ActionSheetCheckboxStyle {
|
||||
case `default`
|
||||
case alignRight
|
||||
}
|
||||
|
||||
public class ActionSheetCheckboxItem: ActionSheetItem {
|
||||
public let title: String
|
||||
public let label: String
|
||||
public let value: Bool
|
||||
public let style: ActionSheetCheckboxStyle
|
||||
public let action: (Bool) -> Void
|
||||
|
||||
public init(title: String, label: String, value: Bool, style: ActionSheetCheckboxStyle = .default, action: @escaping (Bool) -> Void) {
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.style = style
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
||||
let node = ActionSheetCheckboxItemNode(theme: theme)
|
||||
node.setItem(self)
|
||||
return node
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ActionSheetItemNode) {
|
||||
guard let node = node as? ActionSheetCheckboxItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
node.setItem(self)
|
||||
}
|
||||
}
|
||||
|
||||
public class ActionSheetCheckboxItemNode: ActionSheetItemNode {
|
||||
public static let defaultFont: UIFont = Font.regular(20.0)
|
||||
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
private var item: ActionSheetCheckboxItem?
|
||||
|
||||
private let button: HighlightTrackingButton
|
||||
private let titleNode: ImmediateTextNode
|
||||
private let labelNode: ImmediateTextNode
|
||||
private let checkNode: ASImageNode
|
||||
|
||||
override public init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.button = HighlightTrackingButton()
|
||||
|
||||
self.titleNode = ImmediateTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.labelNode = ImmediateTextNode()
|
||||
self.labelNode.maximumNumberOfLines = 1
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
|
||||
self.checkNode = ASImageNode()
|
||||
self.checkNode.isUserInteractionEnabled = false
|
||||
self.checkNode.displayWithoutProcessing = true
|
||||
self.checkNode.displaysAsynchronously = false
|
||||
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(theme.controlAccentColor.cgColor)
|
||||
context.setLineWidth(2.0)
|
||||
context.move(to: CGPoint(x: 12.0, y: 1.0))
|
||||
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
|
||||
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
|
||||
context.strokePath()
|
||||
})
|
||||
|
||||
super.init(theme: theme)
|
||||
|
||||
self.view.addSubview(self.button)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.checkNode)
|
||||
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setItem(_ item: ActionSheetCheckboxItem) {
|
||||
self.item = item
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: item.title, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.primaryTextColor)
|
||||
self.labelNode.attributedText = NSAttributedString(string: item.label, font: ActionSheetCheckboxItemNode.defaultFont, textColor: self.theme.secondaryTextColor)
|
||||
self.checkNode.isHidden = !item.value
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: constrainedSize.width, height: 57.0)
|
||||
}
|
||||
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
var titleOrigin: CGFloat = 44.0
|
||||
var checkOrigin: CGFloat = 22.0
|
||||
if let item = self.item, item.style == .alignRight {
|
||||
titleOrigin = 24.0
|
||||
checkOrigin = size.width - 22.0
|
||||
}
|
||||
|
||||
let labelSize = self.labelNode.updateLayout(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height))
|
||||
let titleSize = self.titleNode.updateLayout(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: titleOrigin, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
|
||||
if let image = self.checkNode.image {
|
||||
self.checkNode.frame = CGRect(origin: CGPoint(x: floor(checkOrigin - (image.size.width / 2.0)), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
if let item = self.item {
|
||||
item.action(!item.value)
|
||||
}
|
||||
}
|
||||
}
|
82
submodules/Display/Display/ActionSheetController.swift
Normal file
82
submodules/Display/Display/ActionSheetController.swift
Normal file
@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
open class ActionSheetController: ViewController, PresentableController {
|
||||
private var actionSheetNode: ActionSheetControllerNode {
|
||||
return self.displayNode as! ActionSheetControllerNode
|
||||
}
|
||||
|
||||
public var theme: ActionSheetControllerTheme {
|
||||
didSet {
|
||||
if oldValue != self.theme {
|
||||
self.actionSheetNode.theme = self.theme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var groups: [ActionSheetItemGroup] = []
|
||||
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public var dismissed: ((Bool) -> Void)?
|
||||
|
||||
public init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
self.actionSheetNode.animateOut(cancelled: false)
|
||||
}
|
||||
}
|
||||
|
||||
open override func loadDisplayNode() {
|
||||
self.displayNode = ActionSheetControllerNode(theme: self.theme)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.actionSheetNode.dismiss = { [weak self] cancelled in
|
||||
self?.dismissed?(cancelled)
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
|
||||
self.actionSheetNode.setGroups(self.groups)
|
||||
}
|
||||
|
||||
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.actionSheetNode.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
open override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.viewDidAppear(completion: {})
|
||||
}
|
||||
|
||||
public func viewDidAppear(completion: @escaping () -> Void) {
|
||||
self.actionSheetNode.animateIn(completion: completion)
|
||||
}
|
||||
|
||||
public func setItemGroups(_ groups: [ActionSheetItemGroup]) {
|
||||
self.groups = groups
|
||||
if self.isViewLoaded {
|
||||
self.actionSheetNode.setGroups(groups)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
|
||||
if self.isViewLoaded {
|
||||
self.actionSheetNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f)
|
||||
}
|
||||
}
|
||||
}
|
200
submodules/Display/Display/ActionSheetControllerNode.swift
Normal file
200
submodules/Display/Display/ActionSheetControllerNode.swift
Normal file
@ -0,0 +1,200 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
private let containerInsets = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0)
|
||||
|
||||
private class ActionSheetControllerNodeScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class ActionSheetControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
var theme: ActionSheetControllerTheme {
|
||||
didSet {
|
||||
self.itemGroupsContainerNode.theme = self.theme
|
||||
self.updateTheme()
|
||||
}
|
||||
}
|
||||
|
||||
private let dismissTapView: UIView
|
||||
|
||||
private let leftDimView: UIView
|
||||
private let rightDimView: UIView
|
||||
private let topDimView: UIView
|
||||
private let bottomDimView: UIView
|
||||
|
||||
private let itemGroupsContainerNode: ActionSheetItemGroupsContainerNode
|
||||
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
var dismiss: (Bool) -> Void = { _ in }
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.scrollView = ActionSheetControllerNodeScrollView()
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
|
||||
self.dismissTapView = UIView()
|
||||
|
||||
self.leftDimView = UIView()
|
||||
self.leftDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.rightDimView = UIView()
|
||||
self.rightDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.topDimView = UIView()
|
||||
self.topDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.bottomDimView = UIView()
|
||||
self.bottomDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.itemGroupsContainerNode = ActionSheetItemGroupsContainerNode(theme: self.theme)
|
||||
|
||||
super.init()
|
||||
|
||||
self.scrollView.delegate = self
|
||||
|
||||
self.view.addSubview(self.scrollView)
|
||||
|
||||
self.scrollView.addSubview(self.dismissTapView)
|
||||
|
||||
self.scrollView.addSubview(self.leftDimView)
|
||||
self.scrollView.addSubview(self.rightDimView)
|
||||
self.scrollView.addSubview(self.topDimView)
|
||||
self.scrollView.addSubview(self.bottomDimView)
|
||||
|
||||
self.dismissTapView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTap(_:))))
|
||||
|
||||
self.scrollView.addSubnode(self.itemGroupsContainerNode)
|
||||
|
||||
self.updateTheme()
|
||||
}
|
||||
|
||||
func updateTheme() {
|
||||
self.leftDimView.backgroundColor = self.theme.dimColor
|
||||
self.rightDimView.backgroundColor = self.theme.dimColor
|
||||
self.topDimView.backgroundColor = self.theme.dimColor
|
||||
self.bottomDimView.backgroundColor = self.theme.dimColor
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var insets = layout.insets(options: [.statusBar])
|
||||
|
||||
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
insets.left = floor((layout.size.width - containerWidth) / 2.0)
|
||||
insets.right = insets.left
|
||||
if !insets.bottom.isZero {
|
||||
insets.bottom -= 12.0
|
||||
}
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
self.scrollView.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
self.dismissTapView.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
|
||||
self.itemGroupsContainerNode.measure(CGSize(width: layout.size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: layout.size.height - containerInsets.top - containerInsets.bottom - insets.top - insets.bottom))
|
||||
self.itemGroupsContainerNode.frame = CGRect(origin: CGPoint(x: insets.left + containerInsets.left, y: layout.size.height - insets.bottom - containerInsets.bottom - self.itemGroupsContainerNode.calculatedSize.height), size: self.itemGroupsContainerNode.calculatedSize)
|
||||
self.itemGroupsContainerNode.layout()
|
||||
|
||||
self.updateScrollDimViews(size: layout.size, insets: insets)
|
||||
}
|
||||
|
||||
func animateIn(completion: @escaping () -> Void) {
|
||||
let tempDimView = UIView()
|
||||
tempDimView.backgroundColor = self.theme.dimColor
|
||||
tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height)
|
||||
self.view.addSubview(tempDimView)
|
||||
|
||||
for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] {
|
||||
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
|
||||
self.itemGroupsContainerNode.animateDimViewsAlpha(from: 0.0, to: 1.0, duration: 0.4)
|
||||
|
||||
self.layer.animateBounds(from: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), to: self.bounds, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak tempDimView] _ in
|
||||
tempDimView?.removeFromSuperview()
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func animateOut(cancelled: Bool) {
|
||||
let tempDimView = UIView()
|
||||
tempDimView.backgroundColor = self.theme.dimColor
|
||||
tempDimView.frame = self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height)
|
||||
self.view.addSubview(tempDimView)
|
||||
|
||||
for node in [tempDimView, self.topDimView, self.leftDimView, self.rightDimView, self.bottomDimView] {
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
self.itemGroupsContainerNode.animateDimViewsAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
|
||||
self.layer.animateBounds(from: self.bounds, to: self.bounds.offsetBy(dx: 0.0, dy: -self.bounds.size.height), duration: 0.35, timingFunction: kCAMediaTimingFunctionEaseOut, removeOnCompletion: false, completion: { [weak self, weak tempDimView] _ in
|
||||
tempDimView?.removeFromSuperview()
|
||||
|
||||
self?.dismiss(cancelled)
|
||||
})
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc func dimNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.animateOut(cancelled: true)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if let layout = self.validLayout {
|
||||
var insets = layout.insets(options: [.statusBar])
|
||||
|
||||
let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: layout.safeInsets.left)
|
||||
|
||||
insets.left = floor((layout.size.width - containerWidth) / 2.0)
|
||||
insets.right = insets.left
|
||||
|
||||
self.updateScrollDimViews(size: layout.size, insets: insets)
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
let contentOffset = self.scrollView.contentOffset
|
||||
let additionalTopHeight = max(0.0, -contentOffset.y)
|
||||
|
||||
if additionalTopHeight >= 30.0 {
|
||||
self.animateOut(cancelled: true)
|
||||
}
|
||||
}
|
||||
|
||||
func updateScrollDimViews(size: CGSize, insets: UIEdgeInsets) {
|
||||
let additionalTopHeight = max(0.0, -self.scrollView.contentOffset.y)
|
||||
let additionalBottomHeight = -min(0.0, -self.scrollView.contentOffset.y)
|
||||
|
||||
self.topDimView.frame = CGRect(x: containerInsets.left + insets.left, y: -additionalTopHeight, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, self.itemGroupsContainerNode.frame.minY + additionalTopHeight))
|
||||
self.bottomDimView.frame = CGRect(x: containerInsets.left + insets.left, y: self.itemGroupsContainerNode.frame.maxY, width: size.width - containerInsets.left - containerInsets.right - insets.left - insets.right, height: max(0.0, size.height - self.itemGroupsContainerNode.frame.maxY + additionalBottomHeight))
|
||||
|
||||
self.leftDimView.frame = CGRect(x: 0.0, y: -additionalTopHeight, width: containerInsets.left + insets.left, height: size.height + additionalTopHeight + additionalBottomHeight)
|
||||
self.rightDimView.frame = CGRect(x: size.width - containerInsets.right - insets.right, y: -additionalTopHeight, width: containerInsets.right + insets.right, height: size.height + additionalTopHeight + additionalBottomHeight)
|
||||
}
|
||||
|
||||
func setGroups(_ groups: [ActionSheetItemGroup]) {
|
||||
self.itemGroupsContainerNode.setGroups(groups)
|
||||
}
|
||||
|
||||
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
|
||||
self.itemGroupsContainerNode.updateItem(groupIndex: groupIndex, itemIndex: itemIndex, f)
|
||||
}
|
||||
}
|
6
submodules/Display/Display/ActionSheetItem.swift
Normal file
6
submodules/Display/Display/ActionSheetItem.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
public protocol ActionSheetItem {
|
||||
func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode
|
||||
func updateNode(_ node: ActionSheetItemNode) -> Void
|
||||
}
|
11
submodules/Display/Display/ActionSheetItemGroup.swift
Normal file
11
submodules/Display/Display/ActionSheetItemGroup.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import UIKit
|
||||
|
||||
public final class ActionSheetItemGroup {
|
||||
let items: [ActionSheetItem]
|
||||
let leadingVisibleNodeCount: CGFloat?
|
||||
|
||||
public init(items: [ActionSheetItem], leadingVisibleNodeCount: CGFloat? = nil) {
|
||||
self.items = items
|
||||
self.leadingVisibleNodeCount = leadingVisibleNodeCount
|
||||
}
|
||||
}
|
223
submodules/Display/Display/ActionSheetItemGroupNode.swift
Normal file
223
submodules/Display/Display/ActionSheetItemGroupNode.swift
Normal file
@ -0,0 +1,223 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
private class ActionSheetItemGroupNodeScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class ActionSheetItemGroupNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
private let centerDimView: UIImageView
|
||||
private let topDimView: UIView
|
||||
private let bottomDimView: UIView
|
||||
let trailingDimView: UIView
|
||||
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let backgroundEffectView: UIVisualEffectView
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var itemNodes: [ActionSheetItemNode] = []
|
||||
private var leadingVisibleNodeCount: CGFloat = 100.0
|
||||
|
||||
var respectInputHeight = true
|
||||
|
||||
init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.centerDimView = UIImageView()
|
||||
self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: self.theme.dimColor)
|
||||
|
||||
self.topDimView = UIView()
|
||||
self.topDimView.backgroundColor = self.theme.dimColor
|
||||
self.topDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.bottomDimView = UIView()
|
||||
self.bottomDimView.backgroundColor = self.theme.dimColor
|
||||
self.bottomDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.trailingDimView = UIView()
|
||||
self.trailingDimView.backgroundColor = self.theme.dimColor
|
||||
|
||||
self.clippingNode = ASDisplayNode()
|
||||
self.clippingNode.clipsToBounds = true
|
||||
self.clippingNode.cornerRadius = 16.0
|
||||
|
||||
self.backgroundEffectView = UIVisualEffectView(effect: UIBlurEffect(style: self.theme.backgroundType == .light ? .light : .dark))
|
||||
|
||||
self.scrollView = ActionSheetItemGroupNodeScrollView()
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.centerDimView)
|
||||
self.view.addSubview(self.topDimView)
|
||||
self.view.addSubview(self.bottomDimView)
|
||||
self.view.addSubview(self.trailingDimView)
|
||||
|
||||
self.scrollView.delegate = self
|
||||
|
||||
self.clippingNode.view.addSubview(self.backgroundEffectView)
|
||||
self.clippingNode.view.addSubview(self.scrollView)
|
||||
|
||||
self.addSubnode(self.clippingNode)
|
||||
}
|
||||
|
||||
func updateItemNodes(_ nodes: [ActionSheetItemNode], leadingVisibleNodeCount: CGFloat = 1000.0) {
|
||||
for node in self.itemNodes {
|
||||
if !nodes.contains(where: { $0 === node }) {
|
||||
node.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
for node in nodes {
|
||||
if !self.itemNodes.contains(where: { $0 === node }) {
|
||||
self.scrollView.addSubnode(node)
|
||||
}
|
||||
}
|
||||
|
||||
self.itemNodes = nodes
|
||||
self.leadingVisibleNodeCount = leadingVisibleNodeCount
|
||||
self.invalidateCalculatedLayout()
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
var itemNodesHeight: CGFloat = 0.0
|
||||
var leadingVisibleNodeSize: CGFloat = 0.0
|
||||
|
||||
var i = 0
|
||||
for node in self.itemNodes {
|
||||
if CGFloat(0.0).isLess(than: itemNodesHeight) {
|
||||
itemNodesHeight += UIScreenPixel
|
||||
}
|
||||
let size = node.measure(constrainedSize)
|
||||
itemNodesHeight += size.height
|
||||
|
||||
if ceil(CGFloat(i)).isLessThanOrEqualTo(leadingVisibleNodeCount) {
|
||||
if CGFloat(0.0).isLess(than: leadingVisibleNodeSize) {
|
||||
leadingVisibleNodeSize += UIScreenPixel
|
||||
}
|
||||
let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i))
|
||||
leadingVisibleNodeSize += size.height * factor
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
return CGSize(width: constrainedSize.width, height: min(floorToScreenPixels(itemNodesHeight), constrainedSize.height))
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
let scrollViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize)
|
||||
var updateOffset = false
|
||||
if !self.scrollView.frame.equalTo(scrollViewFrame) {
|
||||
self.scrollView.frame = scrollViewFrame
|
||||
updateOffset = true
|
||||
}
|
||||
|
||||
let backgroundEffectViewFrame = CGRect(origin: CGPoint(), size: self.calculatedSize)
|
||||
if !self.backgroundEffectView.frame.equalTo(backgroundEffectViewFrame) {
|
||||
self.backgroundEffectView.frame = backgroundEffectViewFrame
|
||||
}
|
||||
|
||||
var itemNodesHeight: CGFloat = 0.0
|
||||
var leadingVisibleNodeSize: CGFloat = 0.0
|
||||
|
||||
var i = 0
|
||||
for node in self.itemNodes {
|
||||
if CGFloat(0.0).isLess(than: itemNodesHeight) {
|
||||
itemNodesHeight += UIScreenPixel
|
||||
}
|
||||
node.frame = CGRect(origin: CGPoint(x: 0.0, y: itemNodesHeight), size: node.calculatedSize)
|
||||
itemNodesHeight += node.calculatedSize.height
|
||||
|
||||
if CGFloat(i).isLessThanOrEqualTo(leadingVisibleNodeCount) {
|
||||
if CGFloat(0.0).isLess(than: leadingVisibleNodeSize) {
|
||||
leadingVisibleNodeSize += UIScreenPixel
|
||||
}
|
||||
let factor: CGFloat = min(1.0, leadingVisibleNodeCount - CGFloat(i))
|
||||
leadingVisibleNodeSize += node.calculatedSize.height * factor
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
let scrollViewContentSize = CGSize(width: self.calculatedSize.width, height: itemNodesHeight)
|
||||
if !self.scrollView.contentSize.equalTo(scrollViewContentSize) {
|
||||
self.scrollView.contentSize = scrollViewContentSize
|
||||
}
|
||||
let scrollViewContentInsets = UIEdgeInsets(top: max(0.0, self.calculatedSize.height - leadingVisibleNodeSize), left: 0.0, bottom: 0.0, right: 0.0)
|
||||
|
||||
if !UIEdgeInsetsEqualToEdgeInsets(self.scrollView.contentInset, scrollViewContentInsets) {
|
||||
self.scrollView.contentInset = scrollViewContentInsets
|
||||
}
|
||||
|
||||
if updateOffset {
|
||||
self.scrollView.contentOffset = CGPoint(x: 0.0, y: -scrollViewContentInsets.top)
|
||||
}
|
||||
|
||||
self.updateOverscroll()
|
||||
}
|
||||
|
||||
private func currentVerticalOverscroll() -> CGFloat {
|
||||
var verticalOverscroll: CGFloat = 0.0
|
||||
if scrollView.contentOffset.y < 0.0 {
|
||||
verticalOverscroll = scrollView.contentOffset.y
|
||||
} else if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.size.height {
|
||||
verticalOverscroll = scrollView.contentOffset.y - (scrollView.contentSize.height - scrollView.bounds.size.height)
|
||||
}
|
||||
return verticalOverscroll
|
||||
}
|
||||
|
||||
private func currentRealVerticalOverscroll() -> CGFloat {
|
||||
var verticalOverscroll: CGFloat = 0.0
|
||||
if scrollView.contentOffset.y < 0.0 {
|
||||
verticalOverscroll = scrollView.contentOffset.y
|
||||
} else if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.size.height {
|
||||
verticalOverscroll = scrollView.contentOffset.y - (scrollView.contentSize.height - scrollView.bounds.size.height)
|
||||
}
|
||||
return verticalOverscroll
|
||||
}
|
||||
|
||||
private func updateOverscroll() {
|
||||
let verticalOverscroll = self.currentVerticalOverscroll()
|
||||
|
||||
self.clippingNode.layer.sublayerTransform = CATransform3DMakeTranslation(0.0, min(0.0, verticalOverscroll), 0.0)
|
||||
let clippingNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -verticalOverscroll)), size: CGSize(width: self.calculatedSize.width, height: self.calculatedSize.height - abs(verticalOverscroll)))
|
||||
if !self.clippingNode.frame.equalTo(clippingNodeFrame) {
|
||||
self.clippingNode.frame = clippingNodeFrame
|
||||
|
||||
self.centerDimView.frame = clippingNodeFrame
|
||||
self.topDimView.frame = CGRect(x: 0.0, y: 0.0, width: clippingNodeFrame.size.width, height: max(0.0, clippingNodeFrame.minY))
|
||||
self.bottomDimView.frame = CGRect(x: 0.0, y: clippingNodeFrame.maxY, width: clippingNodeFrame.size.width, height: max(0.0, self.bounds.size.height - clippingNodeFrame.maxY))
|
||||
}
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
self.updateOverscroll()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.clippingNode.frame.contains(point) {
|
||||
return super.hitTest(point, with: event)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) {
|
||||
for node in [self.centerDimView, self.topDimView, self.bottomDimView] {
|
||||
node.layer.animateAlpha(from: from, to: to, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
func itemNode(at index: Int) -> ActionSheetItemNode {
|
||||
return self.itemNodes[index]
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
private let groupSpacing: CGFloat = 8.0
|
||||
|
||||
final class ActionSheetItemGroupsContainerNode: ASDisplayNode {
|
||||
var theme: ActionSheetControllerTheme {
|
||||
didSet {
|
||||
self.setGroups(self.groups)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private var groups: [ActionSheetItemGroup] = []
|
||||
private var groupNodes: [ActionSheetItemGroupNode] = []
|
||||
|
||||
init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func setGroups(_ groups: [ActionSheetItemGroup]) {
|
||||
self.groups = groups
|
||||
|
||||
for groupNode in self.groupNodes {
|
||||
groupNode.removeFromSupernode()
|
||||
}
|
||||
self.groupNodes.removeAll()
|
||||
|
||||
for group in groups {
|
||||
let groupNode = ActionSheetItemGroupNode(theme: self.theme)
|
||||
groupNode.updateItemNodes(group.items.map({ $0.node(theme: self.theme) }), leadingVisibleNodeCount: group.leadingVisibleNodeCount ?? 1000.0)
|
||||
self.groupNodes.append(groupNode)
|
||||
self.addSubnode(groupNode)
|
||||
}
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
var groupsHeight: CGFloat = 0.0
|
||||
|
||||
for groupNode in self.groupNodes.reversed() {
|
||||
if CGFloat(0.0).isLess(than: groupsHeight) {
|
||||
groupsHeight += groupSpacing
|
||||
}
|
||||
|
||||
let size = groupNode.measure(CGSize(width: constrainedSize.width, height: max(0.0, constrainedSize.height - groupsHeight)))
|
||||
groupsHeight += size.height
|
||||
}
|
||||
|
||||
return CGSize(width: constrainedSize.width, height: min(groupsHeight, constrainedSize.height))
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
var groupsHeight: CGFloat = 0.0
|
||||
for i in 0 ..< self.groupNodes.count {
|
||||
let groupNode = self.groupNodes[i]
|
||||
|
||||
let size = groupNode.calculatedSize
|
||||
|
||||
if i != 0 {
|
||||
groupsHeight += groupSpacing
|
||||
self.groupNodes[i - 1].trailingDimView.frame = CGRect(x: 0.0, y: groupNodes[i - 1].bounds.size.height, width: size.width, height: groupSpacing)
|
||||
}
|
||||
|
||||
groupNode.frame = CGRect(origin: CGPoint(x: 0.0, y: groupsHeight), size: size)
|
||||
groupNode.trailingDimView.frame = CGRect()
|
||||
|
||||
groupsHeight += size.height
|
||||
}
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
for groupNode in self.groupNodes {
|
||||
if groupNode.frame.contains(point) {
|
||||
return groupNode.hitTest(self.convert(point, to: groupNode), with: event)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func animateDimViewsAlpha(from: CGFloat, to: CGFloat, duration: Double) {
|
||||
for node in self.groupNodes {
|
||||
node.animateDimViewsAlpha(from: from, to: to, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
public func updateItem(groupIndex: Int, itemIndex: Int, _ f: (ActionSheetItem) -> ActionSheetItem) {
|
||||
var item = self.groups[groupIndex].items[itemIndex]
|
||||
let itemNode = self.groupNodes[groupIndex].itemNode(at: itemIndex)
|
||||
item = f(item)
|
||||
item.updateNode(itemNode)
|
||||
|
||||
var groupItems = self.groups[groupIndex].items
|
||||
groupItems[itemIndex] = item
|
||||
|
||||
self.groups[groupIndex] = ActionSheetItemGroup(items: groupItems)
|
||||
}
|
||||
}
|
33
submodules/Display/Display/ActionSheetItemNode.swift
Normal file
33
submodules/Display/Display/ActionSheetItemNode.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class ActionSheetItemNode: ASDisplayNode {
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
public let backgroundNode: ASDisplayNode
|
||||
private let overflowSeparatorNode: ASDisplayNode
|
||||
|
||||
public init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = self.theme.itemBackgroundColor
|
||||
|
||||
self.overflowSeparatorNode = ASDisplayNode()
|
||||
self.overflowSeparatorNode.backgroundColor = self.theme.itemHighlightedBackgroundColor
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.overflowSeparatorNode)
|
||||
}
|
||||
|
||||
open override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: constrainedSize.width, height: 57.0)
|
||||
}
|
||||
|
||||
open override func layout() {
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: self.calculatedSize)
|
||||
self.overflowSeparatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: self.calculatedSize.height), size: CGSize(width: self.calculatedSize.width, height: UIScreenPixel))
|
||||
}
|
||||
}
|
104
submodules/Display/Display/ActionSheetSwitchItem.swift
Normal file
104
submodules/Display/Display/ActionSheetSwitchItem.swift
Normal file
@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public class ActionSheetSwitchItem: ActionSheetItem {
|
||||
public let title: String
|
||||
public let isOn: Bool
|
||||
public let action: (Bool) -> Void
|
||||
|
||||
public init(title: String, isOn: Bool, action: @escaping (Bool) -> Void) {
|
||||
self.title = title
|
||||
self.isOn = isOn
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
||||
let node = ActionSheetSwitchNode(theme: theme)
|
||||
node.setItem(self)
|
||||
return node
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ActionSheetItemNode) {
|
||||
guard let node = node as? ActionSheetSwitchNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
node.setItem(self)
|
||||
}
|
||||
}
|
||||
|
||||
public class ActionSheetSwitchNode: ActionSheetItemNode {
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
private var item: ActionSheetSwitchItem?
|
||||
|
||||
private let button: HighlightTrackingButton
|
||||
private let label: ASTextNode
|
||||
private let switchNode: SwitchNode
|
||||
|
||||
override public init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.button = HighlightTrackingButton()
|
||||
|
||||
self.label = ASTextNode()
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.label.maximumNumberOfLines = 1
|
||||
self.label.displaysAsynchronously = false
|
||||
self.label.truncationMode = .byTruncatingTail
|
||||
|
||||
self.switchNode = SwitchNode()
|
||||
self.switchNode.frameColor = theme.switchFrameColor
|
||||
self.switchNode.contentColor = theme.switchContentColor
|
||||
self.switchNode.handleColor = theme.switchHandleColor
|
||||
|
||||
super.init(theme: theme)
|
||||
|
||||
self.view.addSubview(self.button)
|
||||
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.addSubnode(self.label)
|
||||
self.addSubnode(self.switchNode)
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
self.switchNode.valueUpdated = { [weak self] value in
|
||||
self?.item?.action(value)
|
||||
}
|
||||
}
|
||||
|
||||
func setItem(_ item: ActionSheetSwitchItem) {
|
||||
self.item = item
|
||||
|
||||
self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetButtonNode.defaultFont, textColor: self.theme.primaryTextColor)
|
||||
|
||||
self.switchNode.isOn = item.isOn
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
return CGSize(width: constrainedSize.width, height: 57.0)
|
||||
}
|
||||
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 51.0 - 16.0 * 2.0), height: size.height))
|
||||
self.label.frame = CGRect(origin: CGPoint(x: 16.0, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
|
||||
let switchSize = CGSize(width: 51.0, height: 31.0)
|
||||
self.switchNode.frame = CGRect(origin: CGPoint(x: size.width - 16.0 - switchSize.width, y: floor((size.height - switchSize.height) / 2.0)), size: switchSize)
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
let value = !self.switchNode.isOn
|
||||
self.switchNode.setOn(value, animated: true)
|
||||
self.item?.action(value)
|
||||
}
|
||||
}
|
73
submodules/Display/Display/ActionSheetTextItem.swift
Normal file
73
submodules/Display/Display/ActionSheetTextItem.swift
Normal file
@ -0,0 +1,73 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public class ActionSheetTextItem: ActionSheetItem {
|
||||
public let title: String
|
||||
|
||||
public init(title: String) {
|
||||
self.title = title
|
||||
}
|
||||
|
||||
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
||||
let node = ActionSheetTextNode(theme: theme)
|
||||
node.setItem(self)
|
||||
return node
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ActionSheetItemNode) {
|
||||
guard let node = node as? ActionSheetTextNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
node.setItem(self)
|
||||
}
|
||||
}
|
||||
|
||||
public class ActionSheetTextNode: ActionSheetItemNode {
|
||||
public static let defaultFont: UIFont = Font.regular(13.0)
|
||||
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
private var item: ActionSheetTextItem?
|
||||
|
||||
private let label: ASTextNode
|
||||
|
||||
override public init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.label = ASTextNode()
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.label.maximumNumberOfLines = 0
|
||||
self.label.displaysAsynchronously = false
|
||||
self.label.truncationMode = .byTruncatingTail
|
||||
|
||||
super.init(theme: theme)
|
||||
|
||||
self.label.isUserInteractionEnabled = false
|
||||
self.addSubnode(self.label)
|
||||
}
|
||||
|
||||
func setItem(_ item: ActionSheetTextItem) {
|
||||
self.item = item
|
||||
|
||||
self.label.attributedText = NSAttributedString(string: item.title, font: ActionSheetTextNode.defaultFont, textColor: self.theme.secondaryTextColor, paragraphAlignment: .center)
|
||||
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
|
||||
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
let labelSize = self.label.measure(CGSize(width: max(1.0, constrainedSize.width - 20.0), height: constrainedSize.height))
|
||||
return CGSize(width: constrainedSize.width, height: max(57.0, labelSize.height + 32.0))
|
||||
}
|
||||
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
let labelSize = self.label.measure(CGSize(width: max(1.0, size.width - 20.0), height: size.height))
|
||||
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
}
|
||||
}
|
87
submodules/Display/Display/ActionSheetTheme.swift
Normal file
87
submodules/Display/Display/ActionSheetTheme.swift
Normal file
@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum ActionSheetControllerThemeBackgroundType {
|
||||
case light
|
||||
case dark
|
||||
}
|
||||
|
||||
public final class ActionSheetControllerTheme: Equatable {
|
||||
public let dimColor: UIColor
|
||||
public let backgroundType: ActionSheetControllerThemeBackgroundType
|
||||
public let itemBackgroundColor: UIColor
|
||||
public let itemHighlightedBackgroundColor: UIColor
|
||||
public let standardActionTextColor: UIColor
|
||||
public let destructiveActionTextColor: UIColor
|
||||
public let disabledActionTextColor: UIColor
|
||||
public let primaryTextColor: UIColor
|
||||
public let secondaryTextColor: UIColor
|
||||
public let controlAccentColor: UIColor
|
||||
public let controlColor: UIColor
|
||||
public let switchFrameColor: UIColor
|
||||
public let switchContentColor: UIColor
|
||||
public let switchHandleColor: UIColor
|
||||
|
||||
public init(dimColor: UIColor, backgroundType: ActionSheetControllerThemeBackgroundType, itemBackgroundColor: UIColor, itemHighlightedBackgroundColor: UIColor, standardActionTextColor: UIColor, destructiveActionTextColor: UIColor, disabledActionTextColor: UIColor, primaryTextColor: UIColor, secondaryTextColor: UIColor, controlAccentColor: UIColor, controlColor: UIColor, switchFrameColor: UIColor, switchContentColor: UIColor, switchHandleColor: UIColor) {
|
||||
self.dimColor = dimColor
|
||||
self.backgroundType = backgroundType
|
||||
self.itemBackgroundColor = itemBackgroundColor
|
||||
self.itemHighlightedBackgroundColor = itemHighlightedBackgroundColor
|
||||
self.standardActionTextColor = standardActionTextColor
|
||||
self.destructiveActionTextColor = destructiveActionTextColor
|
||||
self.disabledActionTextColor = disabledActionTextColor
|
||||
self.primaryTextColor = primaryTextColor
|
||||
self.secondaryTextColor = secondaryTextColor
|
||||
self.controlAccentColor = controlAccentColor
|
||||
self.controlColor = controlColor
|
||||
self.switchFrameColor = switchFrameColor
|
||||
self.switchContentColor = switchContentColor
|
||||
self.switchHandleColor = switchHandleColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: ActionSheetControllerTheme, rhs: ActionSheetControllerTheme) -> Bool {
|
||||
if lhs.dimColor != rhs.dimColor {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundType != rhs.backgroundType {
|
||||
return false
|
||||
}
|
||||
if lhs.itemBackgroundColor != rhs.itemBackgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.itemHighlightedBackgroundColor != rhs.itemHighlightedBackgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.standardActionTextColor != rhs.standardActionTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.destructiveActionTextColor != rhs.destructiveActionTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.disabledActionTextColor != rhs.disabledActionTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.primaryTextColor != rhs.primaryTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.secondaryTextColor != rhs.secondaryTextColor {
|
||||
return false
|
||||
}
|
||||
if lhs.controlAccentColor != rhs.controlAccentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.controlColor != rhs.controlColor {
|
||||
return false
|
||||
}
|
||||
if lhs.switchFrameColor != rhs.switchFrameColor {
|
||||
return false
|
||||
}
|
||||
if lhs.switchContentColor != rhs.switchContentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.switchHandleColor != rhs.switchHandleColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
21
submodules/Display/Display/AlertContentNode.swift
Normal file
21
submodules/Display/Display/AlertContentNode.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class AlertContentNode: ASDisplayNode {
|
||||
open var requestLayout: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
open var dismissOnOutsideTap: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
assertionFailure()
|
||||
|
||||
return CGSize()
|
||||
}
|
||||
|
||||
open func updateTheme(_ theme: AlertControllerTheme) {
|
||||
|
||||
}
|
||||
}
|
133
submodules/Display/Display/AlertController.swift
Normal file
133
submodules/Display/Display/AlertController.swift
Normal file
@ -0,0 +1,133 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public enum AlertControllerThemeBackgroundType {
|
||||
case light
|
||||
case dark
|
||||
}
|
||||
|
||||
public final class AlertControllerTheme: Equatable {
|
||||
public let backgroundType: ActionSheetControllerThemeBackgroundType
|
||||
public let backgroundColor: UIColor
|
||||
public let separatorColor: UIColor
|
||||
public let highlightedItemColor: UIColor
|
||||
public let primaryColor: UIColor
|
||||
public let secondaryColor: UIColor
|
||||
public let accentColor: UIColor
|
||||
public let destructiveColor: UIColor
|
||||
public let disabledColor: UIColor
|
||||
|
||||
public init(backgroundType: ActionSheetControllerThemeBackgroundType, backgroundColor: UIColor, separatorColor: UIColor, highlightedItemColor: UIColor, primaryColor: UIColor, secondaryColor: UIColor, accentColor: UIColor, destructiveColor: UIColor, disabledColor: UIColor) {
|
||||
self.backgroundType = backgroundType
|
||||
self.backgroundColor = backgroundColor
|
||||
self.separatorColor = separatorColor
|
||||
self.highlightedItemColor = highlightedItemColor
|
||||
self.primaryColor = primaryColor
|
||||
self.secondaryColor = secondaryColor
|
||||
self.accentColor = accentColor
|
||||
self.destructiveColor = destructiveColor
|
||||
self.disabledColor = disabledColor
|
||||
}
|
||||
|
||||
public static func ==(lhs: AlertControllerTheme, rhs: AlertControllerTheme) -> Bool {
|
||||
if lhs.backgroundType != rhs.backgroundType {
|
||||
return false
|
||||
}
|
||||
if lhs.backgroundColor != rhs.backgroundColor {
|
||||
return false
|
||||
}
|
||||
if lhs.separatorColor != rhs.separatorColor {
|
||||
return false
|
||||
}
|
||||
if lhs.highlightedItemColor != rhs.highlightedItemColor {
|
||||
return false
|
||||
}
|
||||
if lhs.primaryColor != rhs.primaryColor {
|
||||
return false
|
||||
}
|
||||
if lhs.secondaryColor != rhs.secondaryColor {
|
||||
return false
|
||||
}
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.destructiveColor != rhs.destructiveColor {
|
||||
return false
|
||||
}
|
||||
if lhs.disabledColor != rhs.disabledColor {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
open class AlertController: ViewController {
|
||||
private var controllerNode: AlertControllerNode {
|
||||
return self.displayNode as! AlertControllerNode
|
||||
}
|
||||
|
||||
public var theme: AlertControllerTheme {
|
||||
didSet {
|
||||
if oldValue != self.theme {
|
||||
self.controllerNode.updateTheme(self.theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
private let contentNode: AlertContentNode
|
||||
private let allowInputInset: Bool
|
||||
|
||||
public var dismissed: (() -> Void)?
|
||||
|
||||
public init(theme: AlertControllerTheme, contentNode: AlertContentNode, allowInputInset: Bool = true) {
|
||||
self.theme = theme
|
||||
self.contentNode = contentNode
|
||||
self.allowInputInset = allowInputInset
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override open func loadDisplayNode() {
|
||||
self.displayNode = AlertControllerNode(contentNode: self.contentNode, theme: self.theme, allowInputInset: self.allowInputInset)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.controllerNode.dismiss = { [weak self] in
|
||||
if let strongSelf = self, strongSelf.contentNode.dismissOnOutsideTap {
|
||||
strongSelf.controllerNode.animateOut {
|
||||
self?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.controllerNode.animateIn()
|
||||
}
|
||||
|
||||
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
|
||||
override open func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.dismissed?()
|
||||
self.presentingViewController?.dismiss(animated: false, completion: completion)
|
||||
}
|
||||
|
||||
public func dismissAnimated() {
|
||||
self.controllerNode.animateOut { [weak self] in
|
||||
self?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
147
submodules/Display/Display/AlertControllerNode.swift
Normal file
147
submodules/Display/Display/AlertControllerNode.swift
Normal file
@ -0,0 +1,147 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class AlertControllerNode: ASDisplayNode {
|
||||
private let centerDimView: UIImageView
|
||||
private let topDimView: UIView
|
||||
private let bottomDimView: UIView
|
||||
private let leftDimView: UIView
|
||||
private let rightDimView: UIView
|
||||
|
||||
private let containerNode: ASDisplayNode
|
||||
private let effectNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let contentNode: AlertContentNode
|
||||
private let allowInputInset: Bool
|
||||
|
||||
private var containerLayout: ContainerViewLayout?
|
||||
|
||||
var dismiss: (() -> Void)?
|
||||
|
||||
init(contentNode: AlertContentNode, theme: AlertControllerTheme, allowInputInset: Bool) {
|
||||
self.allowInputInset = allowInputInset
|
||||
|
||||
let dimColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
||||
self.centerDimView = UIImageView()
|
||||
self.centerDimView.image = generateStretchableFilledCircleImage(radius: 16.0, color: nil, backgroundColor: dimColor)
|
||||
|
||||
self.topDimView = UIView()
|
||||
self.topDimView.backgroundColor = dimColor
|
||||
|
||||
self.bottomDimView = UIView()
|
||||
self.bottomDimView.backgroundColor = dimColor
|
||||
|
||||
self.leftDimView = UIView()
|
||||
self.leftDimView.backgroundColor = dimColor
|
||||
|
||||
self.rightDimView = UIView()
|
||||
self.rightDimView.backgroundColor = dimColor
|
||||
|
||||
self.containerNode = ASDisplayNode()
|
||||
self.containerNode.layer.cornerRadius = 14.0
|
||||
self.containerNode.layer.masksToBounds = true
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = theme.backgroundColor
|
||||
|
||||
self.effectNode = ASDisplayNode(viewBlock: {
|
||||
return UIVisualEffectView(effect: UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark))
|
||||
})
|
||||
|
||||
self.contentNode = contentNode
|
||||
|
||||
super.init()
|
||||
|
||||
self.view.addSubview(self.centerDimView)
|
||||
self.view.addSubview(self.topDimView)
|
||||
self.view.addSubview(self.bottomDimView)
|
||||
self.view.addSubview(self.leftDimView)
|
||||
self.view.addSubview(self.rightDimView)
|
||||
|
||||
self.containerNode.addSubnode(self.effectNode)
|
||||
self.containerNode.addSubnode(self.backgroundNode)
|
||||
self.containerNode.addSubnode(self.contentNode)
|
||||
self.addSubnode(self.containerNode)
|
||||
|
||||
self.contentNode.requestLayout = { [weak self] transition in
|
||||
if let strongSelf = self, let containerLayout = self?.containerLayout {
|
||||
strongSelf.containerLayoutUpdated(containerLayout, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.topDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
|
||||
self.bottomDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
|
||||
self.leftDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
|
||||
self.rightDimView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimmingNodeTapGesture(_:))))
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: AlertControllerTheme) {
|
||||
if let effectView = self.effectNode.view as? UIVisualEffectView {
|
||||
effectView.effect = UIBlurEffect(style: theme.backgroundType == .light ? .light : .dark)
|
||||
}
|
||||
self.backgroundNode.backgroundColor = theme.backgroundColor
|
||||
self.contentNode.updateTheme(theme)
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.centerDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.topDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.bottomDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.leftDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.rightDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
self.containerNode.layer.animateSpring(from: 0.8 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, removeOnCompletion: true, additive: false, completion: nil)
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.centerDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.topDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.bottomDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.leftDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.rightDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
self.containerNode.layer.animateScale(from: 1.0, to: 0.8, duration: 0.4, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = layout
|
||||
|
||||
var insetOptions: ContainerViewLayoutInsetOptions = [.statusBar]
|
||||
if self.allowInputInset {
|
||||
insetOptions.insert(.input)
|
||||
}
|
||||
var insets = layout.insets(options: insetOptions)
|
||||
let maxWidth = min(240.0, layout.size.width - 70.0)
|
||||
insets.left = floor((layout.size.width - maxWidth) / 2.0)
|
||||
insets.right = floor((layout.size.width - maxWidth) / 2.0)
|
||||
let contentAvailableFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: layout.size.width - insets.right, height: layout.size.height - insets.top - insets.bottom))
|
||||
let contentSize = self.contentNode.updateLayout(size: contentAvailableFrame.size, transition: transition)
|
||||
let containerSize = CGSize(width: contentSize.width, height: contentSize.height)
|
||||
let containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: contentAvailableFrame.minY + floor((contentAvailableFrame.size.height - containerSize.height) / 2.0)), size: containerSize)
|
||||
|
||||
transition.updateFrame(view: self.centerDimView, frame: containerFrame)
|
||||
transition.updateFrame(view: self.topDimView, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: containerFrame.minY)))
|
||||
transition.updateFrame(view: self.bottomDimView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.maxY), size: CGSize(width: layout.size.width, height: layout.size.height - containerFrame.maxY)))
|
||||
transition.updateFrame(view: self.leftDimView, frame: CGRect(origin: CGPoint(x: 0.0, y: containerFrame.minY), size: CGSize(width: containerFrame.minX, height: containerFrame.height)))
|
||||
transition.updateFrame(view: self.rightDimView, frame: CGRect(origin: CGPoint(x: containerFrame.maxX, y: containerFrame.minY), size: CGSize(width: layout.size.width - containerFrame.maxX, height: containerFrame.height)))
|
||||
|
||||
transition.updateFrame(node: self.containerNode, frame: containerFrame)
|
||||
transition.updateFrame(node: self.effectNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
|
||||
transition.updateFrame(node: self.contentNode, frame: CGRect(origin: CGPoint(), size: containerFrame.size))
|
||||
}
|
||||
|
||||
@objc func dimmingNodeTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.dismiss?()
|
||||
}
|
||||
}
|
||||
}
|
326
submodules/Display/Display/CAAnimationUtils.swift
Normal file
326
submodules/Display/Display/CAAnimationUtils.swift
Normal file
@ -0,0 +1,326 @@
|
||||
import UIKit
|
||||
|
||||
#if BUCK
|
||||
import DisplayPrivate
|
||||
#endif
|
||||
|
||||
@objc private class CALayerAnimationDelegate: NSObject, CAAnimationDelegate {
|
||||
private let keyPath: String?
|
||||
var completion: ((Bool) -> Void)?
|
||||
|
||||
init(animation: CAAnimation, completion: ((Bool) -> Void)?) {
|
||||
if let animation = animation as? CABasicAnimation {
|
||||
self.keyPath = animation.keyPath
|
||||
} else {
|
||||
self.keyPath = nil
|
||||
}
|
||||
self.completion = completion
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
|
||||
if let anim = anim as? CABasicAnimation {
|
||||
if anim.keyPath != self.keyPath {
|
||||
return
|
||||
}
|
||||
}
|
||||
if let completion = self.completion {
|
||||
completion(flag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let completionKey = "CAAnimationUtils_completion"
|
||||
|
||||
public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve"
|
||||
|
||||
public extension CAAnimation {
|
||||
public var completion: ((Bool) -> Void)? {
|
||||
get {
|
||||
if let delegate = self.delegate as? CALayerAnimationDelegate {
|
||||
return delegate.completion
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} set(value) {
|
||||
if let delegate = self.delegate as? CALayerAnimationDelegate {
|
||||
delegate.completion = value
|
||||
} else {
|
||||
self.delegate = CALayerAnimationDelegate(animation: self, completion: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension CALayer {
|
||||
public func makeAnimation(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
|
||||
if timingFunction == kCAMediaTimingFunctionSpring {
|
||||
let animation = makeSpringAnimation(keyPath)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = kCAFillModeForwards
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
animation.speed = speed * Float(animation.duration / duration)
|
||||
animation.isAdditive = additive
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = CACurrentMediaTime() + delay
|
||||
animation.fillMode = kCAFillModeBoth
|
||||
}
|
||||
|
||||
return animation
|
||||
} else {
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
let animation = CABasicAnimation(keyPath: keyPath)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.duration = duration
|
||||
if let mediaTimingFunction = mediaTimingFunction {
|
||||
animation.timingFunction = mediaTimingFunction
|
||||
} else {
|
||||
animation.timingFunction = CAMediaTimingFunction(name: timingFunction)
|
||||
}
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = kCAFillModeForwards
|
||||
animation.speed = speed
|
||||
animation.isAdditive = additive
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
if !delay.isZero {
|
||||
animation.beginTime = CACurrentMediaTime() + delay
|
||||
animation.fillMode = kCAFillModeBoth
|
||||
}
|
||||
|
||||
return animation
|
||||
}
|
||||
}
|
||||
|
||||
public func animate(from: AnyObject, to: AnyObject, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let animation = self.makeAnimation(from: from, to: to, keyPath: keyPath, timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
self.add(animation, forKey: additive ? nil : keyPath)
|
||||
}
|
||||
|
||||
public func animateGroup(_ animations: [CAAnimation], key: String) {
|
||||
let animationGroup = CAAnimationGroup()
|
||||
var timeOffset = 0.0
|
||||
for animation in animations {
|
||||
animation.beginTime = animation.beginTime + timeOffset
|
||||
timeOffset += animation.duration / Double(animation.speed)
|
||||
}
|
||||
animationGroup.animations = animations
|
||||
animationGroup.duration = timeOffset
|
||||
self.add(animationGroup, forKey: key)
|
||||
}
|
||||
|
||||
public func animateKeyframes(values: [AnyObject], duration: Double, keyPath: String, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
let animation = CAKeyframeAnimation(keyPath: keyPath)
|
||||
animation.values = values
|
||||
var keyTimes: [NSNumber] = []
|
||||
for i in 0 ..< values.count {
|
||||
if i == 0 {
|
||||
keyTimes.append(0.0)
|
||||
} else if i == values.count - 1 {
|
||||
keyTimes.append(1.0)
|
||||
} else {
|
||||
keyTimes.append((Double(i) / Double(values.count - 1)) as NSNumber)
|
||||
}
|
||||
}
|
||||
animation.keyTimes = keyTimes
|
||||
animation.speed = speed
|
||||
animation.duration = duration
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
self.add(animation, forKey: keyPath)
|
||||
}
|
||||
|
||||
public func animateSpring(from: AnyObject, to: AnyObject, keyPath: String, duration: Double, initialVelocity: CGFloat = 0.0, damping: CGFloat = 88.0, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let animation: CABasicAnimation
|
||||
if #available(iOS 9.0, *) {
|
||||
animation = makeSpringBounceAnimation(keyPath, initialVelocity, damping)
|
||||
} else {
|
||||
animation = makeSpringAnimation(keyPath)
|
||||
}
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = kCAFillModeForwards
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
animation.speed = speed * Float(animation.duration / duration)
|
||||
animation.isAdditive = additive
|
||||
|
||||
self.add(animation, forKey: keyPath)
|
||||
}
|
||||
|
||||
public func animateAdditive(from: NSValue, to: NSValue, keyPath: String, key: String, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
var speed: Float = 1.0
|
||||
if k != 0 && k != 1 {
|
||||
speed = Float(1.0) / k
|
||||
}
|
||||
|
||||
let animation = CABasicAnimation(keyPath: keyPath)
|
||||
animation.fromValue = from
|
||||
animation.toValue = to
|
||||
animation.duration = duration
|
||||
if let mediaTimingFunction = mediaTimingFunction {
|
||||
animation.timingFunction = mediaTimingFunction
|
||||
} else {
|
||||
animation.timingFunction = CAMediaTimingFunction(name: timingFunction)
|
||||
}
|
||||
animation.isRemovedOnCompletion = removeOnCompletion
|
||||
animation.fillMode = kCAFillModeForwards
|
||||
animation.speed = speed
|
||||
animation.isAdditive = true
|
||||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
|
||||
self.add(animation, forKey: key)
|
||||
}
|
||||
|
||||
public func animateAlpha(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> ())? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "opacity", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
|
||||
public func animateScale(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
|
||||
public func animateRotation(from: CGFloat, to: CGFloat, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: NSNumber(value: Float(from)), to: NSNumber(value: Float(to)), keyPath: "transform.rotation.z", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: completion)
|
||||
}
|
||||
|
||||
func animatePosition(from: CGPoint, to: CGPoint, duration: Double, delay: Double = 0.0, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if from == to && !force {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.animate(from: NSValue(cgPoint: from), to: NSValue(cgPoint: to), keyPath: "position", timingFunction: timingFunction, duration: duration, delay: delay, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
}
|
||||
|
||||
func animateBounds(from: CGRect, to: CGRect, duration: Double, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if from == to && !force {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.animate(from: NSValue(cgRect: from), to: NSValue(cgRect: to), keyPath: "bounds", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: completion)
|
||||
}
|
||||
|
||||
public func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
|
||||
}
|
||||
|
||||
public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, timingFunction: String = kCAMediaTimingFunctionEaseInEaseOut, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: timingFunction, duration: duration, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
|
||||
}
|
||||
|
||||
public func animateBoundsOriginXAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) {
|
||||
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.x", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
|
||||
}
|
||||
|
||||
public func animateBoundsOriginYAdditive(from: CGFloat, to: CGFloat, duration: Double, mediaTimingFunction: CAMediaTimingFunction) {
|
||||
self.animate(from: from as NSNumber, to: to as NSNumber, keyPath: "bounds.origin.y", timingFunction: kCAMediaTimingFunctionEaseInEaseOut, duration: duration, mediaTimingFunction: mediaTimingFunction, additive: true)
|
||||
}
|
||||
|
||||
public func animatePositionKeyframes(values: [CGPoint], duration: Double, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
self.animateKeyframes(values: values.map { NSValue(cgPoint: $0) }, duration: duration, keyPath: "position")
|
||||
}
|
||||
|
||||
public func animateFrame(from: CGRect, to: CGRect, duration: Double, timingFunction: String, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if from == to && !force {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
var interrupted = false
|
||||
var completedPosition = false
|
||||
var completedBounds = false
|
||||
let partialCompletion: () -> Void = {
|
||||
if interrupted || (completedPosition && completedBounds) {
|
||||
if let completion = completion {
|
||||
completion(!interrupted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fromPosition = CGPoint(x: from.midX, y: from.midY)
|
||||
var toPosition = CGPoint(x: to.midX, y: to.midY)
|
||||
|
||||
var fromBounds = CGRect(origin: self.bounds.origin, size: from.size)
|
||||
var toBounds = CGRect(origin: self.bounds.origin, size: to.size)
|
||||
|
||||
if additive {
|
||||
fromPosition.x = -(toPosition.x - fromPosition.x)
|
||||
fromPosition.y = -(toPosition.y - fromPosition.y)
|
||||
toPosition = CGPoint()
|
||||
|
||||
fromBounds.size.width = -(toBounds.width - fromBounds.width)
|
||||
fromBounds.size.height = -(toBounds.height - fromBounds.height)
|
||||
toBounds = CGRect()
|
||||
}
|
||||
|
||||
self.animatePosition(from: fromPosition, to: toPosition, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in
|
||||
if !value {
|
||||
interrupted = true
|
||||
}
|
||||
completedPosition = true
|
||||
partialCompletion()
|
||||
})
|
||||
self.animateBounds(from: fromBounds, to: toBounds, duration: duration, timingFunction: timingFunction, mediaTimingFunction: mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, force: force, completion: { value in
|
||||
if !value {
|
||||
interrupted = true
|
||||
}
|
||||
completedBounds = true
|
||||
partialCompletion()
|
||||
})
|
||||
}
|
||||
|
||||
public func cancelAnimationsRecursive(key: String) {
|
||||
self.removeAnimation(forKey: key)
|
||||
if let sublayers = self.sublayers {
|
||||
for layer in sublayers {
|
||||
layer.cancelAnimationsRecursive(key: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
submodules/Display/Display/CASeeThroughTracingLayer.h
Normal file
9
submodules/Display/Display/CASeeThroughTracingLayer.h
Normal file
@ -0,0 +1,9 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CASeeThroughTracingLayer : CALayer
|
||||
|
||||
@end
|
||||
|
||||
@interface CASeeThroughTracingView : UIView
|
||||
|
||||
@end
|
57
submodules/Display/Display/CASeeThroughTracingLayer.m
Normal file
57
submodules/Display/Display/CASeeThroughTracingLayer.m
Normal file
@ -0,0 +1,57 @@
|
||||
#import "CASeeThroughTracingLayer.h"
|
||||
|
||||
@interface CASeeThroughTracingLayer () {
|
||||
CGPoint _parentOffset;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CASeeThroughTracingLayer
|
||||
|
||||
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
|
||||
[super addAnimation:anim forKey:key];
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame {
|
||||
[super setFrame:frame];
|
||||
|
||||
[self _mirrorTransformToSublayers];
|
||||
}
|
||||
|
||||
- (void)setBounds:(CGRect)bounds {
|
||||
[super setBounds:bounds];
|
||||
|
||||
[self _mirrorTransformToSublayers];
|
||||
}
|
||||
|
||||
- (void)setPosition:(CGPoint)position {
|
||||
[super setPosition:position];
|
||||
|
||||
[self _mirrorTransformToSublayers];
|
||||
}
|
||||
|
||||
- (void)_mirrorTransformToSublayers {
|
||||
CGRect bounds = self.bounds;
|
||||
CGPoint position = self.position;
|
||||
|
||||
CGPoint sublayerParentOffset = _parentOffset;
|
||||
sublayerParentOffset.x += position.x - (bounds.size.width) / 2.0f;
|
||||
sublayerParentOffset.y += position.y - (bounds.size.width) / 2.0f;
|
||||
|
||||
for (CALayer *sublayer in self.sublayers) {
|
||||
if ([sublayer isKindOfClass:[CASeeThroughTracingLayer class]]) {
|
||||
((CASeeThroughTracingLayer *)sublayer)->_parentOffset = sublayerParentOffset;
|
||||
[(CASeeThroughTracingLayer *)sublayer _mirrorTransformToSublayers];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CASeeThroughTracingView
|
||||
|
||||
+ (Class)layerClass {
|
||||
return [CASeeThroughTracingLayer class];
|
||||
}
|
||||
|
||||
@end
|
34
submodules/Display/Display/CATracingLayer.h
Normal file
34
submodules/Display/Display/CATracingLayer.h
Normal file
@ -0,0 +1,34 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CATracingLayer : CALayer
|
||||
|
||||
@end
|
||||
|
||||
@interface CATracingLayerInfo : NSObject
|
||||
|
||||
@property (nonatomic, readonly) bool shouldBeAdjustedToInverseTransform;
|
||||
@property (nonatomic, weak, readonly) id _Nullable userData;
|
||||
@property (nonatomic, readonly) int32_t tracingTag;
|
||||
@property (nonatomic, readonly) int32_t disableChildrenTracingTags;
|
||||
|
||||
- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag disableChildrenTracingTags:(int32_t)disableChildrenTracingTags;
|
||||
|
||||
@end
|
||||
|
||||
@interface CALayer (Tracing)
|
||||
|
||||
- (CATracingLayerInfo * _Nullable)traceableInfo;
|
||||
- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info;
|
||||
|
||||
- (bool)hasPositionOrOpacityAnimations;
|
||||
- (bool)hasPositionAnimations;
|
||||
|
||||
- (void)setInvalidateTracingSublayers:(void (^_Nullable)())block;
|
||||
- (NSArray<NSArray<CALayer *> *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag;
|
||||
- (void)adjustTraceableLayerTransforms:(CGSize)offset;
|
||||
|
||||
- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget;
|
||||
|
||||
- (void)invalidateUpTheTree;
|
||||
|
||||
@end
|
364
submodules/Display/Display/CATracingLayer.m
Normal file
364
submodules/Display/Display/CATracingLayer.m
Normal file
@ -0,0 +1,364 @@
|
||||
#import "CATracingLayer.h"
|
||||
|
||||
#import "RuntimeUtils.h"
|
||||
|
||||
static void *CATracingLayerInvalidatedKey = &CATracingLayerInvalidatedKey;
|
||||
static void *CATracingLayerIsInvalidatedBlock = &CATracingLayerIsInvalidatedBlock;
|
||||
static void *CATracingLayerTraceableInfoKey = &CATracingLayerTraceableInfoKey;
|
||||
static void *CATracingLayerPositionAnimationMirrorTarget = &CATracingLayerPositionAnimationMirrorTarget;
|
||||
|
||||
@implementation CALayer (Tracing)
|
||||
|
||||
- (void)setInvalidateTracingSublayers:(void (^_Nullable)())block {
|
||||
[self setAssociatedObject:[block copy] forKey:CATracingLayerIsInvalidatedBlock];
|
||||
}
|
||||
|
||||
- (void (^_Nullable)())invalidateTracingSublayers {
|
||||
return [self associatedObjectForKey:CATracingLayerIsInvalidatedBlock];
|
||||
}
|
||||
|
||||
- (bool)isTraceable {
|
||||
return [self associatedObjectForKey:CATracingLayerTraceableInfoKey] != nil || [self isKindOfClass:[CATracingLayer class]];
|
||||
}
|
||||
|
||||
- (CATracingLayerInfo * _Nullable)traceableInfo {
|
||||
return [self associatedObjectForKey:CATracingLayerTraceableInfoKey];
|
||||
}
|
||||
|
||||
- (void)setTraceableInfo:(CATracingLayerInfo * _Nullable)info {
|
||||
[self setAssociatedObject:info forKey:CATracingLayerTraceableInfoKey];
|
||||
}
|
||||
|
||||
- (bool)hasPositionOrOpacityAnimations {
|
||||
return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil || [self animationForKey:@"sublayerTransform"] != nil || [self animationForKey:@"opacity"] != nil;
|
||||
}
|
||||
|
||||
- (bool)hasPositionAnimations {
|
||||
return [self animationForKey:@"position"] != nil || [self animationForKey:@"bounds"] != nil;
|
||||
}
|
||||
|
||||
static void traceLayerSurfaces(int32_t tracingTag, int depth, CALayer * _Nonnull layer, NSMutableDictionary<NSNumber *, NSMutableArray<CALayer *> *> *layersByDepth, bool skipIfNoTraceableSublayers) {
|
||||
bool hadTraceableSublayers = false;
|
||||
for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) {
|
||||
CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo];
|
||||
if (sublayerTraceableInfo != nil && sublayerTraceableInfo.tracingTag == tracingTag) {
|
||||
NSMutableArray *array = layersByDepth[@(depth)];
|
||||
if (array == nil) {
|
||||
array = [[NSMutableArray alloc] init];
|
||||
layersByDepth[@(depth)] = array;
|
||||
}
|
||||
[array addObject:sublayer];
|
||||
hadTraceableSublayers = true;
|
||||
}
|
||||
if (sublayerTraceableInfo.disableChildrenTracingTags & tracingTag) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipIfNoTraceableSublayers || !hadTraceableSublayers) {
|
||||
for (CALayer *sublayer in layer.sublayers.reverseObjectEnumerator) {
|
||||
if ([sublayer isKindOfClass:[CATracingLayer class]]) {
|
||||
traceLayerSurfaces(tracingTag, depth + 1, sublayer, layersByDepth, hadTraceableSublayers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray<NSArray<CALayer *> *> * _Nonnull)traceableLayerSurfacesWithTag:(int32_t)tracingTag {
|
||||
NSMutableDictionary<NSNumber *, NSMutableArray<CALayer *> *> *layersByDepth = [[NSMutableDictionary alloc] init];
|
||||
|
||||
traceLayerSurfaces(tracingTag, 0, self, layersByDepth, false);
|
||||
|
||||
NSMutableArray<NSMutableArray<CALayer *> *> *result = [[NSMutableArray alloc] init];
|
||||
|
||||
for (id key in [[layersByDepth allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
[result addObject:layersByDepth[key]];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)adjustTraceableLayerTransforms:(CGSize)offset {
|
||||
CGRect frame = self.frame;
|
||||
CGSize sublayerOffset = CGSizeMake(frame.origin.x + offset.width, frame.origin.y + offset.height);
|
||||
for (CALayer *sublayer in self.sublayers) {
|
||||
CATracingLayerInfo *sublayerTraceableInfo = [sublayer traceableInfo];
|
||||
if (sublayerTraceableInfo != nil && sublayerTraceableInfo.shouldBeAdjustedToInverseTransform) {
|
||||
sublayer.sublayerTransform = CATransform3DMakeTranslation(-sublayerOffset.width, -sublayerOffset.height, 0.0f);
|
||||
} else if ([sublayer isKindOfClass:[CATracingLayer class]]) {
|
||||
[(CATracingLayer *)sublayer adjustTraceableLayerTransforms:sublayerOffset];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CALayer * _Nullable)animationMirrorTarget {
|
||||
return [self associatedObjectForKey:CATracingLayerPositionAnimationMirrorTarget];
|
||||
}
|
||||
|
||||
- (void)setPositionAnimationMirrorTarget:(CALayer * _Nullable)animationMirrorTarget {
|
||||
[self setAssociatedObject:animationMirrorTarget forKey:CATracingLayerPositionAnimationMirrorTarget associationPolicy:NSObjectAssociationPolicyRetain];
|
||||
}
|
||||
|
||||
- (void)invalidateUpTheTree {
|
||||
CALayer *superlayer = self;
|
||||
while (true) {
|
||||
if (superlayer == nil) {
|
||||
break;
|
||||
}
|
||||
|
||||
void (^block)() = [superlayer invalidateTracingSublayers];
|
||||
if (block != nil) {
|
||||
block();
|
||||
}
|
||||
|
||||
superlayer = superlayer.superlayer;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface CATracingLayerAnimationDelegate : NSObject <CAAnimationDelegate> {
|
||||
id<CAAnimationDelegate> _delegate;
|
||||
void (^_animationStopped)();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CATracingLayerAnimationDelegate
|
||||
|
||||
- (instancetype)initWithDelegate:(id<CAAnimationDelegate>)delegate animationStopped:(void (^_Nonnull)())animationStopped {
|
||||
_delegate = delegate;
|
||||
_animationStopped = [animationStopped copy];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)animationDidStart:(CAAnimation *)anim {
|
||||
if ([_delegate respondsToSelector:@selector(animationDidStart:)]) {
|
||||
[(id)_delegate animationDidStart:anim];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
|
||||
if ([_delegate respondsToSelector:@selector(animationDidStop:finished:)]) {
|
||||
[(id)_delegate animationDidStop:anim finished:flag];
|
||||
}
|
||||
|
||||
if (_animationStopped) {
|
||||
_animationStopped();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface CATracingLayer ()
|
||||
|
||||
@property (nonatomic) bool isInvalidated;
|
||||
|
||||
@end
|
||||
|
||||
@implementation CATracingLayer
|
||||
|
||||
- (void)setNeedsDisplay {
|
||||
}
|
||||
|
||||
- (void)displayIfNeeded {
|
||||
}
|
||||
|
||||
- (bool)isInvalidated {
|
||||
return [[self associatedObjectForKey:CATracingLayerInvalidatedKey] intValue] != 0;
|
||||
}
|
||||
|
||||
- (void)setIsInvalidated:(bool)isInvalidated {
|
||||
[self setAssociatedObject: isInvalidated ? @1 : @0 forKey:CATracingLayerInvalidatedKey];
|
||||
}
|
||||
|
||||
- (void)setPosition:(CGPoint)position {
|
||||
[super setPosition:position];
|
||||
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
|
||||
- (void)setOpacity:(float)opacity {
|
||||
[super setOpacity:opacity];
|
||||
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
|
||||
- (void)addSublayer:(CALayer *)layer {
|
||||
[super addSublayer:layer];
|
||||
|
||||
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)insertSublayer:(CALayer *)layer atIndex:(unsigned)idx {
|
||||
[super insertSublayer:layer atIndex:idx];
|
||||
|
||||
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)insertSublayer:(CALayer *)layer below:(nullable CALayer *)sibling {
|
||||
[super insertSublayer:layer below:sibling];
|
||||
|
||||
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)insertSublayer:(CALayer *)layer above:(nullable CALayer *)sibling {
|
||||
[super insertSublayer:layer above:sibling];
|
||||
|
||||
if ([layer isTraceable] || [layer isKindOfClass:[CATracingLayer class]]) {
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)replaceSublayer:(CALayer *)layer with:(CALayer *)layer2 {
|
||||
[super replaceSublayer:layer with:layer2];
|
||||
|
||||
if ([layer isTraceable] || [layer2 isTraceable]) {
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeFromSuperlayer {
|
||||
if ([self isTraceable]) {
|
||||
[self invalidateUpTheTree];
|
||||
}
|
||||
|
||||
[super removeFromSuperlayer];
|
||||
}
|
||||
|
||||
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
|
||||
if ([anim isKindOfClass:[CABasicAnimation class]]) {
|
||||
if (false && [key isEqualToString:@"bounds.origin.y"]) {
|
||||
CABasicAnimation *animCopy = [anim copy];
|
||||
CGFloat from = [animCopy.fromValue floatValue];
|
||||
CGFloat to = [animCopy.toValue floatValue];
|
||||
|
||||
animCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0, to - from, 0.0f)];
|
||||
animCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
|
||||
animCopy.keyPath = @"sublayerTransform";
|
||||
|
||||
__weak CATracingLayer *weakSelf = self;
|
||||
anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
|
||||
__strong CATracingLayer *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf invalidateUpTheTree];
|
||||
}
|
||||
}];
|
||||
|
||||
[super addAnimation:anim forKey:key];
|
||||
|
||||
CABasicAnimation *positionAnimCopy = [animCopy copy];
|
||||
positionAnimCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0.0, 0.0, 0.0f)];
|
||||
positionAnimCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
|
||||
positionAnimCopy.additive = true;
|
||||
positionAnimCopy.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
|
||||
__strong CATracingLayer *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf invalidateUpTheTree];
|
||||
}
|
||||
}];
|
||||
|
||||
[self invalidateUpTheTree];
|
||||
|
||||
[self mirrorAnimationDownTheTree:animCopy key:@"sublayerTransform"];
|
||||
[self mirrorPositionAnimationDownTheTree:positionAnimCopy key:@"sublayerTransform"];
|
||||
} else if ([key isEqualToString:@"position"]) {
|
||||
CABasicAnimation *animCopy = [anim copy];
|
||||
CGPoint from = [animCopy.fromValue CGPointValue];
|
||||
CGPoint to = [animCopy.toValue CGPointValue];
|
||||
|
||||
animCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(to.x - from.x, to.y - from.y, 0.0f)];
|
||||
animCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
|
||||
animCopy.keyPath = @"sublayerTransform";
|
||||
|
||||
__weak CATracingLayer *weakSelf = self;
|
||||
anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
|
||||
__strong CATracingLayer *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf invalidateUpTheTree];
|
||||
}
|
||||
}];
|
||||
|
||||
[super addAnimation:anim forKey:key];
|
||||
|
||||
CABasicAnimation *positionAnimCopy = [animCopy copy];
|
||||
positionAnimCopy.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-to.x + from.x, 0.0, 0.0f)];
|
||||
positionAnimCopy.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
|
||||
positionAnimCopy.additive = true;
|
||||
positionAnimCopy.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
|
||||
__strong CATracingLayer *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf invalidateUpTheTree];
|
||||
}
|
||||
}];
|
||||
|
||||
[self invalidateUpTheTree];
|
||||
|
||||
[self mirrorAnimationDownTheTree:animCopy key:@"sublayerTransform"];
|
||||
[self mirrorPositionAnimationDownTheTree:positionAnimCopy key:@"sublayerTransform"];
|
||||
} else if ([key isEqualToString:@"opacity"]) {
|
||||
__weak CATracingLayer *weakSelf = self;
|
||||
anim.delegate = [[CATracingLayerAnimationDelegate alloc] initWithDelegate:anim.delegate animationStopped:^{
|
||||
__strong CATracingLayer *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf invalidateUpTheTree];
|
||||
}
|
||||
}];
|
||||
|
||||
[super addAnimation:anim forKey:key];
|
||||
|
||||
[self invalidateUpTheTree];
|
||||
} else {
|
||||
[super addAnimation:anim forKey:key];
|
||||
}
|
||||
} else {
|
||||
[super addAnimation:anim forKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mirrorPositionAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key {
|
||||
if ([animation isKindOfClass:[CABasicAnimation class]]) {
|
||||
if ([((CABasicAnimation *)animation).keyPath isEqualToString:@"sublayerTransform"]) {
|
||||
CALayer *positionAnimationMirrorTarget = [self animationMirrorTarget];
|
||||
if (positionAnimationMirrorTarget != nil) {
|
||||
[positionAnimationMirrorTarget addAnimation:[animation copy] forKey:key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mirrorAnimationDownTheTree:(CAAnimation *)animation key:(NSString *)key {
|
||||
for (CALayer *sublayer in self.sublayers) {
|
||||
CATracingLayerInfo *traceableInfo = [sublayer traceableInfo];
|
||||
if (traceableInfo != nil && traceableInfo.shouldBeAdjustedToInverseTransform) {
|
||||
[sublayer addAnimation:[animation copy] forKey:key];
|
||||
}
|
||||
|
||||
if ([sublayer isKindOfClass:[CATracingLayer class]]) {
|
||||
[(CATracingLayer *)sublayer mirrorAnimationDownTheTree:animation key:key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation CATracingLayerInfo
|
||||
|
||||
- (instancetype _Nonnull)initWithShouldBeAdjustedToInverseTransform:(bool)shouldBeAdjustedToInverseTransform userData:(id _Nullable)userData tracingTag:(int32_t)tracingTag disableChildrenTracingTags:(int32_t)disableChildrenTracingTags {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_shouldBeAdjustedToInverseTransform = shouldBeAdjustedToInverseTransform;
|
||||
_userData = userData;
|
||||
_tracingTag = tracingTag;
|
||||
_disableChildrenTracingTags = disableChildrenTracingTags;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
117
submodules/Display/Display/ChildWindowHostView.swift
Normal file
117
submodules/Display/Display/ChildWindowHostView.swift
Normal file
@ -0,0 +1,117 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private final class ChildWindowHostView: UIView, WindowHost {
|
||||
var updateSize: ((CGSize) -> Void)?
|
||||
var layoutSubviewsEvent: (() -> Void)?
|
||||
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
|
||||
var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
|
||||
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
|
||||
var invalidatePreferNavigationUIHiddenImpl: (() -> Void)?
|
||||
var cancelInteractiveKeyboardGesturesImpl: (() -> Void)?
|
||||
var forEachControllerImpl: (((ContainableController) -> Void) -> Void)?
|
||||
var getAccessibilityElementsImpl: (() -> [Any]?)?
|
||||
|
||||
override var frame: CGRect {
|
||||
didSet {
|
||||
if self.frame.size != oldValue.size {
|
||||
self.updateSize?(self.frame.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.layoutSubviewsEvent?()
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return self.hitTestImpl?(point, event)
|
||||
}
|
||||
|
||||
func invalidateDeferScreenEdgeGestures() {
|
||||
self.invalidateDeferScreenEdgeGestureImpl?()
|
||||
}
|
||||
|
||||
func invalidatePreferNavigationUIHidden() {
|
||||
self.invalidatePreferNavigationUIHiddenImpl?()
|
||||
}
|
||||
|
||||
func cancelInteractiveKeyboardGestures() {
|
||||
self.cancelInteractiveKeyboardGesturesImpl?()
|
||||
}
|
||||
|
||||
func forEachController(_ f: (ContainableController) -> Void) {
|
||||
self.forEachControllerImpl?(f)
|
||||
}
|
||||
|
||||
func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) {
|
||||
self.presentController?(controller, level, blockInteraction, completion)
|
||||
}
|
||||
|
||||
func presentInGlobalOverlay(_ controller: ContainableController) {
|
||||
}
|
||||
}
|
||||
|
||||
public func childWindowHostView(parent: UIView) -> WindowHostView {
|
||||
let view = ChildWindowHostView()
|
||||
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
let hostView = WindowHostView(containerView: view, eventView: view, isRotating: {
|
||||
return false
|
||||
}, updateSupportedInterfaceOrientations: { orientations in
|
||||
}, updateDeferScreenEdgeGestures: { edges in
|
||||
}, updatePreferNavigationUIHidden: { value in
|
||||
})
|
||||
|
||||
view.updateSize = { [weak hostView] size in
|
||||
hostView?.updateSize?(size, 0.0)
|
||||
}
|
||||
|
||||
view.layoutSubviewsEvent = { [weak hostView] in
|
||||
hostView?.layoutSubviews?()
|
||||
}
|
||||
|
||||
/*window.updateIsUpdatingOrientationLayout = { [weak hostView] value in
|
||||
hostView?.isUpdatingOrientationLayout = value
|
||||
}
|
||||
|
||||
window.updateToInterfaceOrientation = { [weak hostView] in
|
||||
hostView?.updateToInterfaceOrientation?()
|
||||
}*/
|
||||
|
||||
view.presentController = { [weak hostView] controller, level, block, f in
|
||||
hostView?.present?(controller, level, block, f)
|
||||
}
|
||||
|
||||
/*view.presentNativeImpl = { [weak hostView] controller in
|
||||
hostView?.presentNative?(controller)
|
||||
}*/
|
||||
|
||||
view.hitTestImpl = { [weak hostView] point, event in
|
||||
return hostView?.hitTest?(point, event)
|
||||
}
|
||||
|
||||
view.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in
|
||||
return hostView?.invalidateDeferScreenEdgeGesture?()
|
||||
}
|
||||
|
||||
view.invalidatePreferNavigationUIHiddenImpl = { [weak hostView] in
|
||||
return hostView?.invalidatePreferNavigationUIHidden?()
|
||||
}
|
||||
|
||||
view.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in
|
||||
hostView?.cancelInteractiveKeyboardGestures?()
|
||||
}
|
||||
|
||||
view.forEachControllerImpl = { [weak hostView] f in
|
||||
hostView?.forEachController?(f)
|
||||
}
|
||||
|
||||
view.getAccessibilityElementsImpl = { [weak hostView] in
|
||||
return hostView?.getAccessibilityElements?()
|
||||
}
|
||||
|
||||
return hostView
|
||||
}
|
166
submodules/Display/Display/CollectionIndexNode.swift
Normal file
166
submodules/Display/Display/CollectionIndexNode.swift
Normal file
@ -0,0 +1,166 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
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 scrollFeedback: HapticFeedback?
|
||||
|
||||
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
|
||||
if sections.isEmpty {
|
||||
skipCount = 1
|
||||
} else {
|
||||
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 currentIndex = 0
|
||||
var displayIndex = 0
|
||||
var addedLastTitle = false
|
||||
|
||||
let addTitle: (Int) -> Void = { index in
|
||||
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)
|
||||
}
|
||||
|
||||
currentIndex += skipCount
|
||||
displayIndex += 1
|
||||
}
|
||||
|
||||
while currentIndex < sections.count {
|
||||
if currentIndex == sections.count - 1 {
|
||||
addedLastTitle = true
|
||||
}
|
||||
addTitle(currentIndex)
|
||||
}
|
||||
|
||||
if !addedLastTitle && sections.count > 0 {
|
||||
addTitle(sections.count - 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.isUserInteractionEnabled, 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)
|
||||
|
||||
if self.scrollFeedback == nil {
|
||||
self.scrollFeedback = HapticFeedback()
|
||||
}
|
||||
self.scrollFeedback?.tap()
|
||||
}
|
||||
}
|
||||
case .cancelled, .ended:
|
||||
self.currentSelectedIndex = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
27
submodules/Display/Display/ContainableController.swift
Normal file
27
submodules/Display/Display/ContainableController.swift
Normal file
@ -0,0 +1,27 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public protocol PresentableController: class {
|
||||
func viewDidAppear(completion: @escaping () -> Void)
|
||||
}
|
||||
|
||||
public protocol ContainableController: class {
|
||||
var view: UIView! { get }
|
||||
var displayNode: ASDisplayNode { get }
|
||||
var isViewLoaded: Bool { get }
|
||||
var isOpaqueWhenInOverlay: Bool { get }
|
||||
var blocksBackgroundWhenInOverlay: Bool { get }
|
||||
var ready: Promise<Bool> { get }
|
||||
|
||||
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations
|
||||
var deferScreenEdgeGestures: UIRectEdge { get }
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
|
||||
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation)
|
||||
|
||||
func viewWillAppear(_ animated: Bool)
|
||||
func viewWillDisappear(_ animated: Bool)
|
||||
func viewDidAppear(_ animated: Bool)
|
||||
func viewDidDisappear(_ animated: Bool)
|
||||
}
|
675
submodules/Display/Display/ContainedViewLayoutTransition.swift
Normal file
675
submodules/Display/Display/ContainedViewLayoutTransition.swift
Normal file
@ -0,0 +1,675 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public enum ContainedViewLayoutTransitionCurve {
|
||||
case easeInOut
|
||||
case spring
|
||||
case custom(Float, Float, Float, Float)
|
||||
}
|
||||
|
||||
public extension ContainedViewLayoutTransitionCurve {
|
||||
var timingFunction: String {
|
||||
switch self {
|
||||
case .easeInOut:
|
||||
return kCAMediaTimingFunctionEaseInEaseOut
|
||||
case .spring:
|
||||
return kCAMediaTimingFunctionSpring
|
||||
case .custom:
|
||||
return kCAMediaTimingFunctionEaseInEaseOut
|
||||
}
|
||||
}
|
||||
|
||||
var mediaTimingFunction: CAMediaTimingFunction? {
|
||||
switch self {
|
||||
case .easeInOut:
|
||||
return nil
|
||||
case .spring:
|
||||
return nil
|
||||
case let .custom(p1, p2, p3, p4):
|
||||
return CAMediaTimingFunction(controlPoints: p1, p2, p3, p4)
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
var viewAnimationOptions: UIViewAnimationOptions {
|
||||
switch self {
|
||||
case .easeInOut:
|
||||
return [.curveEaseInOut]
|
||||
case .spring:
|
||||
return UIViewAnimationOptions(rawValue: 7 << 16)
|
||||
case .custom:
|
||||
return []
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public enum ContainedViewLayoutTransition {
|
||||
case immediate
|
||||
case animated(duration: Double, curve: ContainedViewLayoutTransitionCurve)
|
||||
|
||||
public var isAnimated: Bool {
|
||||
if case .immediate = self {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension ContainedViewLayoutTransition {
|
||||
func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.frame.equalTo(frame) && !force {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.frame = frame
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousFrame = node.frame
|
||||
node.frame = frame
|
||||
node.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateBounds(node: ASDisplayNode, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.bounds.equalTo(bounds) && !force {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.bounds = bounds
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousBounds = node.bounds
|
||||
node.bounds = bounds
|
||||
node.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateBounds(layer: CALayer, bounds: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if layer.bounds.equalTo(bounds) && !force {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.bounds = bounds
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousBounds = layer.bounds
|
||||
layer.bounds = bounds
|
||||
layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePosition(node: ASDisplayNode, position: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.position.equalTo(position) {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.position = position
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousPosition = node.position
|
||||
node.position = position
|
||||
node.layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
if layer.position.equalTo(position) {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.position = position
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousPosition = layer.position
|
||||
layer.position = position
|
||||
layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animatePosition(node: ASDisplayNode, to position: CGPoint, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.position.equalTo(position) {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animatePosition(from: node.position, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateFrame(node: ASDisplayNode, from frame: CGRect, to toFrame: CGRect? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animateFrame(from: frame, to: toFrame ?? node.layer.frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: additive, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateBounds(layer: CALayer, from bounds: CGRect, removeOnCompletion: Bool = true, completion: ((Bool) -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
layer.animateBounds(from: bounds, to: layer.bounds, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateOffsetAdditive(node: ASDisplayNode, offset: CGFloat) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func animateHorizontalOffsetAdditive(node: ASDisplayNode, offset: CGFloat) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOffsetAdditive(layer: CALayer, offset: CGFloat, completion: (() -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
completion?()
|
||||
case let .animated(duration, curve):
|
||||
layer.animateBoundsOriginYAdditive(from: offset, to: 0.0, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animatePositionAdditive(node: ASDisplayNode, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func animatePositionAdditive(layer: CALayer, offset: CGFloat, removeOnCompletion: Bool = true, completion: @escaping (Bool) -> Void) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(duration, curve):
|
||||
layer.animatePosition(from: CGPoint(x: 0.0, y: offset), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func animatePositionAdditive(node: ASDisplayNode, offset: CGPoint, removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animatePosition(from: offset, to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animatePositionAdditive(layer: CALayer, offset: CGPoint, to toOffset: CGPoint = CGPoint(), removeOnCompletion: Bool = true, completion: (() -> Void)? = nil) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
break
|
||||
case let .animated(duration, curve):
|
||||
layer.animatePosition(from: offset, to: toOffset, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: removeOnCompletion, additive: true, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateFrame(view: UIView, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if view.frame.equalTo(frame) && !force {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
view.frame = frame
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousFrame = view.frame
|
||||
view.frame = frame
|
||||
view.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)? = nil) {
|
||||
if layer.frame.equalTo(frame) {
|
||||
completion?(true)
|
||||
} else {
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.frame = frame
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousFrame = layer.frame
|
||||
layer.frame = frame
|
||||
layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateAlpha(node: ASDisplayNode, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.alpha.isEqual(to: alpha) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.alpha = alpha
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousAlpha = node.alpha
|
||||
node.alpha = alpha
|
||||
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
if layer.opacity.isEqual(to: Float(alpha)) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.opacity = Float(alpha)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousAlpha = layer.opacity
|
||||
layer.opacity = Float(alpha)
|
||||
layer.animateAlpha(from: CGFloat(previousAlpha), to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateBackgroundColor(node: ASDisplayNode, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
||||
if let nodeColor = node.backgroundColor, nodeColor.isEqual(color) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.backgroundColor = color
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
if let nodeColor = node.backgroundColor {
|
||||
node.backgroundColor = color
|
||||
node.layer.animate(from: nodeColor.cgColor, to: color.cgColor, keyPath: "backgroundColor", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
node.backgroundColor = color
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateCornerRadius(node: ASDisplayNode, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
if node.cornerRadius.isEqual(to: cornerRadius) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.cornerRadius = cornerRadius
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousCornerRadius = node.cornerRadius
|
||||
node.cornerRadius = cornerRadius
|
||||
node.layer.animate(from: NSNumber(value: Float(previousCornerRadius)), to: NSNumber(value: Float(cornerRadius)), keyPath: "cornerRadius", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func animateTransformScale(node: ASDisplayNode, from fromScale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = node.layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
if currentScale.isEqual(to: fromScale) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.animateScale(from: fromScale, to: currentScale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateTransformScale(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = node.layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
if currentScale.isEqual(to: scale) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
node.layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateTransformScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = layer.transform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
if currentScale.isEqual(to: scale) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
layer.transform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
return
|
||||
}
|
||||
let t = node.layer.sublayerTransform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
if currentScale.isEqual(to: scale) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.sublayerTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
return
|
||||
}
|
||||
let t = node.layer.sublayerTransform
|
||||
let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23))
|
||||
if t.m22 < 0.0 {
|
||||
currentScaleY = -currentScaleY
|
||||
}
|
||||
if CGPoint(x: currentScaleX, y: currentScaleY) == scale {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.sublayerTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
return
|
||||
}
|
||||
let t = node.layer.transform
|
||||
let currentScaleX = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
var currentScaleY = sqrt((t.m21 * t.m21) + (t.m22 * t.m22) + (t.m23 * t.m23))
|
||||
if t.m22 < 0.0 {
|
||||
currentScaleY = -currentScaleY
|
||||
}
|
||||
if CGPoint(x: currentScaleX, y: currentScaleY) == scale {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
node.layer.transform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
node.layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: node.layer.transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func updateSublayerTransformOffset(layer: CALayer, offset: CGPoint, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = layer.transform
|
||||
let currentOffset = CGPoint(x: t.m41, y: t.m42)
|
||||
if currentOffset == offset {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0)
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
layer.sublayerTransform = CATransform3DMakeTranslation(offset.x, offset.y, 0.0)
|
||||
layer.animate(from: NSValue(caTransform3D: t), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
public extension ContainedViewLayoutTransition {
|
||||
public func animateView(_ f: @escaping () -> Void) {
|
||||
switch self {
|
||||
case .immediate:
|
||||
f()
|
||||
case let .animated(duration, curve):
|
||||
UIView.animate(withDuration: duration, delay: 0.0, options: curve.viewAnimationOptions, animations: {
|
||||
f()
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
87
submodules/Display/Display/ContainerViewLayout.swift
Normal file
87
submodules/Display/Display/ContainerViewLayout.swift
Normal file
@ -0,0 +1,87 @@
|
||||
import UIKit
|
||||
|
||||
public struct ContainerViewLayoutInsetOptions: OptionSet {
|
||||
public let rawValue: Int
|
||||
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public static let statusBar = ContainerViewLayoutInsetOptions(rawValue: 1 << 0)
|
||||
public static let input = ContainerViewLayoutInsetOptions(rawValue: 1 << 1)
|
||||
}
|
||||
|
||||
public enum ContainerViewLayoutSizeClass {
|
||||
case compact
|
||||
case regular
|
||||
}
|
||||
|
||||
public struct LayoutMetrics: Equatable {
|
||||
public let widthClass: ContainerViewLayoutSizeClass
|
||||
public let heightClass: ContainerViewLayoutSizeClass
|
||||
|
||||
public init(widthClass: ContainerViewLayoutSizeClass, heightClass: ContainerViewLayoutSizeClass) {
|
||||
self.widthClass = widthClass
|
||||
self.heightClass = heightClass
|
||||
}
|
||||
|
||||
public init() {
|
||||
self.widthClass = .compact
|
||||
self.heightClass = .compact
|
||||
}
|
||||
}
|
||||
|
||||
public struct ContainerViewLayout: Equatable {
|
||||
public let size: CGSize
|
||||
public let metrics: LayoutMetrics
|
||||
public let intrinsicInsets: UIEdgeInsets
|
||||
public let safeInsets: UIEdgeInsets
|
||||
public let statusBarHeight: CGFloat?
|
||||
public var inputHeight: CGFloat?
|
||||
public let standardInputHeight: CGFloat
|
||||
public let inputHeightIsInteractivellyChanging: Bool
|
||||
public let inVoiceOver: Bool
|
||||
|
||||
public init(size: CGSize, metrics: LayoutMetrics, intrinsicInsets: UIEdgeInsets, safeInsets: UIEdgeInsets, statusBarHeight: CGFloat?, inputHeight: CGFloat?, standardInputHeight: CGFloat, inputHeightIsInteractivellyChanging: Bool, inVoiceOver: Bool) {
|
||||
self.size = size
|
||||
self.metrics = metrics
|
||||
self.intrinsicInsets = intrinsicInsets
|
||||
self.safeInsets = safeInsets
|
||||
self.statusBarHeight = statusBarHeight
|
||||
self.inputHeight = inputHeight
|
||||
self.standardInputHeight = standardInputHeight
|
||||
self.inputHeightIsInteractivellyChanging = inputHeightIsInteractivellyChanging
|
||||
self.inVoiceOver = inVoiceOver
|
||||
}
|
||||
|
||||
public func insets(options: ContainerViewLayoutInsetOptions) -> UIEdgeInsets {
|
||||
var insets = self.intrinsicInsets
|
||||
if let statusBarHeight = self.statusBarHeight , options.contains(.statusBar) {
|
||||
insets.top += statusBarHeight
|
||||
}
|
||||
if let inputHeight = self.inputHeight , options.contains(.input) {
|
||||
insets.bottom = max(inputHeight, insets.bottom)
|
||||
}
|
||||
return insets
|
||||
}
|
||||
|
||||
public func addedInsets(insets: UIEdgeInsets) -> ContainerViewLayout {
|
||||
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: UIEdgeInsets(top: self.intrinsicInsets.top + insets.top, left: self.intrinsicInsets.left + insets.left, bottom: self.intrinsicInsets.bottom + insets.bottom, right: self.intrinsicInsets.right + insets.right), safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
|
||||
}
|
||||
|
||||
public func withUpdatedSize(_ size: CGSize) -> ContainerViewLayout {
|
||||
return ContainerViewLayout(size: size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
|
||||
}
|
||||
|
||||
public func withUpdatedInputHeight(_ inputHeight: CGFloat?) -> ContainerViewLayout {
|
||||
return ContainerViewLayout(size: self.size, metrics: self.metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
|
||||
}
|
||||
|
||||
public func withUpdatedMetrics(_ metrics: LayoutMetrics) -> ContainerViewLayout {
|
||||
return ContainerViewLayout(size: self.size, metrics: metrics, intrinsicInsets: self.intrinsicInsets, safeInsets: self.safeInsets, statusBarHeight: self.statusBarHeight, inputHeight: self.inputHeight, standardInputHeight: self.standardInputHeight, inputHeightIsInteractivellyChanging: self.inputHeightIsInteractivellyChanging, inVoiceOver: self.inVoiceOver)
|
||||
}
|
||||
}
|
16
submodules/Display/Display/ContextMenuAction.swift
Normal file
16
submodules/Display/Display/ContextMenuAction.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import UIKit
|
||||
|
||||
public enum ContextMenuActionContent {
|
||||
case text(title: String, accessibilityLabel: String)
|
||||
case icon(UIImage)
|
||||
}
|
||||
|
||||
public struct ContextMenuAction {
|
||||
public let content: ContextMenuActionContent
|
||||
public let action: () -> Void
|
||||
|
||||
public init(content: ContextMenuActionContent, action: @escaping () -> Void) {
|
||||
self.content = content
|
||||
self.action = action
|
||||
}
|
||||
}
|
118
submodules/Display/Display/ContextMenuActionNode.swift
Normal file
118
submodules/Display/Display/ContextMenuActionNode.swift
Normal file
@ -0,0 +1,118 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final private class ContextMenuActionButton: HighlightTrackingButton {
|
||||
override func convert(_ point: CGPoint, from view: UIView?) -> CGPoint {
|
||||
if view is UIWindow {
|
||||
return super.convert(point, from: nil)
|
||||
} else {
|
||||
return super.convert(point, from: view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ContextMenuActionNode: ASDisplayNode {
|
||||
private let textNode: ImmediateTextNode?
|
||||
private var textSize: CGSize?
|
||||
private let iconNode: ASImageNode?
|
||||
private let action: () -> Void
|
||||
private let button: ContextMenuActionButton
|
||||
private let actionArea: AccessibilityAreaNode
|
||||
|
||||
var dismiss: (() -> Void)?
|
||||
|
||||
init(action: ContextMenuAction) {
|
||||
self.actionArea = AccessibilityAreaNode()
|
||||
self.actionArea.accessibilityTraits = UIAccessibilityTraitButton
|
||||
|
||||
switch action.content {
|
||||
case let .text(title, accessibilityLabel):
|
||||
self.actionArea.accessibilityLabel = accessibilityLabel
|
||||
|
||||
let textNode = ImmediateTextNode()
|
||||
textNode.isUserInteractionEnabled = false
|
||||
textNode.displaysAsynchronously = false
|
||||
textNode.attributedText = NSAttributedString(string: title, font: Font.regular(14.0), textColor: UIColor.white)
|
||||
textNode.isAccessibilityElement = false
|
||||
|
||||
self.textNode = textNode
|
||||
self.iconNode = nil
|
||||
case let .icon(image):
|
||||
let iconNode = ASImageNode()
|
||||
iconNode.displaysAsynchronously = false
|
||||
iconNode.displayWithoutProcessing = true
|
||||
iconNode.image = image
|
||||
|
||||
self.iconNode = iconNode
|
||||
self.textNode = nil
|
||||
}
|
||||
self.action = action.action
|
||||
|
||||
self.button = ContextMenuActionButton()
|
||||
self.button.isAccessibilityElement = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
|
||||
if let textNode = self.textNode {
|
||||
self.addSubnode(textNode)
|
||||
}
|
||||
if let iconNode = self.iconNode {
|
||||
self.addSubnode(iconNode)
|
||||
}
|
||||
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
self?.backgroundColor = highlighted ? UIColor(white: 0.0, alpha: 0.4) : UIColor(white: 0.0, alpha: 0.8)
|
||||
}
|
||||
self.view.addSubview(self.button)
|
||||
self.addSubnode(self.actionArea)
|
||||
|
||||
self.actionArea.activate = { [weak self] in
|
||||
self?.buttonPressed()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside])
|
||||
}
|
||||
|
||||
@objc private func buttonPressed() {
|
||||
self.backgroundColor = UIColor(white: 0.0, alpha: 0.4)
|
||||
|
||||
self.action()
|
||||
if let dismiss = self.dismiss {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
if let textNode = self.textNode {
|
||||
let textSize = textNode.updateLayout(constrainedSize)
|
||||
self.textSize = textSize
|
||||
return CGSize(width: textSize.width + 36.0, height: 54.0)
|
||||
} else if let iconNode = self.iconNode, let image = iconNode.image {
|
||||
return CGSize(width: image.size.width + 36.0, height: 54.0)
|
||||
} else {
|
||||
return CGSize(width: 36.0, height: 54.0)
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.button.frame = self.bounds
|
||||
self.actionArea.frame = self.bounds
|
||||
|
||||
if let textNode = self.textNode, let textSize = self.textSize {
|
||||
textNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - textSize.width) / 2.0), y: floor((self.bounds.size.height - textSize.height) / 2.0)), size: textSize)
|
||||
}
|
||||
if let iconNode = self.iconNode, let image = iconNode.image {
|
||||
let iconSize = image.size
|
||||
iconNode.frame = CGRect(origin: CGPoint(x: floor((self.bounds.size.width - iconSize.width) / 2.0), y: floor((self.bounds.size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
}
|
||||
}
|
||||
}
|
90
submodules/Display/Display/ContextMenuContainerNode.swift
Normal file
90
submodules/Display/Display/ContextMenuContainerNode.swift
Normal file
@ -0,0 +1,90 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
private struct CachedMaskParams: Equatable {
|
||||
let size: CGSize
|
||||
let relativeArrowPosition: CGFloat
|
||||
let arrowOnBottom: Bool
|
||||
}
|
||||
|
||||
private final class ContextMenuContainerMaskView: UIView {
|
||||
override class var layerClass: AnyClass {
|
||||
return CAShapeLayer.self
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContextMenuContainerNode: ASDisplayNode {
|
||||
private var cachedMaskParams: CachedMaskParams?
|
||||
private let maskView = ContextMenuContainerMaskView()
|
||||
|
||||
public var relativeArrowPosition: (CGFloat, Bool)?
|
||||
|
||||
//private let effectView: UIVisualEffectView
|
||||
|
||||
override public init() {
|
||||
//self.effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = UIColor(rgb: 0xeaecec)
|
||||
//self.view.addSubview(self.effectView)
|
||||
//self.effectView.mask = self.maskView
|
||||
self.view.mask = self.maskView
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.layer.allowsGroupOpacity = true
|
||||
}
|
||||
|
||||
override public func layout() {
|
||||
super.layout()
|
||||
|
||||
self.updateLayout(transition: .immediate)
|
||||
}
|
||||
|
||||
public func updateLayout(transition: ContainedViewLayoutTransition) {
|
||||
//self.effectView.frame = self.bounds
|
||||
|
||||
let maskParams = CachedMaskParams(size: self.bounds.size, relativeArrowPosition: self.relativeArrowPosition?.0 ?? self.bounds.size.width / 2.0, arrowOnBottom: self.relativeArrowPosition?.1 ?? true)
|
||||
if self.cachedMaskParams != maskParams {
|
||||
let path = UIBezierPath()
|
||||
let cornerRadius: CGFloat = 6.0
|
||||
let verticalInset: CGFloat = 9.0
|
||||
let arrowWidth: CGFloat = 18.0
|
||||
let requestedArrowPosition = maskParams.relativeArrowPosition
|
||||
let arrowPosition = max(cornerRadius + arrowWidth / 2.0, min(maskParams.size.width - cornerRadius - arrowWidth / 2.0, requestedArrowPosition))
|
||||
let arrowOnBottom = maskParams.arrowOnBottom
|
||||
|
||||
path.move(to: CGPoint(x: 0.0, y: verticalInset + cornerRadius))
|
||||
path.addArc(withCenter: CGPoint(x: cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat.pi, endAngle: CGFloat(3.0 * CGFloat.pi / 2.0), clockwise: true)
|
||||
if !arrowOnBottom {
|
||||
path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: verticalInset))
|
||||
path.addLine(to: CGPoint(x: arrowPosition, y: 0.0))
|
||||
path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: verticalInset))
|
||||
}
|
||||
path.addLine(to: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset))
|
||||
path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: verticalInset + cornerRadius), radius: cornerRadius, startAngle: CGFloat(3.0 * CGFloat.pi / 2.0), endAngle: 0.0, clockwise: true)
|
||||
path.addLine(to: CGPoint(x: maskParams.size.width, y: maskParams.size.height - cornerRadius - verticalInset))
|
||||
path.addArc(withCenter: CGPoint(x: maskParams.size.width - cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: 0.0, endAngle: CGFloat(CGFloat.pi / 2.0), clockwise: true)
|
||||
if arrowOnBottom {
|
||||
path.addLine(to: CGPoint(x: arrowPosition + arrowWidth / 2.0, y: maskParams.size.height - verticalInset))
|
||||
path.addLine(to: CGPoint(x: arrowPosition, y: maskParams.size.height))
|
||||
path.addLine(to: CGPoint(x: arrowPosition - arrowWidth / 2.0, y: maskParams.size.height - verticalInset))
|
||||
}
|
||||
path.addLine(to: CGPoint(x: cornerRadius, y: maskParams.size.height - verticalInset))
|
||||
path.addArc(withCenter: CGPoint(x: cornerRadius, y: maskParams.size.height - cornerRadius - verticalInset), radius: cornerRadius, startAngle: CGFloat(CGFloat.pi / 2.0), endAngle: CGFloat(M_PI), clockwise: true)
|
||||
path.close()
|
||||
|
||||
self.cachedMaskParams = maskParams
|
||||
if let layer = self.maskView.layer as? CAShapeLayer {
|
||||
if case let .animated(duration, curve) = transition, let previousPath = layer.path {
|
||||
layer.animate(from: previousPath, to: path.cgPath, keyPath: "path", timingFunction: curve.timingFunction, duration: duration)
|
||||
}
|
||||
layer.path = path.cgPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
96
submodules/Display/Display/ContextMenuController.swift
Normal file
96
submodules/Display/Display/ContextMenuController.swift
Normal file
@ -0,0 +1,96 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public final class ContextMenuControllerPresentationArguments {
|
||||
fileprivate let sourceNodeAndRect: () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?
|
||||
|
||||
public init(sourceNodeAndRect: @escaping () -> (ASDisplayNode, CGRect, ASDisplayNode, CGRect)?) {
|
||||
self.sourceNodeAndRect = sourceNodeAndRect
|
||||
}
|
||||
}
|
||||
|
||||
public final class ContextMenuController: ViewController, KeyShortcutResponder {
|
||||
private var contextMenuNode: ContextMenuNode {
|
||||
return self.displayNode as! ContextMenuNode
|
||||
}
|
||||
|
||||
public var keyShortcuts: [KeyShortcut] {
|
||||
return [KeyShortcut(input: UIKeyInputEscape, action: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.dismiss()
|
||||
}
|
||||
})]
|
||||
}
|
||||
private let actions: [ContextMenuAction]
|
||||
private let catchTapsOutside: Bool
|
||||
private let hasHapticFeedback: Bool
|
||||
|
||||
private var layout: ContainerViewLayout?
|
||||
|
||||
public var dismissed: (() -> Void)?
|
||||
|
||||
public init(actions: [ContextMenuAction], catchTapsOutside: Bool = false, hasHapticFeedback: Bool = false) {
|
||||
self.actions = actions
|
||||
self.catchTapsOutside = catchTapsOutside
|
||||
self.hasHapticFeedback = hasHapticFeedback
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ContextMenuNode(actions: self.actions, dismiss: { [weak self] in
|
||||
self?.dismissed?()
|
||||
self?.contextMenuNode.animateOut {
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
}, catchTapsOutside: self.catchTapsOutside, hasHapticFeedback: self.hasHapticFeedback)
|
||||
self.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
override public func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.contextMenuNode.animateIn()
|
||||
}
|
||||
|
||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||
self.dismissed?()
|
||||
self.contextMenuNode.animateOut { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
if self.layout != nil && self.layout! != layout {
|
||||
self.dismissed?()
|
||||
self.contextMenuNode.animateOut { [weak self] in
|
||||
self?.presentingViewController?.dismiss(animated: false)
|
||||
}
|
||||
} else {
|
||||
self.layout = layout
|
||||
|
||||
if let presentationArguments = self.presentationArguments as? ContextMenuControllerPresentationArguments, let (sourceNode, sourceRect, containerNode, containerRect) = presentationArguments.sourceNodeAndRect() {
|
||||
self.contextMenuNode.sourceRect = sourceNode.view.convert(sourceRect, to: nil)
|
||||
self.contextMenuNode.containerRect = containerNode.view.convert(containerRect, to: nil)
|
||||
} else {
|
||||
self.contextMenuNode.sourceRect = nil
|
||||
self.contextMenuNode.containerRect = nil
|
||||
}
|
||||
|
||||
self.contextMenuNode.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
override public func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.contextMenuNode.animateIn()
|
||||
}
|
||||
}
|
264
submodules/Display/Display/ContextMenuNode.swift
Normal file
264
submodules/Display/Display/ContextMenuNode.swift
Normal file
@ -0,0 +1,264 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
private func generateShadowImage() -> UIImage? {
|
||||
return generateImage(CGSize(width: 30.0, height: 1.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setShadow(offset: CGSize(), blur: 10.0, color: UIColor(white: 0.18, alpha: 1.0).cgColor)
|
||||
context.setFillColor(UIColor(white: 0.18, alpha: 1.0).cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(x: -15.0, y: 0.0), size: CGSize(width: 30.0, height: 1.0)))
|
||||
})
|
||||
}
|
||||
|
||||
private final class ContextMenuContentScrollNode: ASDisplayNode {
|
||||
var contentWidth: CGFloat = 0.0
|
||||
|
||||
private var initialOffset: CGFloat = 0.0
|
||||
|
||||
private let leftShadow: ASImageNode
|
||||
private let rightShadow: ASImageNode
|
||||
private let leftOverscrollNode: ASDisplayNode
|
||||
private let rightOverscrollNode: ASDisplayNode
|
||||
let contentNode: ASDisplayNode
|
||||
|
||||
override init() {
|
||||
self.contentNode = ASDisplayNode()
|
||||
|
||||
let shadowImage = generateShadowImage()
|
||||
|
||||
self.leftShadow = ASImageNode()
|
||||
self.leftShadow.displayWithoutProcessing = true
|
||||
self.leftShadow.displaysAsynchronously = false
|
||||
self.leftShadow.image = shadowImage
|
||||
self.rightShadow = ASImageNode()
|
||||
self.rightShadow.displayWithoutProcessing = true
|
||||
self.rightShadow.displaysAsynchronously = false
|
||||
self.rightShadow.image = shadowImage
|
||||
self.rightShadow.transform = CATransform3DMakeScale(-1.0, 1.0, 1.0)
|
||||
|
||||
self.leftOverscrollNode = ASDisplayNode()
|
||||
self.leftOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
|
||||
self.rightOverscrollNode = ASDisplayNode()
|
||||
self.rightOverscrollNode.backgroundColor = UIColor(white: 0.0, alpha: 0.8)
|
||||
|
||||
super.init()
|
||||
|
||||
self.contentNode.addSubnode(self.leftOverscrollNode)
|
||||
self.contentNode.addSubnode(self.rightOverscrollNode)
|
||||
self.addSubnode(self.contentNode)
|
||||
|
||||
self.addSubnode(self.leftShadow)
|
||||
self.addSubnode(self.rightShadow)
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
|
||||
self.view.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
self.initialOffset = self.contentNode.bounds.origin.x
|
||||
case .changed:
|
||||
var bounds = self.contentNode.bounds
|
||||
bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x
|
||||
if bounds.origin.x > self.contentWidth - bounds.size.width {
|
||||
let delta = bounds.origin.x - (self.contentWidth - bounds.size.width)
|
||||
bounds.origin.x = self.contentWidth - bounds.size.width + ((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0)
|
||||
}
|
||||
if bounds.origin.x < 0.0 {
|
||||
let delta = -bounds.origin.x
|
||||
bounds.origin.x = -((1.0 - (1.0 / (((delta) * 0.55 / (50.0)) + 1.0))) * 50.0)
|
||||
}
|
||||
self.contentNode.bounds = bounds
|
||||
self.updateShadows(.immediate)
|
||||
case .ended, .cancelled:
|
||||
var bounds = self.contentNode.bounds
|
||||
bounds.origin.x = self.initialOffset - recognizer.translation(in: self.view).x
|
||||
|
||||
var duration = 0.4
|
||||
|
||||
if abs(bounds.origin.x - self.initialOffset) > 10.0 || abs(recognizer.velocity(in: self.view).x) > 100.0 {
|
||||
duration = 0.2
|
||||
if bounds.origin.x < self.initialOffset {
|
||||
bounds.origin.x = 0.0
|
||||
} else {
|
||||
bounds.origin.x = self.contentWidth - bounds.size.width
|
||||
}
|
||||
} else {
|
||||
bounds.origin.x = self.initialOffset
|
||||
}
|
||||
|
||||
if bounds.origin.x > self.contentWidth - bounds.size.width {
|
||||
bounds.origin.x = self.contentWidth - bounds.size.width
|
||||
}
|
||||
if bounds.origin.x < 0.0 {
|
||||
bounds.origin.x = 0.0
|
||||
}
|
||||
let previousBounds = self.contentNode.bounds
|
||||
self.contentNode.bounds = bounds
|
||||
self.contentNode.layer.animateBounds(from: previousBounds, to: bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.updateShadows(.animated(duration: duration, curve: .spring))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
let bounds = self.bounds
|
||||
self.contentNode.frame = bounds
|
||||
self.leftShadow.frame = CGRect(origin: CGPoint(), size: CGSize(width: 30.0, height: bounds.height))
|
||||
self.rightShadow.frame = CGRect(origin: CGPoint(x: bounds.size.width - 30.0, y: 0.0), size: CGSize(width: 30.0, height: bounds.height))
|
||||
self.leftOverscrollNode.frame = bounds.offsetBy(dx: -bounds.width, dy: 0.0)
|
||||
self.rightOverscrollNode.frame = bounds.offsetBy(dx: self.contentWidth, dy: 0.0)
|
||||
self.updateShadows(.immediate)
|
||||
}
|
||||
|
||||
private func updateShadows(_ transition: ContainedViewLayoutTransition) {
|
||||
let bounds = self.contentNode.bounds
|
||||
|
||||
let leftAlpha = max(0.0, min(1.0, bounds.minX / 20.0))
|
||||
transition.updateAlpha(node: self.leftShadow, alpha: leftAlpha)
|
||||
|
||||
let rightAlpha = max(0.0, min(1.0, (self.contentWidth - bounds.maxX) / 20.0))
|
||||
transition.updateAlpha(node: self.rightShadow, alpha: rightAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
final class ContextMenuNode: ASDisplayNode {
|
||||
private let actions: [ContextMenuAction]
|
||||
private let dismiss: () -> Void
|
||||
|
||||
private let containerNode: ContextMenuContainerNode
|
||||
private let scrollNode: ContextMenuContentScrollNode
|
||||
private let actionNodes: [ContextMenuActionNode]
|
||||
|
||||
var sourceRect: CGRect?
|
||||
var containerRect: CGRect?
|
||||
var arrowOnBottom: Bool = true
|
||||
|
||||
private var dismissedByTouchOutside = false
|
||||
private let catchTapsOutside: Bool
|
||||
|
||||
private let feedback: HapticFeedback?
|
||||
|
||||
init(actions: [ContextMenuAction], dismiss: @escaping () -> Void, catchTapsOutside: Bool, hasHapticFeedback: Bool = false) {
|
||||
self.actions = actions
|
||||
self.dismiss = dismiss
|
||||
self.catchTapsOutside = catchTapsOutside
|
||||
|
||||
self.containerNode = ContextMenuContainerNode()
|
||||
self.scrollNode = ContextMenuContentScrollNode()
|
||||
|
||||
self.actionNodes = actions.map { action in
|
||||
return ContextMenuActionNode(action: action)
|
||||
}
|
||||
|
||||
if hasHapticFeedback {
|
||||
self.feedback = HapticFeedback()
|
||||
self.feedback?.prepareImpact(.light)
|
||||
} else {
|
||||
self.feedback = nil
|
||||
}
|
||||
|
||||
super.init()
|
||||
|
||||
self.containerNode.addSubnode(self.scrollNode)
|
||||
|
||||
self.addSubnode(self.containerNode)
|
||||
let dismissNode = {
|
||||
dismiss()
|
||||
}
|
||||
for actionNode in self.actionNodes {
|
||||
actionNode.dismiss = dismissNode
|
||||
self.scrollNode.contentNode.addSubnode(actionNode)
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
var unboundActionsWidth: CGFloat = 0.0
|
||||
let actionSeparatorWidth: CGFloat = UIScreenPixel
|
||||
for actionNode in self.actionNodes {
|
||||
if !unboundActionsWidth.isZero {
|
||||
unboundActionsWidth += actionSeparatorWidth
|
||||
}
|
||||
let actionSize = actionNode.measure(CGSize(width: layout.size.width, height: 54.0))
|
||||
actionNode.frame = CGRect(origin: CGPoint(x: unboundActionsWidth, y: 0.0), size: actionSize)
|
||||
unboundActionsWidth += actionSize.width
|
||||
}
|
||||
|
||||
let maxActionsWidth = layout.size.width - 20.0
|
||||
let actionsWidth = min(unboundActionsWidth, maxActionsWidth)
|
||||
|
||||
let sourceRect: CGRect = self.sourceRect ?? CGRect(origin: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0), size: CGSize())
|
||||
let containerRect: CGRect = self.containerRect ?? self.bounds
|
||||
|
||||
let insets = layout.insets(options: [.statusBar, .input])
|
||||
|
||||
let verticalOrigin: CGFloat
|
||||
var arrowOnBottom = true
|
||||
if sourceRect.minY - 54.0 > containerRect.minY + insets.top {
|
||||
verticalOrigin = sourceRect.minY - 54.0
|
||||
} else {
|
||||
verticalOrigin = min(containerRect.maxY - insets.bottom - 54.0, sourceRect.maxY)
|
||||
arrowOnBottom = false
|
||||
}
|
||||
self.arrowOnBottom = arrowOnBottom
|
||||
|
||||
let horizontalOrigin: CGFloat = floor(max(8.0, min(max(sourceRect.minX + 8.0, sourceRect.midX - actionsWidth / 2.0), layout.size.width - actionsWidth - 8.0)))
|
||||
|
||||
self.containerNode.frame = CGRect(origin: CGPoint(x: horizontalOrigin, y: verticalOrigin), size: CGSize(width: actionsWidth, height: 54.0))
|
||||
self.containerNode.relativeArrowPosition = (sourceRect.midX - horizontalOrigin, arrowOnBottom)
|
||||
|
||||
self.scrollNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: actionsWidth, height: 54.0))
|
||||
self.scrollNode.contentWidth = unboundActionsWidth
|
||||
|
||||
self.containerNode.layout()
|
||||
self.scrollNode.layout()
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.containerNode.layer.animateSpring(from: NSNumber(value: Float(0.2)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.4)
|
||||
|
||||
let containerPosition = self.containerNode.layer.position
|
||||
self.containerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: containerPosition.x, y: containerPosition.y + (self.arrowOnBottom ? 1.0 : -1.0) * self.containerNode.bounds.size.height / 2.0)), to: NSValue(cgPoint: containerPosition), keyPath: "position", duration: 0.4)
|
||||
|
||||
self.containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
|
||||
|
||||
if let feedback = self.feedback {
|
||||
feedback.impact(.light)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if let event = event {
|
||||
var eventIsPresses = false
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
eventIsPresses = event.type == .presses
|
||||
}
|
||||
if event.type == .touches || eventIsPresses {
|
||||
if !self.containerNode.frame.contains(point) {
|
||||
if !self.dismissedByTouchOutside {
|
||||
self.dismissedByTouchOutside = true
|
||||
self.dismiss()
|
||||
}
|
||||
if self.catchTapsOutside {
|
||||
return self.view
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
}
|
184
submodules/Display/Display/DeviceMetrics.swift
Normal file
184
submodules/Display/Display/DeviceMetrics.swift
Normal file
@ -0,0 +1,184 @@
|
||||
import UIKit
|
||||
|
||||
public enum DeviceMetrics: CaseIterable {
|
||||
case iPhone4
|
||||
case iPhone5
|
||||
case iPhone6
|
||||
case iPhone6Plus
|
||||
case iPhoneX
|
||||
case iPhoneXSMax
|
||||
case iPad
|
||||
case iPadPro10Inch
|
||||
case iPadPro11Inch
|
||||
case iPadPro
|
||||
case iPadPro3rdGen
|
||||
|
||||
public static func forScreenSize(_ size: CGSize, hintHasOnScreenNavigation: Bool = false) -> DeviceMetrics? {
|
||||
let additionalSize = CGSize(width: size.width, height: size.height + 20.0)
|
||||
for device in DeviceMetrics.allCases {
|
||||
let width = device.screenSize.width
|
||||
let height = device.screenSize.height
|
||||
|
||||
if ((size.width.isEqual(to: width) && size.height.isEqual(to: height)) || size.height.isEqual(to: width) && size.width.isEqual(to: height)) || ((additionalSize.width.isEqual(to: width) && additionalSize.height.isEqual(to: height)) || additionalSize.height.isEqual(to: width) && additionalSize.width.isEqual(to: height)) {
|
||||
if hintHasOnScreenNavigation && device.onScreenNavigationHeight(inLandscape: false) == nil {
|
||||
continue
|
||||
}
|
||||
return device
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var screenSize: CGSize {
|
||||
switch self {
|
||||
case .iPhone4:
|
||||
return CGSize(width: 320.0, height: 480.0)
|
||||
case .iPhone5:
|
||||
return CGSize(width: 320.0, height: 568.0)
|
||||
case .iPhone6:
|
||||
return CGSize(width: 375.0, height: 667.0)
|
||||
case .iPhone6Plus:
|
||||
return CGSize(width: 414.0, height: 736.0)
|
||||
case .iPhoneX:
|
||||
return CGSize(width: 375.0, height: 812.0)
|
||||
case .iPhoneXSMax:
|
||||
return CGSize(width: 414.0, height: 896.0)
|
||||
case .iPad:
|
||||
return CGSize(width: 768.0, height: 1024.0)
|
||||
case .iPadPro10Inch:
|
||||
return CGSize(width: 834.0, height: 1112.0)
|
||||
case .iPadPro11Inch:
|
||||
return CGSize(width: 834.0, height: 1194.0)
|
||||
case .iPadPro, .iPadPro3rdGen:
|
||||
return CGSize(width: 1024.0, height: 1366.0)
|
||||
}
|
||||
}
|
||||
|
||||
func safeAreaInsets(inLandscape: Bool) -> UIEdgeInsets {
|
||||
switch self {
|
||||
case .iPhoneX, .iPhoneXSMax:
|
||||
return inLandscape ? UIEdgeInsets(top: 0.0, left: 44.0, bottom: 0.0, right: 44.0) : UIEdgeInsets(top: 44.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
default:
|
||||
return UIEdgeInsets.zero
|
||||
}
|
||||
}
|
||||
|
||||
func onScreenNavigationHeight(inLandscape: Bool) -> CGFloat? {
|
||||
switch self {
|
||||
case .iPhoneX, .iPhoneXSMax:
|
||||
return inLandscape ? 21.0 : 34.0
|
||||
case .iPadPro3rdGen, .iPadPro11Inch:
|
||||
return 21.0
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var statusBarHeight: CGFloat {
|
||||
switch self {
|
||||
case .iPhoneX, .iPhoneXSMax:
|
||||
return 44.0
|
||||
case .iPadPro11Inch, .iPadPro3rdGen:
|
||||
return 24.0
|
||||
default:
|
||||
return 20.0
|
||||
}
|
||||
}
|
||||
|
||||
public func standardInputHeight(inLandscape: Bool) -> CGFloat {
|
||||
if inLandscape {
|
||||
switch self {
|
||||
case .iPhone4, .iPhone5:
|
||||
return 162.0
|
||||
case .iPhone6, .iPhone6Plus:
|
||||
return 163.0
|
||||
case .iPhoneX, .iPhoneXSMax:
|
||||
return 172.0
|
||||
case .iPad, .iPadPro10Inch:
|
||||
return 348.0
|
||||
case .iPadPro11Inch:
|
||||
return 368.0
|
||||
case .iPadPro:
|
||||
return 421.0
|
||||
case .iPadPro3rdGen:
|
||||
return 441.0
|
||||
}
|
||||
} else {
|
||||
switch self {
|
||||
case .iPhone4, .iPhone5, .iPhone6:
|
||||
return 216.0
|
||||
case .iPhone6Plus:
|
||||
return 227.0
|
||||
case .iPhoneX:
|
||||
return 291.0
|
||||
case .iPhoneXSMax:
|
||||
return 302.0
|
||||
case .iPad, .iPadPro10Inch:
|
||||
return 263.0
|
||||
case .iPadPro11Inch:
|
||||
return 283.0
|
||||
case .iPadPro:
|
||||
return 328.0
|
||||
case .iPadPro3rdGen:
|
||||
return 348.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func predictiveInputHeight(inLandscape: Bool) -> CGFloat {
|
||||
if inLandscape {
|
||||
switch self {
|
||||
case .iPhone4, .iPhone5, .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax:
|
||||
return 37.0
|
||||
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
|
||||
return 50.0
|
||||
}
|
||||
} else {
|
||||
switch self {
|
||||
case .iPhone4, .iPhone5:
|
||||
return 37.0
|
||||
case .iPhone6, .iPhone6Plus, .iPhoneX, .iPhoneXSMax:
|
||||
return 44.0
|
||||
case .iPad, .iPadPro10Inch, .iPadPro11Inch, .iPadPro, .iPadPro3rdGen:
|
||||
return 50.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func previewingContentSize(inLandscape: Bool) -> CGSize {
|
||||
let screenSize = self.screenSize
|
||||
if inLandscape {
|
||||
switch self {
|
||||
case .iPhone5:
|
||||
return CGSize(width: screenSize.height, height: screenSize.width - 10.0)
|
||||
case .iPhone6:
|
||||
return CGSize(width: screenSize.height, height: screenSize.width - 22.0)
|
||||
case .iPhone6Plus:
|
||||
return CGSize(width: screenSize.height, height: screenSize.width - 22.0)
|
||||
case .iPhoneX:
|
||||
return CGSize(width: screenSize.height, height: screenSize.width + 48.0)
|
||||
case .iPhoneXSMax:
|
||||
return CGSize(width: screenSize.height, height: screenSize.width - 30.0)
|
||||
default:
|
||||
return CGSize(width: screenSize.height, height: screenSize.width - 10.0)
|
||||
}
|
||||
} else {
|
||||
switch self {
|
||||
case .iPhone5:
|
||||
return CGSize(width: screenSize.width, height: screenSize.height - 50.0)
|
||||
case .iPhone6:
|
||||
return CGSize(width: screenSize.width, height: screenSize.height - 97.0)
|
||||
case .iPhone6Plus:
|
||||
return CGSize(width: screenSize.width, height: screenSize.height - 95.0)
|
||||
case .iPhoneX:
|
||||
return CGSize(width: screenSize.width, height: screenSize.height - 154.0)
|
||||
case .iPhoneXSMax:
|
||||
return CGSize(width: screenSize.width, height: screenSize.height - 84.0)
|
||||
default:
|
||||
return CGSize(width: screenSize.width, height: screenSize.height - 50.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
34
submodules/Display/Display/Display.h
Normal file
34
submodules/Display/Display/Display.h
Normal file
@ -0,0 +1,34 @@
|
||||
//
|
||||
// Display.h
|
||||
// Display
|
||||
//
|
||||
// Created by Peter on 29/07/15.
|
||||
// Copyright © 2015 Telegram. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for Display.
|
||||
FOUNDATION_EXPORT double DisplayVersionNumber;
|
||||
|
||||
//! Project version string for Display.
|
||||
FOUNDATION_EXPORT const unsigned char DisplayVersionString[];
|
||||
|
||||
// In this header, you should import all the public headers of your framework using statements like #import <Display/PublicHeader.h>
|
||||
|
||||
#import <Display/RuntimeUtils.h>
|
||||
#import <Display/UIViewController+Navigation.h>
|
||||
#import <Display/UIKitUtils.h>
|
||||
#import <Display/UINavigationItem+Proxy.h>
|
||||
#import <Display/UIWindow+OrientationChange.h>
|
||||
#import <Display/NotificationCenterUtils.h>
|
||||
#import <Display/NSBag.h>
|
||||
#import <Display/UIBarButtonItem+Proxy.h>
|
||||
#import <Display/NavigationControllerProxy.h>
|
||||
#import <Display/NavigationBarProxy.h>
|
||||
#import <UIKit/UIGestureRecognizerSubclass.h>
|
||||
#import <Display/NSWeakReference.h>
|
||||
#import <Display/FBAnimationPerformanceTracker.h>
|
||||
#import <Display/CATracingLayer.h>
|
||||
#import <Display/CASeeThroughTracingLayer.h>
|
||||
#import <Display/UIMenuItem+Icons.h>
|
47
submodules/Display/Display/DisplayLinkDispatcher.swift
Normal file
47
submodules/Display/Display/DisplayLinkDispatcher.swift
Normal file
@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public class DisplayLinkDispatcher: NSObject {
|
||||
private var displayLink: CADisplayLink!
|
||||
private var blocksToDispatch: [() -> Void] = []
|
||||
private let limit: Int
|
||||
|
||||
public init(limit: Int = 0) {
|
||||
self.limit = limit
|
||||
|
||||
super.init()
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
//self.displayLink.preferredFramesPerSecond = 60
|
||||
} else {
|
||||
self.displayLink = CADisplayLink(target: self, selector: #selector(self.run))
|
||||
self.displayLink.isPaused = true
|
||||
self.displayLink.add(to: RunLoop.main, forMode: RunLoopMode.commonModes)
|
||||
}
|
||||
}
|
||||
|
||||
public func dispatch(f: @escaping () -> Void) {
|
||||
if self.displayLink == nil {
|
||||
if Thread.isMainThread {
|
||||
f()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: f)
|
||||
}
|
||||
} else {
|
||||
self.blocksToDispatch.append(f)
|
||||
self.displayLink.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func run() {
|
||||
for _ in 0 ..< (self.limit == 0 ? 1000 : self.limit) {
|
||||
if self.blocksToDispatch.count == 0 {
|
||||
self.displayLink.isPaused = true
|
||||
break
|
||||
} else {
|
||||
let f = self.blocksToDispatch.removeFirst()
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
submodules/Display/Display/EditableTextNode.swift
Normal file
24
submodules/Display/Display/EditableTextNode.swift
Normal file
@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public class EditableTextNode: ASEditableTextNode {
|
||||
override public var keyboardAppearance: UIKeyboardAppearance {
|
||||
get {
|
||||
return super.keyboardAppearance
|
||||
}
|
||||
set {
|
||||
guard newValue != self.keyboardAppearance else {
|
||||
return
|
||||
}
|
||||
let resigning = self.isFirstResponder()
|
||||
if resigning {
|
||||
self.resignFirstResponder()
|
||||
}
|
||||
super.keyboardAppearance = newValue
|
||||
if resigning {
|
||||
self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
submodules/Display/Display/FBAnimationPerformanceTracker.h
Normal file
136
submodules/Display/Display/FBAnimationPerformanceTracker.h
Normal file
@ -0,0 +1,136 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*
|
||||
* This is an example provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
*
|
||||
* FBAnimationPerformanceTracker
|
||||
* -----------------------------------------------------------------------
|
||||
*
|
||||
* This class provides animation performance tracking functionality. It basically
|
||||
* measures the app's frame rate during an operation, and reports this information.
|
||||
*
|
||||
* 1) In Foo's designated initializer, construct a tracker object
|
||||
*
|
||||
* 2) Add calls to -start and -stop in appropriate places, e.g. for a ScrollView
|
||||
*
|
||||
* - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
||||
* [_apTracker start];
|
||||
* }
|
||||
*
|
||||
* - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
|
||||
* {
|
||||
* if (!scrollView.dragging) {
|
||||
* [_apTracker stop];
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
||||
* if (!decelerate) {
|
||||
* [_apTracker stop];
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Notes
|
||||
* -----
|
||||
* [] The tracker operates by creating a CADisplayLink object to measure the frame rate of the display
|
||||
* during start/stop interval.
|
||||
*
|
||||
* [] Calls to -stop that were not preceded by a matching call to -start have no effect.
|
||||
*
|
||||
* [] 2 calls to -start in a row will trash the data accumulated so far and not log anything.
|
||||
*
|
||||
*
|
||||
* Configuration object for the core tracker
|
||||
*
|
||||
* ===============================================================================
|
||||
* I highly recommend for you to use the standard configuration provided
|
||||
* These are essentially here so that the computation of the metric is transparent
|
||||
* and you can feel confident in what the numbers mean.
|
||||
* ===============================================================================
|
||||
*/
|
||||
struct FBAnimationPerformanceTrackerConfig
|
||||
{
|
||||
// Number of frame drop that defines a "small" drop event. By default, 1.
|
||||
NSInteger smallDropEventFrameNumber;
|
||||
// Number of frame drop that defines a "large" drop event. By default, 4.
|
||||
NSInteger largeDropEventFrameNumber;
|
||||
// Number of maximum frame drops to which the drop will be trimmed down to. Currently 15.
|
||||
NSInteger maxFrameDropAccount;
|
||||
|
||||
// If YES, will report stack traces
|
||||
BOOL reportStackTraces;
|
||||
};
|
||||
typedef struct FBAnimationPerformanceTrackerConfig FBAnimationPerformanceTrackerConfig;
|
||||
|
||||
|
||||
@protocol FBAnimationPerformanceTrackerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Core Metric
|
||||
*
|
||||
* You are responsible for the aggregation of these metrics (it being on the client or the server). I recommend to implement both
|
||||
* to limit the payload you are sending to the server.
|
||||
*
|
||||
* The final recommended metric being: - SUM(duration) / SUM(smallDropEvent) aka the number of seconds between one frame drop or more
|
||||
* - SUM(duration) / SUM(largeDropEvent) aka the number of seconds between four frame drops or more
|
||||
*
|
||||
* The first metric will tell you how smooth is your scroll view.
|
||||
* The second metric will tell you how clowny your scroll view can get.
|
||||
*
|
||||
* Every time stop is called, this event will fire reporting the performance.
|
||||
*
|
||||
* NOTE on this metric:
|
||||
* - It has been tested at scale on many Facebook apps.
|
||||
* - It follows the curves of devices.
|
||||
* - You will need about 100K calls for the number to converge.
|
||||
* - It is perfectly correlated to X = Percentage of time spent at 60fps. Number of seconds between one frame drop = 1 / ( 1 - Time spent at 60 fps)
|
||||
* - We report fraction of drops. 7 frame drop = 1.75 of a large frame drop if a large drop is 4 frame drop.
|
||||
* This is to preserve the correlation mentionned above.
|
||||
*/
|
||||
- (void)reportDurationInMS:(NSInteger)duration smallDropEvent:(double)smallDropEvent largeDropEvent:(double)largeDropEvent;
|
||||
|
||||
/**
|
||||
* Stack traces
|
||||
*
|
||||
* Dark magic of the animation tracker. In case of a frame drop, this will return a stack trace.
|
||||
* This will NOT be reported on the main-thread, but off-main thread to save a few CPU cycles.
|
||||
*
|
||||
* The slide is constant value that needs to be reported with the stack for processing.
|
||||
* This currently only allows for symbolication of your own image.
|
||||
*
|
||||
* Future work includes symbolicating all modules. I personnaly find it usually
|
||||
* good enough to know the name of the module.
|
||||
*
|
||||
* The stack will have the following format:
|
||||
* Foundation:0x123|MyApp:0x234|MyApp:0x345|
|
||||
*
|
||||
* The slide will have the following format:
|
||||
* 0x456
|
||||
*/
|
||||
- (void)reportStackTrace:(NSString *)stack withSlide:(NSString *)slide;
|
||||
|
||||
@end
|
||||
|
||||
@interface FBAnimationPerformanceTracker : NSObject
|
||||
|
||||
- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config;
|
||||
|
||||
+ (FBAnimationPerformanceTrackerConfig)standardConfig;
|
||||
|
||||
@property (weak, nonatomic, readwrite) id<FBAnimationPerformanceTrackerDelegate> delegate;
|
||||
|
||||
- (void)start;
|
||||
- (void)stop;
|
||||
|
||||
@end
|
412
submodules/Display/Display/FBAnimationPerformanceTracker.mm
Normal file
412
submodules/Display/Display/FBAnimationPerformanceTracker.mm
Normal file
@ -0,0 +1,412 @@
|
||||
//
|
||||
// FBAnimationPerformanceTracker.m
|
||||
// Display
|
||||
//
|
||||
// Created by Peter on 3/16/16.
|
||||
// Copyright © 2016 Telegram. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FBAnimationPerformanceTracker.h"
|
||||
|
||||
#import <dlfcn.h>
|
||||
#import <map>
|
||||
#import <pthread.h>
|
||||
|
||||
#import <QuartzCore/CADisplayLink.h>
|
||||
|
||||
#import <mach-o/dyld.h>
|
||||
|
||||
#import "execinfo.h"
|
||||
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
static BOOL _signalSetup;
|
||||
static pthread_t _mainThread;
|
||||
static NSThread *_trackerThread;
|
||||
|
||||
static std::map<void *, NSString *, std::greater<void *>> _imageNames;
|
||||
|
||||
#ifdef __LP64__
|
||||
typedef mach_header_64 fb_mach_header;
|
||||
typedef segment_command_64 fb_mach_segment_command;
|
||||
#define LC_SEGMENT_ARCH LC_SEGMENT_64
|
||||
#else
|
||||
typedef mach_header fb_mach_header;
|
||||
typedef segment_command fb_mach_segment_command;
|
||||
#define LC_SEGMENT_ARCH LC_SEGMENT
|
||||
#endif
|
||||
|
||||
static volatile BOOL _scrolling;
|
||||
pthread_mutex_t _scrollingMutex;
|
||||
pthread_cond_t _scrollingCondVariable;
|
||||
dispatch_queue_t _symbolicationQueue;
|
||||
|
||||
// We record at most 16 frames since I cap the number of frames dropped measured at 15.
|
||||
// Past 15, something went very wrong (massive contention, priority inversion, rpc call going wrong...) .
|
||||
// It will only pollute the data to get more.
|
||||
static const int callstack_max_number = 16;
|
||||
|
||||
static int callstack_i;
|
||||
static bool callstack_dirty;
|
||||
static int callstack_size[callstack_max_number];
|
||||
static void *callstacks[callstack_max_number][128];
|
||||
uint64_t callstack_time_capture;
|
||||
|
||||
static void _callstack_signal_handler(int signr, siginfo_t *info, void *secret)
|
||||
{
|
||||
// This is run on the main thread every 16 ms or so during scroll.
|
||||
|
||||
// Signals are run one by one so there is no risk of concurrency of a signal
|
||||
// by the same signal.
|
||||
|
||||
// The backtrace call is technically signal-safe on Unix-based system
|
||||
// See: http://www.unix.com/man-page/all/3c/walkcontext/
|
||||
|
||||
// WARNING: this is signal handler, no memory allocation is safe.
|
||||
// Essentially nothing is safe unless specified it is.
|
||||
callstack_size[callstack_i] = backtrace(callstacks[callstack_i], 128);
|
||||
callstack_i = (callstack_i + 1) & (callstack_max_number - 1); // & is a cheap modulo (only works for power of 2)
|
||||
callstack_dirty = true;
|
||||
}
|
||||
|
||||
@interface FBCallstack : NSObject
|
||||
@property (nonatomic, readonly, assign) int size;
|
||||
@property (nonatomic, readonly, assign) void **callstack;
|
||||
- (instancetype)initWithSize:(int)size callstack:(void *)callstack;
|
||||
@end
|
||||
|
||||
@implementation FBCallstack
|
||||
- (instancetype)initWithSize:(int)size callstack:(void *)callstack
|
||||
{
|
||||
if (self = [super init]) {
|
||||
_size = size;
|
||||
_callstack = (void **)malloc(size * sizeof(void *));
|
||||
memcpy(_callstack, callstack, size * sizeof(void *));
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
free(_callstack);
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation FBAnimationPerformanceTracker
|
||||
{
|
||||
FBAnimationPerformanceTrackerConfig _config;
|
||||
|
||||
BOOL _tracking;
|
||||
BOOL _firstUpdate;
|
||||
NSTimeInterval _previousFrameTimestamp;
|
||||
CADisplayLink *_displayLink;
|
||||
BOOL _prepared;
|
||||
|
||||
// numbers used to track the performance metrics
|
||||
double _durationTotal;
|
||||
double _maxFrameTime;
|
||||
double _smallDrops;
|
||||
double _largeDrops;
|
||||
}
|
||||
|
||||
- (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config
|
||||
{
|
||||
if (self = [super init]) {
|
||||
// Stack trace logging is not working well in debug mode
|
||||
// We don't want the data anyway. So let's bail.
|
||||
#if defined(DEBUG)
|
||||
config.reportStackTraces = NO;
|
||||
#endif
|
||||
_config = config;
|
||||
if (config.reportStackTraces) {
|
||||
[self _setupSignal];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (FBAnimationPerformanceTrackerConfig)standardConfig
|
||||
{
|
||||
FBAnimationPerformanceTrackerConfig config = {
|
||||
.smallDropEventFrameNumber = 1,
|
||||
.largeDropEventFrameNumber = 4,
|
||||
.maxFrameDropAccount = 15,
|
||||
.reportStackTraces = NO
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
+ (void)_trackerLoop
|
||||
{
|
||||
while (true) {
|
||||
// If you are confused by this part,
|
||||
// Check out https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables
|
||||
|
||||
// Lock the mutex
|
||||
pthread_mutex_lock(&_scrollingMutex);
|
||||
while (!_scrolling) {
|
||||
// Unlock the mutex and sleep until the conditional variable is signaled
|
||||
pthread_cond_wait(&_scrollingCondVariable, &_scrollingMutex);
|
||||
// The conditional variable was signaled, but we need to check _scrolling
|
||||
// As nothing guarantees that it is still true
|
||||
}
|
||||
// _scrolling is true, go ahead and capture traces for a while.
|
||||
pthread_mutex_unlock(&_scrollingMutex);
|
||||
|
||||
// We are scrolling, yay, capture traces
|
||||
while (_scrolling) {
|
||||
usleep(16000);
|
||||
|
||||
// Here I use SIGPROF which is a signal supposed to be used for profiling
|
||||
// I haven't stumbled upon any collision so far.
|
||||
// There is no guarantee that it won't impact the system in unpredicted ways.
|
||||
// Use wisely.
|
||||
|
||||
pthread_kill(_mainThread, SIGPROF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_setupSignal
|
||||
{
|
||||
if (!_signalSetup) {
|
||||
// The signal hook should be setup once and only once
|
||||
_signalSetup = YES;
|
||||
|
||||
// I actually don't know if the main thread can die. If it does, well,
|
||||
// this is not going to work.
|
||||
// UPDATE 4/2015: on iOS8, it looks like the main-thread never dies, and this pointer is correct
|
||||
_mainThread = pthread_self();
|
||||
|
||||
callstack_i = 0;
|
||||
|
||||
// Setup the signal
|
||||
struct sigaction sa;
|
||||
sigfillset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_SIGINFO;
|
||||
sa.sa_sigaction = _callstack_signal_handler;
|
||||
sigaction(SIGPROF, &sa, NULL);
|
||||
|
||||
pthread_mutex_init(&_scrollingMutex, NULL);
|
||||
pthread_cond_init (&_scrollingCondVariable, NULL);
|
||||
|
||||
// Setup the signal firing loop
|
||||
_trackerThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(_trackerLoop) object:nil];
|
||||
// We wanna be higher priority than the main thread
|
||||
// On iOS8 : this will roughly stick us at priority 61, while the main thread oscillates between 20 and 47
|
||||
_trackerThread.threadPriority = 1.0;
|
||||
[_trackerThread start];
|
||||
|
||||
_symbolicationQueue = dispatch_queue_create("com.facebook.symbolication", DISPATCH_QUEUE_SERIAL);
|
||||
dispatch_async(_symbolicationQueue, ^(void) {[self _setupSymbolication];});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_setupSymbolication
|
||||
{
|
||||
// This extract the starting slide of every module in the app
|
||||
// This is used to know which module an instruction pointer belongs to.
|
||||
|
||||
// These operations is NOT thread-safe according to Apple docs
|
||||
// Do not call this multiple times
|
||||
int images = _dyld_image_count();
|
||||
|
||||
for (int i = 0; i < images; i ++) {
|
||||
intptr_t imageSlide = _dyld_get_image_vmaddr_slide(i);
|
||||
|
||||
// Here we extract the module name from the full path
|
||||
// Typically it looks something like: /path/to/lib/UIKit
|
||||
// And I just extract UIKit
|
||||
NSString *fullName = [NSString stringWithUTF8String:_dyld_get_image_name(i)];
|
||||
NSRange range = [fullName rangeOfString:@"/" options:NSBackwardsSearch];
|
||||
NSUInteger startP = (range.location != NSNotFound) ? range.location + 1 : 0;
|
||||
NSString *imageName = [fullName substringFromIndex:startP];
|
||||
|
||||
// This is parsing the mach header in order to extract the slide.
|
||||
// See https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachORuntime/index.html
|
||||
// For the structure of mach headers
|
||||
fb_mach_header *header = (fb_mach_header*)_dyld_get_image_header(i);
|
||||
if (!header) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const struct load_command *cmd =
|
||||
reinterpret_cast<const struct load_command *>(header + 1);
|
||||
|
||||
for (unsigned int c = 0; cmd && (c < header->ncmds); c++) {
|
||||
if (cmd->cmd == LC_SEGMENT_ARCH) {
|
||||
const fb_mach_segment_command *seg =
|
||||
reinterpret_cast<const fb_mach_segment_command *>(cmd);
|
||||
|
||||
if (!strcmp(seg->segname, "__TEXT")) {
|
||||
_imageNames[(void *)(seg->vmaddr + imageSlide)] = imageName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
cmd = reinterpret_cast<struct load_command*>((char *)cmd + cmd->cmdsize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_prepared) {
|
||||
[self _tearDownCADisplayLink];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Tracking
|
||||
|
||||
- (void)start
|
||||
{
|
||||
if (!_tracking) {
|
||||
if ([self prepare]) {
|
||||
_displayLink.paused = NO;
|
||||
_tracking = YES;
|
||||
[self _reset];
|
||||
|
||||
if (_config.reportStackTraces) {
|
||||
pthread_mutex_lock(&_scrollingMutex);
|
||||
_scrolling = YES;
|
||||
// Signal the tracker thread to start firing the signals
|
||||
pthread_cond_signal(&_scrollingCondVariable);
|
||||
pthread_mutex_unlock(&_scrollingMutex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stop
|
||||
{
|
||||
if (_tracking) {
|
||||
_tracking = NO;
|
||||
_displayLink.paused = YES;
|
||||
if (_durationTotal > 0) {
|
||||
[_delegate reportDurationInMS:round(1000.0 * _durationTotal) smallDropEvent:_smallDrops largeDropEvent:_largeDrops];
|
||||
if (_config.reportStackTraces) {
|
||||
pthread_mutex_lock(&_scrollingMutex);
|
||||
_scrolling = NO;
|
||||
pthread_mutex_unlock(&_scrollingMutex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)prepare
|
||||
{
|
||||
if (_prepared) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
[self _setUpCADisplayLink];
|
||||
_prepared = YES;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)_setUpCADisplayLink
|
||||
{
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update)];
|
||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
|
||||
_displayLink.paused = YES;
|
||||
}
|
||||
|
||||
- (void)_tearDownCADisplayLink
|
||||
{
|
||||
[_displayLink invalidate];
|
||||
_displayLink = nil;
|
||||
}
|
||||
|
||||
- (void)_reset
|
||||
{
|
||||
_firstUpdate = YES;
|
||||
_previousFrameTimestamp = 0.0;
|
||||
_durationTotal = 0;
|
||||
_maxFrameTime = 0;
|
||||
_largeDrops = 0;
|
||||
_smallDrops = 0;
|
||||
}
|
||||
|
||||
- (void)_addFrameTime:(NSTimeInterval)actualFrameTime singleFrameTime:(NSTimeInterval)singleFrameTime
|
||||
{
|
||||
_maxFrameTime = MAX(actualFrameTime, _maxFrameTime);
|
||||
|
||||
NSInteger frameDropped = round(actualFrameTime / singleFrameTime) - 1;
|
||||
frameDropped = MAX(frameDropped, 0);
|
||||
// This is to reduce noise. Massive frame drops will just add noise to your data.
|
||||
frameDropped = MIN(_config.maxFrameDropAccount, frameDropped);
|
||||
|
||||
_durationTotal += (frameDropped + 1) * singleFrameTime;
|
||||
// We account 2 frame drops as 2 small events. This way the metric correlates perfectly with Time at X fps.
|
||||
_smallDrops += (frameDropped >= _config.smallDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.smallDropEventFrameNumber : 0.0;
|
||||
_largeDrops += (frameDropped >= _config.largeDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.largeDropEventFrameNumber : 0.0;
|
||||
|
||||
if (frameDropped >= 1) {
|
||||
if (_config.reportStackTraces) {
|
||||
callstack_dirty = false;
|
||||
for (int ci = 0; ci <= frameDropped ; ci ++) {
|
||||
// This is computing the previous indexes
|
||||
// callstack - 1 - ci takes us back ci frames
|
||||
// I want a positive number so I add callstack_max_number
|
||||
// And then just modulo it, with & (callstack_max_number - 1)
|
||||
int callstackPreviousIndex = ((callstack_i - 1 - ci) + callstack_max_number) & (callstack_max_number - 1);
|
||||
FBCallstack *callstackCopy = [[FBCallstack alloc] initWithSize:callstack_size[callstackPreviousIndex] callstack:callstacks[callstackPreviousIndex]];
|
||||
// Check that in between the beginning and the end of the copy the signal did not fire
|
||||
if (!callstack_dirty) {
|
||||
// The copy has been made. We are now fine, let's punt the rest off main-thread.
|
||||
__weak FBAnimationPerformanceTracker *weakSelf = self;
|
||||
dispatch_async(_symbolicationQueue, ^(void) {
|
||||
[weakSelf _reportStackTrace:callstackCopy];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_update
|
||||
{
|
||||
if (!_tracking) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_firstUpdate) {
|
||||
_firstUpdate = NO;
|
||||
_previousFrameTimestamp = _displayLink.timestamp;
|
||||
return;
|
||||
}
|
||||
|
||||
NSTimeInterval currentTimestamp = _displayLink.timestamp;
|
||||
NSTimeInterval frameTime = currentTimestamp - _previousFrameTimestamp;
|
||||
[self _addFrameTime:frameTime singleFrameTime:_displayLink.duration];
|
||||
_previousFrameTimestamp = currentTimestamp;
|
||||
}
|
||||
|
||||
- (void)_reportStackTrace:(FBCallstack *)callstack
|
||||
{
|
||||
static NSString *slide;
|
||||
static dispatch_once_t slide_predicate;
|
||||
|
||||
dispatch_once(&slide_predicate, ^{
|
||||
slide = [NSString stringWithFormat:@"%p", (void *)_dyld_get_image_header(0)];
|
||||
});
|
||||
|
||||
@autoreleasepool {
|
||||
NSMutableString *stack = [NSMutableString string];
|
||||
|
||||
for (int j = 2; j < callstack.size; j ++) {
|
||||
void *instructionPointer = callstack.callstack[j];
|
||||
auto it = _imageNames.lower_bound(instructionPointer);
|
||||
|
||||
NSString *imageName = (it != _imageNames.end()) ? it->second : @"???";
|
||||
|
||||
[stack appendString:imageName];
|
||||
[stack appendString:@":"];
|
||||
[stack appendString:[NSString stringWithFormat:@"%p", instructionPointer]];
|
||||
[stack appendString:@"|"];
|
||||
}
|
||||
|
||||
[_delegate reportStackTrace:stack withSlide:slide];
|
||||
}
|
||||
}
|
||||
@end
|
84
submodules/Display/Display/Font.swift
Normal file
84
submodules/Display/Display/Font.swift
Normal file
@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public struct Font {
|
||||
public static func regular(_ size: CGFloat) -> UIFont {
|
||||
return UIFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
public static func medium(_ size: CGFloat) -> UIFont {
|
||||
if #available(iOS 8.2, *) {
|
||||
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.medium)
|
||||
} else {
|
||||
return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil)
|
||||
}
|
||||
}
|
||||
|
||||
public static func semibold(_ size: CGFloat) -> UIFont {
|
||||
if #available(iOS 8.2, *) {
|
||||
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.semibold)
|
||||
} else {
|
||||
return CTFontCreateWithName("HelveticaNeue-Medium" as CFString, size, nil)
|
||||
}
|
||||
}
|
||||
|
||||
public static func bold(_ size: CGFloat) -> UIFont {
|
||||
if #available(iOS 8.2, *) {
|
||||
return UIFont.boldSystemFont(ofSize: size)
|
||||
} else {
|
||||
return CTFontCreateWithName("HelveticaNeue-Bold" as CFString, size, nil)
|
||||
}
|
||||
}
|
||||
|
||||
public static func light(_ size: CGFloat) -> UIFont {
|
||||
if #available(iOS 8.2, *) {
|
||||
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light)
|
||||
} else {
|
||||
return CTFontCreateWithName("HelveticaNeue-Light" as CFString, size, nil)
|
||||
}
|
||||
}
|
||||
|
||||
public static func semiboldItalic(_ size: CGFloat) -> UIFont {
|
||||
if let descriptor = UIFont.systemFont(ofSize: size).fontDescriptor.withSymbolicTraits([.traitBold, .traitItalic]) {
|
||||
return UIFont(descriptor: descriptor, size: size)
|
||||
} else {
|
||||
return UIFont.italicSystemFont(ofSize: size)
|
||||
}
|
||||
}
|
||||
|
||||
public static func monospace(_ size: CGFloat) -> UIFont {
|
||||
return UIFont(name: "Menlo-Regular", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
public static func semiboldMonospace(_ size: CGFloat) -> UIFont {
|
||||
return UIFont(name: "Menlo-Bold", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
public static func italicMonospace(_ size: CGFloat) -> UIFont {
|
||||
return UIFont(name: "Menlo-Italic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
public static func semiboldItalicMonospace(_ size: CGFloat) -> UIFont {
|
||||
return UIFont(name: "Menlo-BoldItalic", size: size - 1.0) ?? UIFont.systemFont(ofSize: size)
|
||||
}
|
||||
|
||||
public static func italic(_ size: CGFloat) -> UIFont {
|
||||
return UIFont.italicSystemFont(ofSize: size)
|
||||
}
|
||||
}
|
||||
|
||||
public extension NSAttributedString {
|
||||
convenience init(string: String, font: UIFont? = nil, textColor: UIColor = UIColor.black, paragraphAlignment: NSTextAlignment? = nil) {
|
||||
var attributes: [NSAttributedStringKey: AnyObject] = [:]
|
||||
if let font = font {
|
||||
attributes[NSAttributedStringKey.font] = font
|
||||
}
|
||||
attributes[NSAttributedStringKey.foregroundColor] = textColor
|
||||
if let paragraphAlignment = paragraphAlignment {
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.alignment = paragraphAlignment
|
||||
attributes[NSAttributedStringKey.paragraphStyle] = paragraphStyle
|
||||
}
|
||||
self.init(string: string, attributes: attributes)
|
||||
}
|
||||
}
|
493
submodules/Display/Display/GenerateImage.swift
Normal file
493
submodules/Display/Display/GenerateImage.swift
Normal file
@ -0,0 +1,493 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
let deviceColorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let deviceScale = UIScreen.main.scale
|
||||
|
||||
public func generateImagePixel(_ size: CGSize, pixelGenerator: (CGSize, UnsafeMutablePointer<Int8>) -> Void) -> UIImage? {
|
||||
let scale = deviceScale
|
||||
let scaledSize = CGSize(width: size.width * scale, height: size.height * scale)
|
||||
let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
|
||||
let length = bytesPerRow * Int(scaledSize.height)
|
||||
let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self)
|
||||
guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
|
||||
free(bytes)
|
||||
})
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
pixelGenerator(scaledSize, bytes)
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
|
||||
|
||||
guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIImage(cgImage: image, scale: scale, orientation: .up)
|
||||
}
|
||||
|
||||
public func generateImage(_ size: CGSize, contextGenerator: (CGSize, CGContext) -> Void, opaque: Bool = false, scale: CGFloat? = nil) -> UIImage? {
|
||||
let selectedScale = scale ?? deviceScale
|
||||
let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale)
|
||||
let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
|
||||
let length = bytesPerRow * Int(scaledSize.height)
|
||||
let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self)
|
||||
|
||||
guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
|
||||
free(bytes)
|
||||
})
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue))
|
||||
|
||||
guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
context.scaleBy(x: selectedScale, y: selectedScale)
|
||||
|
||||
contextGenerator(size, context)
|
||||
|
||||
guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIImage(cgImage: image, scale: selectedScale, orientation: .up)
|
||||
}
|
||||
|
||||
public func generateImage(_ size: CGSize, opaque: Bool = false, scale: CGFloat? = nil, rotatedContext: (CGSize, CGContext) -> Void) -> UIImage? {
|
||||
let selectedScale = scale ?? deviceScale
|
||||
let scaledSize = CGSize(width: size.width * selectedScale, height: size.height * selectedScale)
|
||||
let bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
|
||||
let length = bytesPerRow * Int(scaledSize.height)
|
||||
let bytes = malloc(length)!.assumingMemoryBound(to: Int8.self)
|
||||
|
||||
guard let provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
|
||||
free(bytes)
|
||||
})
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | (opaque ? CGImageAlphaInfo.noneSkipFirst.rawValue : CGImageAlphaInfo.premultipliedFirst.rawValue))
|
||||
|
||||
guard let context = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
context.scaleBy(x: selectedScale, y: selectedScale)
|
||||
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
|
||||
|
||||
rotatedContext(size, context)
|
||||
|
||||
guard let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIImage(cgImage: image, scale: selectedScale, orientation: .up)
|
||||
}
|
||||
|
||||
public func generateFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
|
||||
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
if let backgroundColor = backgroundColor {
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
|
||||
if let strokeColor = strokeColor, let strokeWidth = strokeWidth {
|
||||
context.setFillColor(strokeColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
if let color = color {
|
||||
context.setFillColor(color.cgColor)
|
||||
} else {
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
}
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: strokeWidth, y: strokeWidth), size: CGSize(width: size.width - strokeWidth * 2.0, height: size.height - strokeWidth * 2.0)))
|
||||
} else {
|
||||
if let color = color {
|
||||
context.setFillColor(color.cgColor)
|
||||
} else {
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
}
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func generateCircleImage(diameter: CGFloat, lineWidth: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
|
||||
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
if let backgroundColor = backgroundColor {
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
|
||||
if let color = color {
|
||||
context.setStrokeColor(color.cgColor)
|
||||
} else {
|
||||
context.setStrokeColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
}
|
||||
context.setLineWidth(lineWidth)
|
||||
context.strokeEllipse(in: CGRect(origin: CGPoint(x: lineWidth / 2.0, y: lineWidth / 2.0), size: CGSize(width: size.width - lineWidth, height: size.height - lineWidth)))
|
||||
})
|
||||
}
|
||||
|
||||
public func generateStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
|
||||
let intRadius = Int(radius)
|
||||
let cap = intRadius == 1 ? 2 : intRadius
|
||||
return generateFilledCircleImage(diameter: radius * 2.0, color: color, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap)
|
||||
}
|
||||
|
||||
public func generateStretchableFilledCircleImage(diameter: CGFloat, color: UIColor?, strokeColor: UIColor? = nil, strokeWidth: CGFloat? = nil, backgroundColor: UIColor? = nil) -> UIImage? {
|
||||
let intRadius = Int(diameter / 2.0)
|
||||
let intDiameter = Int(diameter)
|
||||
let cap: Int
|
||||
if intDiameter == 3 {
|
||||
cap = 1
|
||||
} else if intRadius == 1 {
|
||||
cap = 2
|
||||
} else {
|
||||
cap = intRadius
|
||||
}
|
||||
|
||||
return generateFilledCircleImage(diameter: diameter, color: color, strokeColor: strokeColor, strokeWidth: strokeWidth, backgroundColor: backgroundColor)?.stretchableImage(withLeftCapWidth: cap, topCapHeight: cap)
|
||||
}
|
||||
|
||||
public func generateVerticallyStretchableFilledCircleImage(radius: CGFloat, color: UIColor?, backgroundColor: UIColor? = nil) -> UIImage? {
|
||||
return generateImage(CGSize(width: radius * 2.0, height: radius * 2.0 + radius), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
if let backgroundColor = backgroundColor {
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
|
||||
if let color = color {
|
||||
context.setFillColor(color.cgColor)
|
||||
} else {
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.setBlendMode(.copy)
|
||||
}
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: radius + radius, height: radius + radius)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: radius), size: CGSize(width: radius + radius, height: radius + radius)))
|
||||
})?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius))
|
||||
}
|
||||
|
||||
public func generateTintedImage(image: UIImage?, color: UIColor, backgroundColor: UIColor? = nil) -> UIImage? {
|
||||
guard let image = image else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let imageSize = image.size
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, backgroundColor != nil, image.scale)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
if let backgroundColor = backgroundColor {
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: imageSize))
|
||||
}
|
||||
|
||||
let imageRect = CGRect(origin: CGPoint(), size: imageSize)
|
||||
context.saveGState()
|
||||
context.translateBy(x: imageRect.midX, y: imageRect.midY)
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: -imageRect.midX, y: -imageRect.midY)
|
||||
context.clip(to: imageRect, mask: image.cgImage!)
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(imageRect)
|
||||
context.restoreGState()
|
||||
}
|
||||
|
||||
let tintedImage = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return tintedImage
|
||||
}
|
||||
|
||||
public func generateScaledImage(image: UIImage?, size: CGSize, opaque: Bool = true, scale: CGFloat? = nil) -> UIImage? {
|
||||
guard let image = image else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return generateImage(size, contextGenerator: { size, context in
|
||||
if !opaque {
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size))
|
||||
}, opaque: opaque, scale: scale)
|
||||
}
|
||||
|
||||
private func generateSingleColorImage(size: CGSize, color: UIColor) -> UIImage? {
|
||||
return generateImage(size, contextGenerator: { size, context in
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
})
|
||||
}
|
||||
|
||||
public enum DrawingContextBltMode {
|
||||
case Alpha
|
||||
}
|
||||
|
||||
public class DrawingContext {
|
||||
public let size: CGSize
|
||||
public let scale: CGFloat
|
||||
private let scaledSize: CGSize
|
||||
public let bytesPerRow: Int
|
||||
private let bitmapInfo: CGBitmapInfo
|
||||
public let length: Int
|
||||
public let bytes: UnsafeMutableRawPointer
|
||||
let provider: CGDataProvider?
|
||||
|
||||
private var _context: CGContext?
|
||||
|
||||
public func withContext(_ f: (CGContext) -> ()) {
|
||||
if self._context == nil {
|
||||
if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) {
|
||||
c.scaleBy(x: scale, y: scale)
|
||||
self._context = c
|
||||
}
|
||||
}
|
||||
|
||||
if let _context = self._context {
|
||||
_context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
|
||||
_context.scaleBy(x: 1.0, y: -1.0)
|
||||
_context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
|
||||
|
||||
f(_context)
|
||||
|
||||
_context.translateBy(x: self.size.width / 2.0, y: self.size.height / 2.0)
|
||||
_context.scaleBy(x: 1.0, y: -1.0)
|
||||
_context.translateBy(x: -self.size.width / 2.0, y: -self.size.height / 2.0)
|
||||
}
|
||||
}
|
||||
|
||||
public func withFlippedContext(_ f: (CGContext) -> ()) {
|
||||
if self._context == nil {
|
||||
if let c = CGContext(data: bytes, width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: self.bitmapInfo.rawValue) {
|
||||
c.scaleBy(x: scale, y: scale)
|
||||
self._context = c
|
||||
}
|
||||
}
|
||||
|
||||
if let _context = self._context {
|
||||
f(_context)
|
||||
}
|
||||
}
|
||||
|
||||
public init(size: CGSize, scale: CGFloat = 0.0, clear: Bool = false) {
|
||||
let actualScale: CGFloat
|
||||
if scale.isZero {
|
||||
actualScale = deviceScale
|
||||
} else {
|
||||
actualScale = scale
|
||||
}
|
||||
self.size = size
|
||||
self.scale = actualScale
|
||||
self.scaledSize = CGSize(width: size.width * actualScale, height: size.height * actualScale)
|
||||
|
||||
self.bytesPerRow = (4 * Int(scaledSize.width) + 15) & (~15)
|
||||
self.length = bytesPerRow * Int(scaledSize.height)
|
||||
|
||||
self.bitmapInfo = CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
|
||||
|
||||
self.bytes = malloc(length)!
|
||||
if clear {
|
||||
memset(self.bytes, 0, self.length)
|
||||
}
|
||||
self.provider = CGDataProvider(dataInfo: bytes, data: bytes, size: length, releaseData: { bytes, _, _ in
|
||||
free(bytes)
|
||||
})
|
||||
|
||||
assert(self.bytesPerRow % 16 == 0)
|
||||
assert(Int64(Int(bitPattern: self.bytes)) % 16 == 0)
|
||||
}
|
||||
|
||||
public func generateImage() -> UIImage? {
|
||||
if self.scaledSize.width.isZero || self.scaledSize.height.isZero {
|
||||
return nil
|
||||
}
|
||||
if let image = CGImage(width: Int(scaledSize.width), height: Int(scaledSize.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: bytesPerRow, space: deviceColorSpace, bitmapInfo: bitmapInfo, provider: provider!, decode: nil, shouldInterpolate: false, intent: .defaultIntent) {
|
||||
return UIImage(cgImage: image, scale: scale, orientation: .up)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func colorAt(_ point: CGPoint) -> UIColor {
|
||||
let x = Int(point.x * self.scale)
|
||||
let y = Int(point.y * self.scale)
|
||||
if x >= 0 && x < Int(self.scaledSize.width) && y >= 0 && y < Int(self.scaledSize.height) {
|
||||
let srcLine = self.bytes.advanced(by: y * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
let pixel = srcLine + x
|
||||
let colorValue = pixel.pointee
|
||||
return UIColor(rgb: UInt32(colorValue))
|
||||
} else {
|
||||
return UIColor.clear
|
||||
}
|
||||
}
|
||||
|
||||
public func blt(_ other: DrawingContext, at: CGPoint, mode: DrawingContextBltMode = .Alpha) {
|
||||
if abs(other.scale - self.scale) < CGFloat.ulpOfOne {
|
||||
let srcX = 0
|
||||
var srcY = 0
|
||||
let dstX = Int(at.x * self.scale)
|
||||
var dstY = Int(at.y * self.scale)
|
||||
if dstX < 0 || dstY < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
let width = min(Int(self.size.width * self.scale) - dstX, Int(other.size.width * self.scale))
|
||||
let height = min(Int(self.size.height * self.scale) - dstY, Int(other.size.height * self.scale))
|
||||
|
||||
let maxDstX = dstX + width
|
||||
let maxDstY = dstY + height
|
||||
|
||||
switch mode {
|
||||
case .Alpha:
|
||||
while dstY < maxDstY {
|
||||
let srcLine = other.bytes.advanced(by: max(0, srcY) * other.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
let dstLine = self.bytes.advanced(by: max(0, dstY) * self.bytesPerRow).assumingMemoryBound(to: UInt32.self)
|
||||
|
||||
var dx = dstX
|
||||
var sx = srcX
|
||||
while dx < maxDstX {
|
||||
let srcPixel = srcLine + sx
|
||||
let dstPixel = dstLine + dx
|
||||
|
||||
let baseColor = dstPixel.pointee
|
||||
let baseAlpha = (baseColor >> 24) & 0xff
|
||||
let baseR = (baseColor >> 16) & 0xff
|
||||
let baseG = (baseColor >> 8) & 0xff
|
||||
let baseB = baseColor & 0xff
|
||||
|
||||
let alpha = min(baseAlpha, srcPixel.pointee >> 24)
|
||||
|
||||
let r = (baseR * alpha) / 255
|
||||
let g = (baseG * alpha) / 255
|
||||
let b = (baseB * alpha) / 255
|
||||
|
||||
dstPixel.pointee = (alpha << 24) | (r << 16) | (g << 8) | b
|
||||
|
||||
dx += 1
|
||||
sx += 1
|
||||
}
|
||||
|
||||
dstY += 1
|
||||
srcY += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ParsingError: Error {
|
||||
case Generic
|
||||
}
|
||||
|
||||
public func readCGFloat(_ index: inout UnsafePointer<UInt8>, end: UnsafePointer<UInt8>, separator: UInt8) throws -> CGFloat {
|
||||
let begin = index
|
||||
var seenPoint = false
|
||||
while index <= end {
|
||||
let c = index.pointee
|
||||
index = index.successor()
|
||||
|
||||
if c == 46 { // .
|
||||
if seenPoint {
|
||||
throw ParsingError.Generic
|
||||
} else {
|
||||
seenPoint = true
|
||||
}
|
||||
} else if c == separator {
|
||||
break
|
||||
} else if !((c >= 48 && c <= 57) || c == 45 || c == 101 || c == 69) {
|
||||
throw ParsingError.Generic
|
||||
}
|
||||
}
|
||||
|
||||
if index == begin {
|
||||
throw ParsingError.Generic
|
||||
}
|
||||
|
||||
if let value = NSString(bytes: UnsafeRawPointer(begin), length: index - begin, encoding: String.Encoding.utf8.rawValue)?.floatValue {
|
||||
return CGFloat(value)
|
||||
} else {
|
||||
throw ParsingError.Generic
|
||||
}
|
||||
}
|
||||
|
||||
public func drawSvgPath(_ context: CGContext, path: StaticString, strokeOnMove: Bool = false) throws {
|
||||
var index: UnsafePointer<UInt8> = path.utf8Start
|
||||
let end = path.utf8Start.advanced(by: path.utf8CodeUnitCount)
|
||||
while index < end {
|
||||
let c = index.pointee
|
||||
index = index.successor()
|
||||
|
||||
if c == 77 { // M
|
||||
let x = try readCGFloat(&index, end: end, separator: 44)
|
||||
let y = try readCGFloat(&index, end: end, separator: 32)
|
||||
|
||||
//print("Move to \(x), \(y)")
|
||||
context.move(to: CGPoint(x: x, y: y))
|
||||
} else if c == 76 { // L
|
||||
let x = try readCGFloat(&index, end: end, separator: 44)
|
||||
let y = try readCGFloat(&index, end: end, separator: 32)
|
||||
|
||||
//print("Line to \(x), \(y)")
|
||||
context.addLine(to: CGPoint(x: x, y: y))
|
||||
|
||||
if strokeOnMove {
|
||||
context.strokePath()
|
||||
context.move(to: CGPoint(x: x, y: y))
|
||||
}
|
||||
} else if c == 67 { // C
|
||||
let x1 = try readCGFloat(&index, end: end, separator: 44)
|
||||
let y1 = try readCGFloat(&index, end: end, separator: 32)
|
||||
let x2 = try readCGFloat(&index, end: end, separator: 44)
|
||||
let y2 = try readCGFloat(&index, end: end, separator: 32)
|
||||
let x = try readCGFloat(&index, end: end, separator: 44)
|
||||
let y = try readCGFloat(&index, end: end, separator: 32)
|
||||
context.addCurve(to: CGPoint(x: x, y: y), control1: CGPoint(x: x1, y: y1), control2: CGPoint(x: x2, y: y2))
|
||||
|
||||
//print("Line to \(x), \(y)")
|
||||
if strokeOnMove {
|
||||
context.strokePath()
|
||||
context.move(to: CGPoint(x: x, y: y))
|
||||
}
|
||||
} else if c == 90 { // Z
|
||||
if index != end && index.pointee != 32 {
|
||||
throw ParsingError.Generic
|
||||
}
|
||||
|
||||
//CGContextClosePath(context)
|
||||
context.fillPath()
|
||||
//CGContextBeginPath(context)
|
||||
//print("Close")
|
||||
} else if c == 83 { // S
|
||||
if index != end && index.pointee != 32 {
|
||||
throw ParsingError.Generic
|
||||
}
|
||||
|
||||
//CGContextClosePath(context)
|
||||
context.strokePath()
|
||||
//CGContextBeginPath(context)
|
||||
//print("Close")
|
||||
} else if c == 32 { // space
|
||||
continue
|
||||
} else {
|
||||
throw ParsingError.Generic
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
private func isViewVisibleInHierarchy(_ view: UIView, _ initial: Bool = true) -> Bool {
|
||||
guard let window = view.window else {
|
||||
return false
|
||||
}
|
||||
if view.isHidden || view.alpha == 0.0 {
|
||||
return false
|
||||
}
|
||||
if view.superview === window {
|
||||
return true
|
||||
} else if let superview = view.superview {
|
||||
if initial && view.frame.minY >= superview.frame.height {
|
||||
return false
|
||||
} else {
|
||||
return isViewVisibleInHierarchy(superview, false)
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
final class GlobalOverlayPresentationContext {
|
||||
private let statusBarHost: StatusBarHost?
|
||||
|
||||
private var controllers: [ContainableController] = []
|
||||
|
||||
private var presentationDisposables = DisposableSet()
|
||||
private var layout: ContainerViewLayout?
|
||||
|
||||
private var ready: Bool {
|
||||
return self.currentPresentationView() != nil && self.layout != nil
|
||||
}
|
||||
|
||||
init(statusBarHost: StatusBarHost?) {
|
||||
self.statusBarHost = statusBarHost
|
||||
}
|
||||
|
||||
private func currentPresentationView() -> UIView? {
|
||||
if let statusBarHost = self.statusBarHost {
|
||||
if let keyboardWindow = statusBarHost.keyboardWindow, let keyboardView = statusBarHost.keyboardView, !keyboardView.frame.height.isZero, isViewVisibleInHierarchy(keyboardView) {
|
||||
return keyboardWindow
|
||||
} else {
|
||||
return statusBarHost.statusBarWindow
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func present(_ controller: ContainableController) {
|
||||
let controllerReady = controller.ready.get()
|
||||
|> filter({ $0 })
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> timeout(2.0, queue: Queue.mainQueue(), alternate: .single(true))
|
||||
|
||||
if let _ = self.currentPresentationView(), let initialLayout = self.layout {
|
||||
controller.view.frame = CGRect(origin: CGPoint(), size: initialLayout.size)
|
||||
controller.containerLayoutUpdated(initialLayout, transition: .immediate)
|
||||
|
||||
self.presentationDisposables.add(controllerReady.start(next: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.controllers.contains(where: { $0 === controller }) {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.controllers.append(controller)
|
||||
if let view = strongSelf.currentPresentationView(), let layout = strongSelf.layout {
|
||||
(controller as? UIViewController)?.navigation_setDismiss({ [weak controller] in
|
||||
if let strongSelf = self, let controller = controller {
|
||||
strongSelf.dismiss(controller)
|
||||
}
|
||||
}, rootController: nil)
|
||||
(controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(true)
|
||||
if layout != initialLayout {
|
||||
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
view.addSubview(controller.view)
|
||||
controller.containerLayoutUpdated(layout, transition: .immediate)
|
||||
} else {
|
||||
view.addSubview(controller.view)
|
||||
}
|
||||
(controller as? UIViewController)?.setIgnoreAppearanceMethodInvocations(false)
|
||||
view.layer.invalidateUpTheTree()
|
||||
controller.viewWillAppear(false)
|
||||
controller.viewDidAppear(false)
|
||||
}
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
self.controllers.append(controller)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDisposables.dispose()
|
||||
}
|
||||
|
||||
private func dismiss(_ controller: ContainableController) {
|
||||
if let index = self.controllers.index(where: { $0 === controller }) {
|
||||
self.controllers.remove(at: index)
|
||||
controller.viewWillDisappear(false)
|
||||
controller.view.removeFromSuperview()
|
||||
controller.viewDidDisappear(false)
|
||||
}
|
||||
}
|
||||
|
||||
public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
let wasReady = self.ready
|
||||
self.layout = layout
|
||||
|
||||
if wasReady != self.ready {
|
||||
self.readyChanged(wasReady: wasReady)
|
||||
} else if self.ready {
|
||||
for controller in self.controllers {
|
||||
controller.containerLayoutUpdated(layout, transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func readyChanged(wasReady: Bool) {
|
||||
if !wasReady {
|
||||
self.addViews()
|
||||
} else {
|
||||
self.removeViews()
|
||||
}
|
||||
}
|
||||
|
||||
private func addViews() {
|
||||
if let view = self.currentPresentationView(), let layout = self.layout {
|
||||
for controller in self.controllers {
|
||||
controller.viewWillAppear(false)
|
||||
view.addSubview(controller.view)
|
||||
controller.view.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
controller.containerLayoutUpdated(layout, transition: .immediate)
|
||||
controller.viewDidAppear(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeViews() {
|
||||
for controller in self.controllers {
|
||||
controller.viewWillDisappear(false)
|
||||
controller.view.removeFromSuperview()
|
||||
controller.viewDidDisappear(false)
|
||||
}
|
||||
}
|
||||
|
||||
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
for controller in self.controllers.reversed() {
|
||||
if controller.isViewLoaded {
|
||||
if let result = controller.view.hitTest(point, with: event) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) {
|
||||
if self.ready {
|
||||
for controller in self.controllers {
|
||||
controller.updateToInterfaceOrientation(orientation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
|
||||
var mask = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .all)
|
||||
|
||||
for controller in self.controllers {
|
||||
mask = mask.intersection(controller.combinedSupportedOrientations(currentOrientationToLock: currentOrientationToLock))
|
||||
}
|
||||
|
||||
return mask
|
||||
}
|
||||
|
||||
func combinedDeferScreenEdgeGestures() -> UIRectEdge {
|
||||
var edges: UIRectEdge = []
|
||||
|
||||
for controller in self.controllers {
|
||||
edges = edges.union(controller.deferScreenEdgeGestures)
|
||||
}
|
||||
|
||||
return edges
|
||||
}
|
||||
}
|
34
submodules/Display/Display/GridItem.swift
Normal file
34
submodules/Display/Display/GridItem.swift
Normal file
@ -0,0 +1,34 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public protocol GridSection {
|
||||
var height: CGFloat { get }
|
||||
var hashValue: Int { get }
|
||||
|
||||
func isEqual(to: GridSection) -> Bool
|
||||
func node() -> ASDisplayNode
|
||||
}
|
||||
|
||||
public protocol GridItem {
|
||||
var section: GridSection? { get }
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode
|
||||
func update(node: GridItemNode)
|
||||
var aspectRatio: CGFloat { get }
|
||||
var fillsRowWithHeight: CGFloat? { get }
|
||||
var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? { get }
|
||||
}
|
||||
|
||||
public extension GridItem {
|
||||
var aspectRatio: CGFloat {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
var fillsRowWithHeight: CGFloat? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var fillsRowWithDynamicHeight: ((CGFloat) -> CGFloat)? {
|
||||
return nil
|
||||
}
|
||||
}
|
21
submodules/Display/Display/GridItemNode.swift
Normal file
21
submodules/Display/Display/GridItemNode.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class GridItemNode: ASDisplayNode {
|
||||
open var isVisibleInGrid = false
|
||||
open var isGridScrolling = false
|
||||
|
||||
final var cachedFrame: CGRect = CGRect()
|
||||
override open var frame: CGRect {
|
||||
get {
|
||||
return self.cachedFrame
|
||||
} set(value) {
|
||||
self.cachedFrame = value
|
||||
super.frame = value
|
||||
}
|
||||
}
|
||||
|
||||
open func updateLayout(item: GridItem, size: CGSize, isVisible: Bool, synchronousLoads: Bool) {
|
||||
}
|
||||
}
|
1348
submodules/Display/Display/GridNode.swift
Normal file
1348
submodules/Display/Display/GridNode.swift
Normal file
File diff suppressed because it is too large
Load Diff
53
submodules/Display/Display/GridNodeScroller.swift
Normal file
53
submodules/Display/Display/GridNodeScroller.swift
Normal file
@ -0,0 +1,53 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
private class GridNodeScrollerLayer: CALayer {
|
||||
override func setNeedsDisplay() {
|
||||
}
|
||||
}
|
||||
|
||||
private class GridNodeScrollerView: UIScrollView {
|
||||
override class var layerClass: AnyClass {
|
||||
return GridNodeScrollerLayer.self
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
open class GridNodeScroller: ASDisplayNode, UIGestureRecognizerDelegate {
|
||||
public var scrollView: UIScrollView {
|
||||
return self.view as! UIScrollView
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return GridNodeScrollerView(frame: CGRect())
|
||||
})
|
||||
|
||||
self.scrollView.scrollsToTop = false
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
132
submodules/Display/Display/HapticFeedback.swift
Normal file
132
submodules/Display/Display/HapticFeedback.swift
Normal file
@ -0,0 +1,132 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public enum ImpactHapticFeedbackStyle: Hashable {
|
||||
case light
|
||||
case medium
|
||||
case heavy
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
|
||||
private final class HapticFeedbackImpl {
|
||||
private lazy var impactGenerator: [ImpactHapticFeedbackStyle : UIImpactFeedbackGenerator] = {
|
||||
[.light: UIImpactFeedbackGenerator(style: .light),
|
||||
.medium: UIImpactFeedbackGenerator(style: .medium),
|
||||
.heavy: UIImpactFeedbackGenerator(style: .heavy)] }()
|
||||
private lazy var selectionGenerator = { UISelectionFeedbackGenerator() }()
|
||||
private lazy var notificationGenerator = { UINotificationFeedbackGenerator() }()
|
||||
|
||||
func prepareTap() {
|
||||
self.selectionGenerator.prepare()
|
||||
}
|
||||
|
||||
func tap() {
|
||||
self.selectionGenerator.selectionChanged()
|
||||
}
|
||||
|
||||
func prepareImpact(_ style: ImpactHapticFeedbackStyle) {
|
||||
self.impactGenerator[style]?.prepare()
|
||||
}
|
||||
|
||||
func impact(_ style: ImpactHapticFeedbackStyle) {
|
||||
self.impactGenerator[style]?.impactOccurred()
|
||||
}
|
||||
|
||||
func success() {
|
||||
self.notificationGenerator.notificationOccurred(.success)
|
||||
}
|
||||
|
||||
func prepareError() {
|
||||
self.notificationGenerator.prepare()
|
||||
}
|
||||
|
||||
func error() {
|
||||
self.notificationGenerator.notificationOccurred(.error)
|
||||
}
|
||||
|
||||
@objc dynamic func f() {
|
||||
}
|
||||
}
|
||||
|
||||
public final class HapticFeedback {
|
||||
private var impl: AnyObject?
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
deinit {
|
||||
let impl = self.impl
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0, execute: {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
if let impl = impl as? HapticFeedbackImpl {
|
||||
impl.f()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 10.0, iOS 10.0, *)
|
||||
private func withImpl(_ f: (HapticFeedbackImpl) -> Void) {
|
||||
if self.impl == nil {
|
||||
self.impl = HapticFeedbackImpl()
|
||||
}
|
||||
f(self.impl as! HapticFeedbackImpl)
|
||||
}
|
||||
|
||||
public func prepareTap() {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.withImpl { impl in
|
||||
impl.prepareTap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func tap() {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.withImpl { impl in
|
||||
impl.tap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareImpact(_ style: ImpactHapticFeedbackStyle = .medium) {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.withImpl { impl in
|
||||
impl.prepareImpact(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func impact(_ style: ImpactHapticFeedbackStyle = .medium) {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.withImpl { impl in
|
||||
impl.impact(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func success() {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.withImpl { impl in
|
||||
impl.success()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func prepareError() {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.withImpl { impl in
|
||||
impl.prepareError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func error() {
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
self.withImpl { impl in
|
||||
impl.error()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
submodules/Display/Display/HighlightTrackingButton.swift
Normal file
48
submodules/Display/Display/HighlightTrackingButton.swift
Normal file
@ -0,0 +1,48 @@
|
||||
import UIKit
|
||||
|
||||
open class HighlightTrackingButton: UIButton {
|
||||
private var internalHighlighted = false
|
||||
|
||||
public var internalHighligthedChanged: (Bool) -> Void = { _ in }
|
||||
public var highligthedChanged: (Bool) -> Void = { _ in }
|
||||
|
||||
open override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
if !self.internalHighlighted {
|
||||
self.internalHighlighted = true
|
||||
self.highligthedChanged(true)
|
||||
self.internalHighligthedChanged(true)
|
||||
}
|
||||
|
||||
return super.beginTracking(touch, with: event)
|
||||
}
|
||||
|
||||
open override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
if self.internalHighlighted {
|
||||
self.internalHighlighted = false
|
||||
self.highligthedChanged(false)
|
||||
self.internalHighligthedChanged(false)
|
||||
}
|
||||
|
||||
super.endTracking(touch, with: event)
|
||||
}
|
||||
|
||||
open override func cancelTracking(with event: UIEvent?) {
|
||||
if self.internalHighlighted {
|
||||
self.internalHighlighted = false
|
||||
self.highligthedChanged(false)
|
||||
self.internalHighligthedChanged(false)
|
||||
}
|
||||
|
||||
super.cancelTracking(with: event)
|
||||
}
|
||||
|
||||
open override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
if self.internalHighlighted {
|
||||
self.internalHighlighted = false
|
||||
self.highligthedChanged(false)
|
||||
self.internalHighligthedChanged(false)
|
||||
}
|
||||
|
||||
super.touchesCancelled(touches, with: event)
|
||||
}
|
||||
}
|
87
submodules/Display/Display/HighlightableButton.swift
Normal file
87
submodules/Display/Display/HighlightableButton.swift
Normal file
@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class HighlightableButton: HighlightTrackingButton {
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.adjustsImageWhenHighlighted = false
|
||||
self.adjustsImageWhenDisabled = false
|
||||
self.internalHighligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.alpha = 1.0
|
||||
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
open class HighlightTrackingButtonNode: ASButtonNode {
|
||||
private var internalHighlighted = false
|
||||
|
||||
public var highligthedChanged: (Bool) -> Void = { _ in }
|
||||
|
||||
open override func beginTracking(with touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
if !self.internalHighlighted {
|
||||
self.internalHighlighted = true
|
||||
self.highligthedChanged(true)
|
||||
}
|
||||
|
||||
return super.beginTracking(with: touch, with: event)
|
||||
}
|
||||
|
||||
open override func endTracking(with touch: UITouch?, with event: UIEvent?) {
|
||||
if self.internalHighlighted {
|
||||
self.internalHighlighted = false
|
||||
self.highligthedChanged(false)
|
||||
}
|
||||
|
||||
super.endTracking(with: touch, with: event)
|
||||
}
|
||||
|
||||
open override func cancelTracking(with event: UIEvent?) {
|
||||
if self.internalHighlighted {
|
||||
self.internalHighlighted = false
|
||||
self.highligthedChanged(false)
|
||||
}
|
||||
|
||||
super.cancelTracking(with: event)
|
||||
}
|
||||
|
||||
open override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
if self.internalHighlighted {
|
||||
self.internalHighlighted = false
|
||||
self.highligthedChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class HighlightableButtonNode: HighlightTrackingButtonNode {
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.layer.removeAnimation(forKey: "opacity")
|
||||
strongSelf.alpha = 0.4
|
||||
} else {
|
||||
strongSelf.alpha = 1.0
|
||||
strongSelf.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
135
submodules/Display/Display/ImmediateTextNode.swift
Normal file
135
submodules/Display/Display/ImmediateTextNode.swift
Normal file
@ -0,0 +1,135 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public struct ImmediateTextNodeLayoutInfo {
|
||||
public let size: CGSize
|
||||
public let truncated: Bool
|
||||
}
|
||||
|
||||
public class ImmediateTextNode: TextNode {
|
||||
public var attributedText: NSAttributedString?
|
||||
public var textAlignment: NSTextAlignment = .natural
|
||||
public var truncationType: CTLineTruncationType = .end
|
||||
public var maximumNumberOfLines: Int = 1
|
||||
public var lineSpacing: CGFloat = 0.0
|
||||
public var insets: UIEdgeInsets = UIEdgeInsets()
|
||||
|
||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
public var linkHighlightColor: UIColor?
|
||||
|
||||
public var trailingLineWidth: CGFloat?
|
||||
|
||||
public var highlightAttributeAction: (([NSAttributedStringKey: Any]) -> NSAttributedStringKey?)? {
|
||||
didSet {
|
||||
if self.isNodeLoaded {
|
||||
self.updateInteractiveActions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var tapAttributeAction: (([NSAttributedStringKey: Any]) -> Void)?
|
||||
public var longTapAttributeAction: (([NSAttributedStringKey: Any]) -> Void)?
|
||||
|
||||
public func updateLayout(_ constrainedSize: CGSize) -> CGSize {
|
||||
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()
|
||||
if layout.numberOfLines > 1 {
|
||||
self.trailingLineWidth = layout.trailingLineWidth
|
||||
} else {
|
||||
self.trailingLineWidth = nil
|
||||
}
|
||||
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()
|
||||
|
||||
self.updateInteractiveActions()
|
||||
}
|
||||
|
||||
private func updateInteractiveActions() {
|
||||
if self.highlightAttributeAction != nil {
|
||||
if self.tapRecognizer == nil {
|
||||
let tapRecognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapAction(_:)))
|
||||
tapRecognizer.highlight = { [weak self] point in
|
||||
if let strongSelf = self {
|
||||
var rects: [CGRect]?
|
||||
if let point = point {
|
||||
if let (index, attributes) = strongSelf.attributesAtPoint(CGPoint(x: point.x, y: point.y)) {
|
||||
if let selectedAttribute = strongSelf.highlightAttributeAction?(attributes) {
|
||||
let initialRects = strongSelf.lineAndAttributeRects(name: selectedAttribute.rawValue, at: index)
|
||||
if let initialRects = initialRects, case .center = strongSelf.textAlignment {
|
||||
var mappedRects: [CGRect] = []
|
||||
for i in 0 ..< initialRects.count {
|
||||
let lineRect = initialRects[i].0
|
||||
var itemRect = initialRects[i].1
|
||||
itemRect.origin.x = floor((strongSelf.bounds.size.width - lineRect.width) / 2.0) + itemRect.origin.x
|
||||
mappedRects.append(itemRect)
|
||||
}
|
||||
rects = mappedRects
|
||||
} else {
|
||||
rects = strongSelf.attributeRects(name: selectedAttribute.rawValue, at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rects = rects {
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = strongSelf.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: strongSelf.linkHighlightColor ?? .clear)
|
||||
strongSelf.linkHighlightingNode = linkHighlightingNode
|
||||
strongSelf.addSubnode(linkHighlightingNode)
|
||||
}
|
||||
linkHighlightingNode.frame = strongSelf.bounds
|
||||
linkHighlightingNode.updateRects(rects.map { $0.offsetBy(dx: 0.0, dy: 0.0) })
|
||||
} else if let linkHighlightingNode = strongSelf.linkHighlightingNode {
|
||||
strongSelf.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
self.view.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
} else if let tapRecognizer = self.tapRecognizer {
|
||||
self.tapRecognizer = nil
|
||||
self.view.removeGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func tapAction(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .ended:
|
||||
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
|
||||
switch gesture {
|
||||
case .tap:
|
||||
if let (_, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
|
||||
self.tapAttributeAction?(attributes)
|
||||
}
|
||||
case .longTap:
|
||||
if let (_, attributes) = self.attributesAtPoint(CGPoint(x: location.x, y: location.y)) {
|
||||
self.longTapAttributeAction?(attributes)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
26
submodules/Display/Display/Info.plist
Normal file
26
submodules/Display/Display/Info.plist
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private func hasHorizontalGestures(_ view: UIView, point: CGPoint?) -> Bool {
|
||||
if view.disablesInteractiveTransitionGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
|
||||
if let point = point, let test = view.interactiveTransitionGestureRecognizerTest, test(point) {
|
||||
return true
|
||||
}
|
||||
|
||||
if let view = view as? ListViewBackingView {
|
||||
let transform = view.transform
|
||||
let angle: Double = Double(atan2f(Float(transform.b), Float(transform.a)))
|
||||
let term1: Double = abs(angle - Double.pi / 2.0)
|
||||
let term2: Double = abs(angle + Double.pi / 2.0)
|
||||
let term3: Double = abs(angle - Double.pi * 3.0 / 2.0)
|
||||
if term1 < 0.001 || term2 < 0.001 || term3 < 0.001 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if let superview = view.superview {
|
||||
return hasHorizontalGestures(superview, point: point != nil ? view.convert(point!, to: superview) : nil)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class InteractiveTransitionGestureRecognizer: UIPanGestureRecognizer {
|
||||
var validatedGesture = false
|
||||
var firstLocation: CGPoint = CGPoint()
|
||||
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.maximumNumberOfTouches = 1
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
validatedGesture = false
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
let touch = touches.first!
|
||||
self.firstLocation = touch.location(in: self.view)
|
||||
|
||||
if let target = self.view?.hitTest(self.firstLocation, with: event) {
|
||||
if hasHorizontalGestures(target, point: self.view?.convert(self.firstLocation, to: target)) {
|
||||
self.state = .cancelled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
let location = touches.first!.location(in: self.view)
|
||||
let translation = CGPoint(x: location.x - firstLocation.x, y: location.y - firstLocation.y)
|
||||
|
||||
let absTranslationX: CGFloat = abs(translation.x)
|
||||
let absTranslationY: CGFloat = abs(translation.y)
|
||||
|
||||
if !validatedGesture {
|
||||
if self.firstLocation.x < 16.0 {
|
||||
validatedGesture = true
|
||||
} else if translation.x < 0.0 {
|
||||
self.state = .failed
|
||||
} else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 {
|
||||
self.state = .failed
|
||||
} else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX {
|
||||
validatedGesture = true
|
||||
}
|
||||
}
|
||||
|
||||
if validatedGesture {
|
||||
super.touchesMoved(touches, with: event)
|
||||
}
|
||||
}
|
||||
}
|
43
submodules/Display/Display/KeyShortcut.swift
Normal file
43
submodules/Display/Display/KeyShortcut.swift
Normal file
@ -0,0 +1,43 @@
|
||||
import UIKit
|
||||
|
||||
public struct KeyShortcut: Hashable {
|
||||
let title: String
|
||||
let input: String
|
||||
let modifiers: UIKeyModifierFlags
|
||||
let action: () -> Void
|
||||
|
||||
public init(title: String = "", input: String = "", modifiers: UIKeyModifierFlags = [], action: @escaping () -> Void = {}) {
|
||||
self.title = title
|
||||
self.input = input
|
||||
self.modifiers = modifiers
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public var hashValue: Int {
|
||||
return input.hashValue ^ modifiers.hashValue
|
||||
}
|
||||
|
||||
public static func ==(lhs: KeyShortcut, rhs: KeyShortcut) -> Bool {
|
||||
return lhs.hashValue == rhs.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
extension UIKeyModifierFlags: Hashable {
|
||||
public var hashValue: Int {
|
||||
return self.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension KeyShortcut {
|
||||
var uiKeyCommand: UIKeyCommand {
|
||||
if #available(iOSApplicationExtension 9.0, *), !self.title.isEmpty {
|
||||
return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)), discoverabilityTitle: self.title)
|
||||
} else {
|
||||
return UIKeyCommand(input: self.input, modifierFlags: self.modifiers, action: #selector(KeyShortcutsController.handleKeyCommand(_:)))
|
||||
}
|
||||
}
|
||||
|
||||
func isEqual(to command: UIKeyCommand) -> Bool {
|
||||
return self.input == command.input && self.modifiers == command.modifierFlags
|
||||
}
|
||||
}
|
84
submodules/Display/Display/KeyShortcutsController.swift
Normal file
84
submodules/Display/Display/KeyShortcutsController.swift
Normal file
@ -0,0 +1,84 @@
|
||||
import UIKit
|
||||
|
||||
public protocol KeyShortcutResponder {
|
||||
var keyShortcuts: [KeyShortcut] { get };
|
||||
}
|
||||
|
||||
public class KeyShortcutsController: UIResponder {
|
||||
private var effectiveShortcuts: [KeyShortcut]?
|
||||
private var viewControllerEnumerator: ((ContainableController) -> Bool) -> Void
|
||||
|
||||
public static var isAvailable: Bool {
|
||||
if #available(iOSApplicationExtension 8.0, *), UIDevice.current.userInterfaceIdiom == .pad {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public init(enumerator: @escaping ((ContainableController) -> Bool) -> Void) {
|
||||
self.viewControllerEnumerator = enumerator
|
||||
super.init()
|
||||
}
|
||||
|
||||
public override var keyCommands: [UIKeyCommand]? {
|
||||
var convertedCommands: [UIKeyCommand] = []
|
||||
var shortcuts: [KeyShortcut] = []
|
||||
|
||||
self.viewControllerEnumerator({ viewController -> Bool in
|
||||
guard let viewController = viewController as? KeyShortcutResponder else {
|
||||
return true
|
||||
}
|
||||
shortcuts.removeAll(where: { viewController.keyShortcuts.contains($0) })
|
||||
shortcuts.append(contentsOf: viewController.keyShortcuts)
|
||||
return true
|
||||
})
|
||||
|
||||
// iOS 8 fix
|
||||
convertedCommands.append(KeyShortcut(modifiers:[.command]).uiKeyCommand)
|
||||
convertedCommands.append(KeyShortcut(modifiers:[.alternate]).uiKeyCommand)
|
||||
|
||||
convertedCommands.append(contentsOf: shortcuts.map { $0.uiKeyCommand })
|
||||
|
||||
self.effectiveShortcuts = shortcuts
|
||||
|
||||
return convertedCommands
|
||||
}
|
||||
|
||||
@objc func handleKeyCommand(_ command: UIKeyCommand) {
|
||||
if let shortcut = findShortcut(for: command) {
|
||||
shortcut.action()
|
||||
}
|
||||
}
|
||||
|
||||
private func findShortcut(for command: UIKeyCommand) -> KeyShortcut? {
|
||||
if let shortcuts = self.effectiveShortcuts {
|
||||
for shortcut in shortcuts {
|
||||
if shortcut.isEqual(to: command) {
|
||||
return shortcut
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) {
|
||||
return true
|
||||
} else {
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
public override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
||||
if let keyCommand = sender as? UIKeyCommand, let _ = findShortcut(for: keyCommand) {
|
||||
return self
|
||||
} else {
|
||||
return super.target(forAction: action, withSender: sender)
|
||||
}
|
||||
}
|
||||
|
||||
public override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
11
submodules/Display/Display/Keyboard.swift
Normal file
11
submodules/Display/Display/Keyboard.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
#if BUCK
|
||||
import DisplayPrivate
|
||||
#endif
|
||||
|
||||
public enum Keyboard {
|
||||
public static func applyAutocorrection() {
|
||||
applyKeyboardAutocorrection()
|
||||
}
|
||||
}
|
127
submodules/Display/Display/KeyboardManager.swift
Normal file
127
submodules/Display/Display/KeyboardManager.swift
Normal file
@ -0,0 +1,127 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if BUCK
|
||||
import DisplayPrivate
|
||||
#endif
|
||||
|
||||
struct KeyboardSurface {
|
||||
let host: UIView
|
||||
}
|
||||
|
||||
private func getFirstResponder(_ view: UIView) -> UIView? {
|
||||
if view.isFirstResponder {
|
||||
return view
|
||||
} else {
|
||||
for subview in view.subviews {
|
||||
if let result = getFirstResponder(subview) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
class KeyboardManager {
|
||||
private let host: StatusBarHost
|
||||
|
||||
private weak var previousPositionAnimationMirrorSource: CATracingLayer?
|
||||
private weak var previousFirstResponderView: UIView?
|
||||
private var interactiveInputOffset: CGFloat = 0.0
|
||||
|
||||
var surfaces: [KeyboardSurface] = [] {
|
||||
didSet {
|
||||
self.updateSurfaces(oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
init(host: StatusBarHost) {
|
||||
self.host = host
|
||||
}
|
||||
|
||||
func getCurrentKeyboardHeight() -> CGFloat {
|
||||
guard let keyboardView = self.host.keyboardView else {
|
||||
return 0.0
|
||||
}
|
||||
return keyboardView.bounds.height
|
||||
}
|
||||
|
||||
func updateInteractiveInputOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
guard let keyboardView = self.host.keyboardView else {
|
||||
return
|
||||
}
|
||||
|
||||
self.interactiveInputOffset = offset
|
||||
|
||||
let previousBounds = keyboardView.bounds
|
||||
let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: -offset), size: previousBounds.size)
|
||||
keyboardView.layer.bounds = updatedBounds
|
||||
if transition.isAnimated {
|
||||
transition.animateOffsetAdditive(layer: keyboardView.layer, offset: previousBounds.minY - updatedBounds.minY, completion: completion)
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
|
||||
//transition.updateSublayerTransformOffset(layer: keyboardView.layer, offset: CGPoint(x: 0.0, y: offset))
|
||||
}
|
||||
|
||||
private func updateSurfaces(_ previousSurfaces: [KeyboardSurface]) {
|
||||
guard let keyboardWindow = self.host.keyboardWindow else {
|
||||
return
|
||||
}
|
||||
|
||||
var firstResponderView: UIView?
|
||||
var firstResponderDisableAutomaticKeyboardHandling: UIResponderDisableAutomaticKeyboardHandling = []
|
||||
for surface in self.surfaces {
|
||||
if let view = getFirstResponder(surface.host) {
|
||||
firstResponderView = surface.host
|
||||
firstResponderDisableAutomaticKeyboardHandling = view.disableAutomaticKeyboardHandling
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let firstResponderView = firstResponderView {
|
||||
let containerOrigin = firstResponderView.convert(CGPoint(), to: nil)
|
||||
var filteredTranslation = containerOrigin.x
|
||||
if firstResponderDisableAutomaticKeyboardHandling.contains(.forward) {
|
||||
filteredTranslation = max(0.0, filteredTranslation)
|
||||
}
|
||||
if firstResponderDisableAutomaticKeyboardHandling.contains(.backward) {
|
||||
filteredTranslation = min(0.0, filteredTranslation)
|
||||
}
|
||||
let horizontalTranslation = CATransform3DMakeTranslation(filteredTranslation, 0.0, 0.0)
|
||||
let currentTransform = keyboardWindow.layer.sublayerTransform
|
||||
if !CATransform3DEqualToTransform(horizontalTranslation, currentTransform) {
|
||||
//print("set to \(CGPoint(x: containerOrigin.x, y: self.interactiveInputOffset))")
|
||||
keyboardWindow.layer.sublayerTransform = horizontalTranslation
|
||||
}
|
||||
if let tracingLayer = firstResponderView.layer as? CATracingLayer, firstResponderDisableAutomaticKeyboardHandling.isEmpty {
|
||||
if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource, previousPositionAnimationMirrorSource !== tracingLayer {
|
||||
previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil)
|
||||
}
|
||||
tracingLayer.setPositionAnimationMirrorTarget(keyboardWindow.layer)
|
||||
self.previousPositionAnimationMirrorSource = tracingLayer
|
||||
} else if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource {
|
||||
previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil)
|
||||
self.previousPositionAnimationMirrorSource = nil
|
||||
}
|
||||
} else {
|
||||
keyboardWindow.layer.sublayerTransform = CATransform3DIdentity
|
||||
if let previousPositionAnimationMirrorSource = self.previousPositionAnimationMirrorSource {
|
||||
previousPositionAnimationMirrorSource.setPositionAnimationMirrorTarget(nil)
|
||||
self.previousPositionAnimationMirrorSource = nil
|
||||
}
|
||||
if let previousFirstResponderView = previousFirstResponderView {
|
||||
if previousFirstResponderView.window == nil {
|
||||
keyboardWindow.isHidden = true
|
||||
keyboardWindow.layer.cancelAnimationsRecursive(key: "position")
|
||||
keyboardWindow.layer.cancelAnimationsRecursive(key: "bounds")
|
||||
keyboardWindow.isHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.previousFirstResponderView = firstResponderView
|
||||
}
|
||||
}
|
10
submodules/Display/Display/LayoutSizes.swift
Normal file
10
submodules/Display/Display/LayoutSizes.swift
Normal file
@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public func horizontalContainerFillingSizeForLayout(layout: ContainerViewLayout, sideInset: CGFloat) -> CGFloat {
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
return min(layout.size.width, 414.0) - sideInset * 2.0
|
||||
} else {
|
||||
return layout.size.width - sideInset * 2.0
|
||||
}
|
||||
}
|
152
submodules/Display/Display/LegacyPresentedController.swift
Normal file
152
submodules/Display/Display/LegacyPresentedController.swift
Normal file
@ -0,0 +1,152 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if BUCK
|
||||
import DisplayPrivate
|
||||
#endif
|
||||
|
||||
public enum LegacyPresentedControllerPresentation {
|
||||
case custom
|
||||
case modal
|
||||
}
|
||||
|
||||
private func passControllerAppearanceAnimated(presentation: LegacyPresentedControllerPresentation) -> Bool {
|
||||
switch presentation {
|
||||
case .custom:
|
||||
return false
|
||||
case .modal:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
open class LegacyPresentedController: ViewController {
|
||||
private let legacyController: UIViewController
|
||||
private let presentation: LegacyPresentedControllerPresentation
|
||||
|
||||
private var controllerNode: LegacyPresentedControllerNode {
|
||||
return self.displayNode as! LegacyPresentedControllerNode
|
||||
}
|
||||
private var loadedController = false
|
||||
|
||||
var controllerLoaded: (() -> Void)?
|
||||
|
||||
private let asPresentable = true
|
||||
|
||||
public init(legacyController: UIViewController, presentation: LegacyPresentedControllerPresentation) {
|
||||
self.legacyController = legacyController
|
||||
self.presentation = presentation
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
/*legacyController.navigation_setDismiss { [weak self] in
|
||||
self?.dismiss()
|
||||
}*/
|
||||
if !asPresentable {
|
||||
self.addChildViewController(legacyController)
|
||||
}
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override open func loadDisplayNode() {
|
||||
self.displayNode = LegacyPresentedControllerNode()
|
||||
}
|
||||
|
||||
override open func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if self.ignoreAppearanceMethodInvocations() {
|
||||
return
|
||||
}
|
||||
|
||||
if !loadedController && !asPresentable {
|
||||
loadedController = true
|
||||
|
||||
self.controllerNode.controllerView = self.legacyController.view
|
||||
self.controllerNode.view.addSubview(self.legacyController.view)
|
||||
self.legacyController.didMove(toParentViewController: self)
|
||||
|
||||
if let controllerLoaded = self.controllerLoaded {
|
||||
controllerLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
if !asPresentable {
|
||||
self.legacyController.viewWillAppear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
|
||||
}
|
||||
}
|
||||
|
||||
override open func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
if self.ignoreAppearanceMethodInvocations() {
|
||||
return
|
||||
}
|
||||
|
||||
if !asPresentable {
|
||||
self.legacyController.viewWillDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
|
||||
}
|
||||
}
|
||||
|
||||
override open func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if self.ignoreAppearanceMethodInvocations() {
|
||||
return
|
||||
}
|
||||
|
||||
if asPresentable {
|
||||
if !loadedController {
|
||||
loadedController = true
|
||||
//self.legacyController.modalPresentationStyle = .currentContext
|
||||
self.present(self.legacyController, animated: false, completion: nil)
|
||||
}
|
||||
} else {
|
||||
switch self.presentation {
|
||||
case .modal:
|
||||
self.controllerNode.animateModalIn()
|
||||
self.legacyController.viewDidAppear(true)
|
||||
case .custom:
|
||||
self.legacyController.viewDidAppear(animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
if !self.asPresentable {
|
||||
self.legacyController.viewDidDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation))
|
||||
}
|
||||
}
|
||||
|
||||
override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
||||
}
|
||||
|
||||
override open func dismiss(completion: (() -> Void)? = nil) {
|
||||
switch self.presentation {
|
||||
case .modal:
|
||||
self.controllerNode.animateModalOut { [weak self] in
|
||||
/*if let controller = self?.legacyController as? TGViewController {
|
||||
controller.didDismiss()
|
||||
} else if let controller = self?.legacyController as? TGNavigationController {
|
||||
controller.didDismiss()
|
||||
}*/
|
||||
self?.presentingViewController?.dismiss(animated: false, completion: completion)
|
||||
}
|
||||
case .custom:
|
||||
/*if let controller = self.legacyController as? TGViewController {
|
||||
controller.didDismiss()
|
||||
} else if let controller = self.legacyController as? TGNavigationController {
|
||||
controller.didDismiss()
|
||||
}*/
|
||||
self.presentingViewController?.dismiss(animated: false, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class LegacyPresentedControllerNode: ASDisplayNode {
|
||||
private var containerLayout: ContainerViewLayout?
|
||||
|
||||
var controllerView: UIView? {
|
||||
didSet {
|
||||
if let controllerView = self.controllerView, let containerLayout = self.containerLayout {
|
||||
controllerView.frame = CGRect(origin: CGPoint(), size: containerLayout.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return UITracingLayerView()
|
||||
})
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.containerLayout = layout
|
||||
if let controllerView = self.controllerView {
|
||||
controllerView.frame = CGRect(origin: CGPoint(), size: layout.size)
|
||||
}
|
||||
}
|
||||
|
||||
func animateModalIn() {
|
||||
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
|
||||
func animateModalOut(completion: @escaping () -> Void) {
|
||||
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
237
submodules/Display/Display/LinkHighlightingNode.swift
Normal file
237
submodules/Display/Display/LinkHighlightingNode.swift
Normal file
@ -0,0 +1,237 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
private enum CornerType {
|
||||
case topLeft
|
||||
case topRight
|
||||
case bottomLeft
|
||||
case bottomRight
|
||||
}
|
||||
|
||||
private func drawFullCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
|
||||
context.setFillColor(color.cgColor)
|
||||
switch type {
|
||||
case .topLeft:
|
||||
context.clear(CGRect(origin: point, size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: point, size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .topRight:
|
||||
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomLeft:
|
||||
context.clear(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomRight:
|
||||
context.clear(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
}
|
||||
}
|
||||
|
||||
private func drawConnectingCorner(context: CGContext, color: UIColor, at point: CGPoint, type: CornerType, radius: CGFloat) {
|
||||
context.setFillColor(color.cgColor)
|
||||
switch type {
|
||||
case .topLeft:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .topRight:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomLeft:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x - radius, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x - radius * 2.0, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
case .bottomRight:
|
||||
context.fill(CGRect(origin: CGPoint(x: point.x, y: point.y - radius), size: CGSize(width: radius, height: radius)))
|
||||
context.setFillColor(UIColor.clear.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: point.x, y: point.y - radius * 2.0), size: CGSize(width: radius * 2.0, height: radius * 2.0)))
|
||||
}
|
||||
}
|
||||
|
||||
private func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat) -> (CGPoint, UIImage?) {
|
||||
if rects.isEmpty {
|
||||
return (CGPoint(), nil)
|
||||
}
|
||||
|
||||
var topLeft = rects[0].origin
|
||||
var bottomRight = CGPoint(x: rects[0].maxX, y: rects[0].maxY)
|
||||
for i in 1 ..< rects.count {
|
||||
topLeft.x = min(topLeft.x, rects[i].origin.x)
|
||||
topLeft.y = min(topLeft.y, rects[i].origin.y)
|
||||
bottomRight.x = max(bottomRight.x, rects[i].maxX)
|
||||
bottomRight.y = max(bottomRight.y, rects[i].maxY)
|
||||
}
|
||||
|
||||
topLeft.x -= inset
|
||||
topLeft.y -= inset
|
||||
bottomRight.x += inset * 2.0
|
||||
bottomRight.y += inset * 2.0
|
||||
|
||||
return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(color.cgColor)
|
||||
|
||||
context.setBlendMode(.copy)
|
||||
|
||||
for i in 0 ..< rects.count {
|
||||
let rect = rects[i].insetBy(dx: -inset, dy: -inset)
|
||||
context.fill(rect.offsetBy(dx: -topLeft.x, dy: -topLeft.y))
|
||||
}
|
||||
|
||||
for i in 0 ..< rects.count {
|
||||
let rect = rects[i].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
|
||||
|
||||
var previous: CGRect?
|
||||
if i != 0 {
|
||||
previous = rects[i - 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
|
||||
}
|
||||
|
||||
var next: CGRect?
|
||||
if i != rects.count - 1 {
|
||||
next = rects[i + 1].insetBy(dx: -inset, dy: -inset).offsetBy(dx: -topLeft.x, dy: -topLeft.y)
|
||||
}
|
||||
|
||||
if let previous = previous {
|
||||
if previous.contains(rect.topLeft) {
|
||||
if abs(rect.topLeft.x - previous.minX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let next = next {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topLeft.x, y: previous.maxY), type: .topLeft, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
|
||||
}
|
||||
if previous.contains(rect.topRight.offsetBy(dx: -1.0, dy: 0.0)) {
|
||||
if abs(rect.topRight.x - previous.maxX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let next = next {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.topRight.x, y: previous.maxY), type: .topRight, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.topLeft, type: .topLeft, radius: outerRadius)
|
||||
drawFullCorner(context: context, color: color, at: rect.topRight, type: .topRight, radius: outerRadius)
|
||||
}
|
||||
|
||||
if let next = next {
|
||||
if next.contains(rect.bottomLeft) {
|
||||
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let previous = previous {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomLeft.x, y: next.minY), type: .bottomLeft, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
|
||||
}
|
||||
if next.contains(rect.bottomRight.offsetBy(dx: -1.0, dy: 0.0)) {
|
||||
if abs(rect.bottomRight.x - next.maxX) >= innerRadius {
|
||||
var radius = innerRadius
|
||||
if let previous = previous {
|
||||
radius = min(radius, floor((next.minY - previous.maxY) / 2.0))
|
||||
}
|
||||
drawConnectingCorner(context: context, color: color, at: CGPoint(x: rect.bottomRight.x, y: next.minY), type: .bottomRight, radius: radius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
|
||||
}
|
||||
} else {
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomLeft, type: .bottomLeft, radius: outerRadius)
|
||||
drawFullCorner(context: context, color: color, at: rect.bottomRight, type: .bottomRight, radius: outerRadius)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
public final class LinkHighlightingNode: ASDisplayNode {
|
||||
private var rects: [CGRect] = []
|
||||
private let imageNode: ASImageNode
|
||||
|
||||
public var innerRadius: CGFloat = 4.0
|
||||
public var outerRadius: CGFloat = 4.0
|
||||
public var inset: CGFloat = 2.0
|
||||
|
||||
private var _color: UIColor
|
||||
public var color: UIColor {
|
||||
get {
|
||||
return _color
|
||||
} set(value) {
|
||||
self._color = value
|
||||
if !self.rects.isEmpty {
|
||||
self.updateImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(color: UIColor) {
|
||||
self._color = color
|
||||
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isUserInteractionEnabled = false
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
}
|
||||
|
||||
public func updateRects(_ rects: [CGRect]) {
|
||||
if self.rects != rects {
|
||||
self.rects = rects
|
||||
|
||||
self.updateImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateImage() {
|
||||
if rects.isEmpty {
|
||||
self.imageNode.image = nil
|
||||
}
|
||||
let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius)
|
||||
|
||||
if let image = image {
|
||||
self.imageNode.image = image
|
||||
self.imageNode.frame = CGRect(origin: offset, size: image.size)
|
||||
}
|
||||
}
|
||||
|
||||
public func asyncLayout() -> (UIColor, [CGRect], CGFloat, CGFloat, CGFloat) -> () -> Void {
|
||||
let currentRects = self.rects
|
||||
let currentColor = self._color
|
||||
let currentInnerRadius = self.innerRadius
|
||||
let currentOuterRadius = self.outerRadius
|
||||
let currentInset = self.inset
|
||||
|
||||
return { [weak self] color, rects, innerRadius, outerRadius, inset in
|
||||
var updatedImage: (CGPoint, UIImage?)?
|
||||
if currentRects != rects || !currentColor.isEqual(color) || currentInnerRadius != innerRadius || currentOuterRadius != outerRadius || currentInset != inset {
|
||||
updatedImage = generateRectsImage(color: color, rects: rects, inset: inset, outerRadius: outerRadius, innerRadius: innerRadius)
|
||||
}
|
||||
|
||||
return {
|
||||
if let strongSelf = self {
|
||||
strongSelf._color = color
|
||||
strongSelf.rects = rects
|
||||
strongSelf.innerRadius = innerRadius
|
||||
strongSelf.outerRadius = outerRadius
|
||||
strongSelf.inset = inset
|
||||
|
||||
if let (offset, maybeImage) = updatedImage, let image = maybeImage {
|
||||
strongSelf.imageNode.image = image
|
||||
strongSelf.imageNode.frame = CGRect(origin: offset, size: image.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3980
submodules/Display/Display/ListView.swift
Normal file
3980
submodules/Display/Display/ListView.swift
Normal file
File diff suppressed because it is too large
Load Diff
6
submodules/Display/Display/ListViewAccessoryItem.swift
Normal file
6
submodules/Display/Display/ListViewAccessoryItem.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
public protocol ListViewAccessoryItem {
|
||||
func isEqualToItem(_ other: ListViewAccessoryItem) -> Bool
|
||||
func node(synchronous: Bool) -> ListViewAccessoryItemNode
|
||||
}
|
50
submodules/Display/Display/ListViewAccessoryItemNode.swift
Normal file
50
submodules/Display/Display/ListViewAccessoryItemNode.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class ListViewAccessoryItemNode: ASDisplayNode {
|
||||
var transitionOffset: CGPoint = CGPoint() {
|
||||
didSet {
|
||||
self.bounds = CGRect(origin: self.transitionOffset, size: self.bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
private var transitionOffsetAnimation: ListViewAnimation?
|
||||
|
||||
final func animateTransitionOffset(_ from: CGPoint, beginAt: Double, duration: Double, curve: @escaping (CGFloat) -> CGFloat) {
|
||||
self.transitionOffset = from
|
||||
self.transitionOffsetAnimation = ListViewAnimation(from: from, to: CGPoint(), duration: duration, curve: curve, beginAt: beginAt, update: { [weak self] _, currentValue in
|
||||
if let strongSelf = self {
|
||||
strongSelf.transitionOffset = currentValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
final func removeAllAnimations() {
|
||||
self.transitionOffsetAnimation = nil
|
||||
self.transitionOffset = CGPoint()
|
||||
}
|
||||
|
||||
final func animate(_ timestamp: Double) -> Bool {
|
||||
if let animation = self.transitionOffsetAnimation {
|
||||
animation.applyAt(timestamp)
|
||||
|
||||
if animation.completeAt(timestamp) {
|
||||
self.transitionOffsetAnimation = nil
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override open func layout() {
|
||||
super.layout()
|
||||
|
||||
self.updateLayout(size: self.bounds.size, leftInset: 0.0, rightInset: 0.0)
|
||||
}
|
||||
|
||||
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
}
|
||||
}
|
182
submodules/Display/Display/ListViewAnimation.swift
Normal file
182
submodules/Display/Display/ListViewAnimation.swift
Normal file
@ -0,0 +1,182 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
#if BUCK
|
||||
import DisplayPrivate
|
||||
#endif
|
||||
|
||||
public protocol Interpolatable {
|
||||
static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> (Interpolatable)
|
||||
}
|
||||
|
||||
private func floorToPixels(_ value: CGFloat) -> CGFloat {
|
||||
return round(value * 10.0) / 10.0
|
||||
}
|
||||
|
||||
private func floorToPixels(_ value: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: round(value.x * 10.0) / 10.0, y: round(value.y * 10.0) / 10.0)
|
||||
}
|
||||
|
||||
private func floorToPixels(_ value: CGSize) -> CGSize {
|
||||
return CGSize(width: round(value.width * 10.0) / 10.0, height: round(value.height * 10.0) / 10.0)
|
||||
}
|
||||
|
||||
private func floorToPixels(_ value: CGRect) -> CGRect {
|
||||
return CGRect(origin: floorToPixels(value.origin), size: floorToPixels(value.size))
|
||||
}
|
||||
|
||||
private func floorToPixels(_ value: UIEdgeInsets) -> UIEdgeInsets {
|
||||
return UIEdgeInsets(top: round(value.top * 10.0) / 10.0, left: round(value.left * 10.0) / 10.0, bottom: round(value.bottom * 10.0) / 10.0, right: round(value.right * 10.0) / 10.0)
|
||||
}
|
||||
|
||||
extension CGFloat: Interpolatable {
|
||||
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
|
||||
return { from, to, t -> Interpolatable in
|
||||
let fromValue: CGFloat = from as! CGFloat
|
||||
let toValue: CGFloat = to as! CGFloat
|
||||
let invT: CGFloat = 1.0 - t
|
||||
let term: CGFloat = toValue * t + fromValue * invT
|
||||
return floorToPixels(term)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIEdgeInsets: Interpolatable {
|
||||
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
|
||||
return { from, to, t -> Interpolatable in
|
||||
let fromValue = from as! UIEdgeInsets
|
||||
let toValue = to as! UIEdgeInsets
|
||||
return floorToPixels(UIEdgeInsets(top: toValue.top * t + fromValue.top * (1.0 - t), left: toValue.left * t + fromValue.left * (1.0 - t), bottom: toValue.bottom * t + fromValue.bottom * (1.0 - t), right: toValue.right * t + fromValue.right * (1.0 - t)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect: Interpolatable {
|
||||
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
|
||||
return { from, to, t -> Interpolatable in
|
||||
let fromValue = from as! CGRect
|
||||
let toValue = to as! CGRect
|
||||
return floorToPixels(CGRect(x: toValue.origin.x * t + fromValue.origin.x * (1.0 - t), y: toValue.origin.y * t + fromValue.origin.y * (1.0 - t), width: toValue.size.width * t + fromValue.size.width * (1.0 - t), height: toValue.size.height * t + fromValue.size.height * (1.0 - t)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint: Interpolatable {
|
||||
public static func interpolator() -> (Interpolatable, Interpolatable, CGFloat) -> Interpolatable {
|
||||
return { from, to, t -> Interpolatable in
|
||||
let fromValue = from as! CGPoint
|
||||
let toValue = to as! CGPoint
|
||||
return floorToPixels(CGPoint(x: toValue.x * t + fromValue.x * (1.0 - t), y: toValue.y * t + fromValue.y * (1.0 - t)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let springAnimationIn: CABasicAnimation = {
|
||||
let animation = makeSpringAnimation("")
|
||||
return animation
|
||||
}()
|
||||
|
||||
private let springAnimationSolver: (CGFloat) -> CGFloat = { () -> (CGFloat) -> CGFloat in
|
||||
if #available(iOS 9.0, *) {
|
||||
return { t in
|
||||
return springAnimationValueAt(springAnimationIn, t)
|
||||
}
|
||||
} else {
|
||||
return { t in
|
||||
return bezierPoint(0.23, 1.0, 0.32, 1.0, t)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
public let listViewAnimationCurveSystem: (CGFloat) -> CGFloat = { t in
|
||||
return springAnimationSolver(t)
|
||||
}
|
||||
|
||||
public let listViewAnimationCurveLinear: (CGFloat) -> CGFloat = { t in
|
||||
return t
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
public func listViewAnimationCurveFromAnimationOptions(animationOptions: UIViewAnimationOptions) -> (CGFloat) -> CGFloat {
|
||||
if animationOptions.rawValue == UInt(7 << 16) {
|
||||
return listViewAnimationCurveSystem
|
||||
} else {
|
||||
return listViewAnimationCurveLinear
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public final class ListViewAnimation {
|
||||
let from: Interpolatable
|
||||
let to: Interpolatable
|
||||
let duration: Double
|
||||
let startTime: Double
|
||||
private let curve: (CGFloat) -> CGFloat
|
||||
private let interpolator: (Interpolatable, Interpolatable, CGFloat) -> Interpolatable
|
||||
private let update: (CGFloat, Interpolatable) -> Void
|
||||
private let completed: (Bool) -> Void
|
||||
|
||||
public init<T: Interpolatable>(from: T, to: T, duration: Double, curve: @escaping (CGFloat) -> CGFloat, beginAt: Double, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) {
|
||||
self.from = from
|
||||
self.to = to
|
||||
self.duration = duration
|
||||
self.curve = curve
|
||||
self.startTime = beginAt
|
||||
self.interpolator = T.interpolator()
|
||||
self.update = { progress, value in
|
||||
update(progress, value as! T)
|
||||
}
|
||||
self.completed = completed
|
||||
}
|
||||
|
||||
init<T: Interpolatable>(copying: ListViewAnimation, update: @escaping (CGFloat, T) -> Void, completed: @escaping (Bool) -> Void = { _ in }) {
|
||||
self.from = copying.from
|
||||
self.to = copying.to
|
||||
self.duration = copying.duration
|
||||
self.curve = copying.curve
|
||||
self.startTime = copying.startTime
|
||||
self.interpolator = copying.interpolator
|
||||
self.update = { progress, value in
|
||||
update(progress, value as! T)
|
||||
}
|
||||
self.completed = completed
|
||||
}
|
||||
|
||||
public func completeAt(_ timestamp: Double) -> Bool {
|
||||
if timestamp >= self.startTime + self.duration {
|
||||
self.completed(true)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
self.completed(false)
|
||||
}
|
||||
|
||||
private func valueAt(_ t: CGFloat) -> Interpolatable {
|
||||
if t <= 0.0 {
|
||||
return self.from
|
||||
} else if t >= 1.0 {
|
||||
return self.to
|
||||
} else {
|
||||
return self.interpolator(self.from, self.to, t)
|
||||
}
|
||||
}
|
||||
|
||||
public func applyAt(_ timestamp: Double) {
|
||||
var t = CGFloat((timestamp - self.startTime) / self.duration)
|
||||
let ct: CGFloat
|
||||
if t <= 0.0 + CGFloat.ulpOfOne {
|
||||
t = 0.0
|
||||
ct = 0.0
|
||||
} else if t >= 1.0 - CGFloat.ulpOfOne {
|
||||
t = 1.0
|
||||
ct = 1.0
|
||||
} else {
|
||||
ct = self.curve(t)
|
||||
}
|
||||
self.update(ct, self.valueAt(ct))
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
open class ListViewFloatingHeaderNode: ASDisplayNode {
|
||||
open func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
return 0.0
|
||||
}
|
||||
}
|
868
submodules/Display/Display/ListViewIntermediateState.swift
Normal file
868
submodules/Display/Display/ListViewIntermediateState.swift
Normal file
@ -0,0 +1,868 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ListViewCenterScrollPositionOverflow {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
public enum ListViewScrollPosition: Equatable {
|
||||
case top(CGFloat)
|
||||
case bottom(CGFloat)
|
||||
case center(ListViewCenterScrollPositionOverflow)
|
||||
case visible
|
||||
}
|
||||
|
||||
public enum ListViewScrollToItemDirectionHint {
|
||||
case Up
|
||||
case Down
|
||||
}
|
||||
|
||||
public enum ListViewAnimationCurve {
|
||||
case Spring(duration: Double)
|
||||
case Default(duration: Double?)
|
||||
}
|
||||
|
||||
public struct ListViewScrollToItem {
|
||||
public let index: Int
|
||||
public let position: ListViewScrollPosition
|
||||
public let animated: Bool
|
||||
public let curve: ListViewAnimationCurve
|
||||
public let directionHint: ListViewScrollToItemDirectionHint
|
||||
|
||||
public init(index: Int, position: ListViewScrollPosition, animated: Bool, curve: ListViewAnimationCurve, directionHint: ListViewScrollToItemDirectionHint) {
|
||||
self.index = index
|
||||
self.position = position
|
||||
self.animated = animated
|
||||
self.curve = curve
|
||||
self.directionHint = directionHint
|
||||
}
|
||||
}
|
||||
|
||||
public enum ListViewItemOperationDirectionHint {
|
||||
case Up
|
||||
case Down
|
||||
}
|
||||
|
||||
public struct ListViewDeleteItem {
|
||||
public let index: Int
|
||||
public let directionHint: ListViewItemOperationDirectionHint?
|
||||
|
||||
public init(index: Int, directionHint: ListViewItemOperationDirectionHint?) {
|
||||
self.index = index
|
||||
self.directionHint = directionHint
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListViewInsertItem {
|
||||
public let index: Int
|
||||
public let previousIndex: Int?
|
||||
public let item: ListViewItem
|
||||
public let directionHint: ListViewItemOperationDirectionHint?
|
||||
public let forceAnimateInsertion: Bool
|
||||
|
||||
public init(index: Int, previousIndex: Int?, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?, forceAnimateInsertion: Bool = false) {
|
||||
self.index = index
|
||||
self.previousIndex = previousIndex
|
||||
self.item = item
|
||||
self.directionHint = directionHint
|
||||
self.forceAnimateInsertion = forceAnimateInsertion
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListViewUpdateItem {
|
||||
public let index: Int
|
||||
public let previousIndex: Int
|
||||
public let item: ListViewItem
|
||||
public let directionHint: ListViewItemOperationDirectionHint?
|
||||
|
||||
public init(index: Int, previousIndex: Int, item: ListViewItem, directionHint: ListViewItemOperationDirectionHint?) {
|
||||
self.index = index
|
||||
self.previousIndex = previousIndex
|
||||
self.item = item
|
||||
self.directionHint = directionHint
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListViewDeleteAndInsertOptions: OptionSet {
|
||||
public let rawValue: Int
|
||||
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let AnimateInsertion = ListViewDeleteAndInsertOptions(rawValue: 1)
|
||||
public static let AnimateAlpha = ListViewDeleteAndInsertOptions(rawValue: 2)
|
||||
public static let LowLatency = ListViewDeleteAndInsertOptions(rawValue: 4)
|
||||
public static let Synchronous = ListViewDeleteAndInsertOptions(rawValue: 8)
|
||||
public static let RequestItemInsertionAnimations = ListViewDeleteAndInsertOptions(rawValue: 16)
|
||||
public static let AnimateTopItemPosition = ListViewDeleteAndInsertOptions(rawValue: 32)
|
||||
public static let PreferSynchronousDrawing = ListViewDeleteAndInsertOptions(rawValue: 64)
|
||||
public static let PreferSynchronousResourceLoading = ListViewDeleteAndInsertOptions(rawValue: 128)
|
||||
public static let AnimateCrossfade = ListViewDeleteAndInsertOptions(rawValue: 256)
|
||||
}
|
||||
|
||||
public struct ListViewUpdateSizeAndInsets {
|
||||
public let size: CGSize
|
||||
public let insets: UIEdgeInsets
|
||||
public let headerInsets: UIEdgeInsets?
|
||||
public let scrollIndicatorInsets: UIEdgeInsets?
|
||||
public let duration: Double
|
||||
public let curve: ListViewAnimationCurve
|
||||
public let ensureTopInsetForOverlayHighlightedItems: CGFloat?
|
||||
|
||||
public init(size: CGSize, insets: UIEdgeInsets, headerInsets: UIEdgeInsets? = nil, scrollIndicatorInsets: UIEdgeInsets? = nil, duration: Double, curve: ListViewAnimationCurve, ensureTopInsetForOverlayHighlightedItems: CGFloat? = nil) {
|
||||
self.size = size
|
||||
self.insets = insets
|
||||
self.headerInsets = headerInsets
|
||||
self.scrollIndicatorInsets = scrollIndicatorInsets
|
||||
self.duration = duration
|
||||
self.curve = curve
|
||||
self.ensureTopInsetForOverlayHighlightedItems = ensureTopInsetForOverlayHighlightedItems
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListViewItemRange: Equatable {
|
||||
public let firstIndex: Int
|
||||
public let lastIndex: Int
|
||||
}
|
||||
|
||||
public struct ListViewVisibleItemRange: Equatable {
|
||||
public let firstIndex: Int
|
||||
public let firstIndexFullyVisible: Bool
|
||||
public let lastIndex: Int
|
||||
}
|
||||
|
||||
public struct ListViewDisplayedItemRange: Equatable {
|
||||
public let loadedRange: ListViewItemRange?
|
||||
public let visibleRange: ListViewVisibleItemRange?
|
||||
}
|
||||
|
||||
struct IndexRange {
|
||||
let first: Int
|
||||
let last: Int
|
||||
|
||||
func contains(_ index: Int) -> Bool {
|
||||
return index >= first && index <= last
|
||||
}
|
||||
|
||||
var empty: Bool {
|
||||
return first > last
|
||||
}
|
||||
}
|
||||
|
||||
struct OffsetRanges {
|
||||
var offsets: [(IndexRange, CGFloat)] = []
|
||||
|
||||
mutating func append(_ other: OffsetRanges) {
|
||||
self.offsets.append(contentsOf: other.offsets)
|
||||
}
|
||||
|
||||
mutating func offset(_ indexRange: IndexRange, offset: CGFloat) {
|
||||
self.offsets.append((indexRange, offset))
|
||||
}
|
||||
|
||||
func offsetForIndex(_ index: Int) -> CGFloat {
|
||||
var result: CGFloat = 0.0
|
||||
for offset in self.offsets {
|
||||
if offset.0.contains(index) {
|
||||
result += offset.1
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func binarySearch(_ inputArr: [Int], searchItem: Int) -> Int? {
|
||||
var lowerIndex = 0;
|
||||
var upperIndex = inputArr.count - 1
|
||||
|
||||
if lowerIndex > upperIndex {
|
||||
return nil
|
||||
}
|
||||
|
||||
while (true) {
|
||||
let currentIndex = (lowerIndex + upperIndex) / 2
|
||||
if (inputArr[currentIndex] == searchItem) {
|
||||
return currentIndex
|
||||
} else if (lowerIndex > upperIndex) {
|
||||
return nil
|
||||
} else {
|
||||
if (inputArr[currentIndex] > searchItem) {
|
||||
upperIndex = currentIndex - 1
|
||||
} else {
|
||||
lowerIndex = currentIndex + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TransactionState {
|
||||
let visibleSize: CGSize
|
||||
let items: [ListViewItem]
|
||||
}
|
||||
|
||||
struct PendingNode {
|
||||
let index: Int
|
||||
let node: QueueLocalObject<ListViewItemNode>
|
||||
let apply: () -> (Signal<Void, NoError>?, () -> Void)
|
||||
let frame: CGRect
|
||||
let apparentHeight: CGFloat
|
||||
}
|
||||
|
||||
enum ListViewStateNode {
|
||||
case Node(index: Int, frame: CGRect, referenceNode: QueueLocalObject<ListViewItemNode>?)
|
||||
case Placeholder(frame: CGRect)
|
||||
|
||||
var index: Int? {
|
||||
switch self {
|
||||
case .Node(let index, _, _):
|
||||
return index
|
||||
case .Placeholder(_):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var frame: CGRect {
|
||||
get {
|
||||
switch self {
|
||||
case .Node(_, let frame, _):
|
||||
return frame
|
||||
case .Placeholder(let frame):
|
||||
return frame
|
||||
}
|
||||
} set(value) {
|
||||
switch self {
|
||||
case let .Node(index, _, referenceNode):
|
||||
self = .Node(index: index, frame: value, referenceNode: referenceNode)
|
||||
case .Placeholder(_):
|
||||
self = .Placeholder(frame: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ListViewInsertionOffsetDirection {
|
||||
case Up
|
||||
case Down
|
||||
|
||||
init(_ hint: ListViewItemOperationDirectionHint) {
|
||||
switch hint {
|
||||
case .Up:
|
||||
self = .Up
|
||||
case .Down:
|
||||
self = .Down
|
||||
}
|
||||
}
|
||||
|
||||
func inverted() -> ListViewInsertionOffsetDirection {
|
||||
switch self {
|
||||
case .Up:
|
||||
return .Down
|
||||
case .Down:
|
||||
return .Up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ListViewInsertionPoint {
|
||||
let index: Int
|
||||
let point: CGPoint
|
||||
let direction: ListViewInsertionOffsetDirection
|
||||
}
|
||||
|
||||
struct ListViewState {
|
||||
var insets: UIEdgeInsets
|
||||
var visibleSize: CGSize
|
||||
let invisibleInset: CGFloat
|
||||
var nodes: [ListViewStateNode]
|
||||
var scrollPosition: (Int, ListViewScrollPosition)?
|
||||
var stationaryOffset: (Int, CGFloat)?
|
||||
let stackFromBottom: Bool
|
||||
|
||||
mutating func fixScrollPosition(_ itemCount: Int) {
|
||||
if let (fixedIndex, fixedPosition) = self.scrollPosition {
|
||||
for node in self.nodes {
|
||||
if let index = node.index, index == fixedIndex {
|
||||
let offset: CGFloat
|
||||
switch fixedPosition {
|
||||
case let .bottom(additionalOffset):
|
||||
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY + additionalOffset
|
||||
case let .top(additionalOffset):
|
||||
offset = self.insets.top - node.frame.minY + additionalOffset
|
||||
case let .center(overflow):
|
||||
let contentAreaHeight = self.visibleSize.height - self.insets.bottom - self.insets.top
|
||||
if node.frame.size.height <= contentAreaHeight + CGFloat.ulpOfOne {
|
||||
offset = self.insets.top + floor((contentAreaHeight - node.frame.size.height) / 2.0) - node.frame.minY
|
||||
} else {
|
||||
switch overflow {
|
||||
case .top:
|
||||
offset = self.insets.top - node.frame.minY
|
||||
case .bottom:
|
||||
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY
|
||||
}
|
||||
}
|
||||
case .visible:
|
||||
if node.frame.maxY > self.visibleSize.height - self.insets.bottom {
|
||||
offset = (self.visibleSize.height - self.insets.bottom) - node.frame.maxY
|
||||
} else if node.frame.minY < self.insets.top {
|
||||
offset = self.insets.top - node.frame.minY
|
||||
} else {
|
||||
offset = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
var minY: CGFloat = CGFloat.greatestFiniteMagnitude
|
||||
var maxY: CGFloat = 0.0
|
||||
for i in 0 ..< self.nodes.count {
|
||||
var frame = self.nodes[i].frame
|
||||
frame = frame.offsetBy(dx: 0.0, dy: offset)
|
||||
self.nodes[i].frame = frame
|
||||
|
||||
minY = min(minY, frame.minY)
|
||||
maxY = max(maxY, frame.maxY)
|
||||
}
|
||||
|
||||
var additionalOffset: CGFloat = 0.0
|
||||
if minY > self.insets.top {
|
||||
additionalOffset = self.insets.top - minY
|
||||
}
|
||||
|
||||
if abs(additionalOffset) > CGFloat.ulpOfOne {
|
||||
for i in 0 ..< self.nodes.count {
|
||||
var frame = self.nodes[i].frame
|
||||
frame = frame.offsetBy(dx: 0.0, dy: additionalOffset)
|
||||
self.nodes[i].frame = frame
|
||||
}
|
||||
}
|
||||
|
||||
self.snapToBounds(itemCount, snapTopItem: true, stackFromBottom: self.stackFromBottom)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if let (stationaryIndex, stationaryOffset) = self.stationaryOffset {
|
||||
for node in self.nodes {
|
||||
if node.index == stationaryIndex {
|
||||
let offset = stationaryOffset - node.frame.minY
|
||||
|
||||
if abs(offset) > CGFloat.ulpOfOne {
|
||||
for i in 0 ..< self.nodes.count {
|
||||
var frame = self.nodes[i].frame
|
||||
frame = frame.offsetBy(dx: 0.0, dy: offset)
|
||||
self.nodes[i].frame = frame
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func setupStationaryOffset(_ index: Int, boundary: Int, frames: [Int: CGRect]) {
|
||||
if index < boundary {
|
||||
for node in self.nodes {
|
||||
if let nodeIndex = node.index , nodeIndex >= index {
|
||||
if let frame = frames[nodeIndex] {
|
||||
self.stationaryOffset = (nodeIndex, frame.minY)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for node in self.nodes.reversed() {
|
||||
if let nodeIndex = node.index , nodeIndex <= index {
|
||||
if let frame = frames[nodeIndex] {
|
||||
self.stationaryOffset = (nodeIndex, frame.minY)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutating func snapToBounds(_ itemCount: Int, snapTopItem: Bool, stackFromBottom: Bool) {
|
||||
var completeHeight: CGFloat = 0.0
|
||||
var topItemFound = false
|
||||
var bottomItemFound = false
|
||||
var topItemEdge: CGFloat = 0.0
|
||||
var bottomItemEdge: CGFloat = 0.0
|
||||
|
||||
for node in self.nodes {
|
||||
if let index = node.index {
|
||||
if index == 0 {
|
||||
topItemFound = true
|
||||
topItemEdge = node.frame.minY
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for node in self.nodes.reversed() {
|
||||
if let index = node.index {
|
||||
if index == itemCount - 1 {
|
||||
bottomItemFound = true
|
||||
bottomItemEdge = node.frame.maxY
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if topItemFound && bottomItemFound {
|
||||
for node in self.nodes {
|
||||
completeHeight += node.frame.size.height
|
||||
}
|
||||
}
|
||||
|
||||
let overscroll: CGFloat = 0.0
|
||||
|
||||
var offset: CGFloat = 0.0
|
||||
if topItemFound && bottomItemFound {
|
||||
let areaHeight = min(completeHeight, self.visibleSize.height - self.insets.bottom - self.insets.top)
|
||||
if bottomItemEdge < self.insets.top + areaHeight - overscroll {
|
||||
offset = self.insets.top + areaHeight - overscroll - bottomItemEdge
|
||||
} else if topItemEdge > self.insets.top - overscroll && snapTopItem {
|
||||
offset = (self.insets.top - overscroll) - topItemEdge
|
||||
}
|
||||
} else if topItemFound {
|
||||
if topItemEdge > self.insets.top - overscroll && snapTopItem {
|
||||
offset = (self.insets.top - overscroll) - topItemEdge
|
||||
}
|
||||
} else if bottomItemFound {
|
||||
if bottomItemEdge < self.visibleSize.height - self.insets.bottom - overscroll {
|
||||
offset = self.visibleSize.height - self.insets.bottom - overscroll - bottomItemEdge
|
||||
}
|
||||
}
|
||||
|
||||
if abs(offset) > CGFloat.ulpOfOne {
|
||||
for i in 0 ..< self.nodes.count {
|
||||
var frame = self.nodes[i].frame
|
||||
frame.origin.y += offset
|
||||
self.nodes[i].frame = frame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func insertionPoint(_ insertDirectionHints: [Int: ListViewItemOperationDirectionHint], itemCount: Int) -> ListViewInsertionPoint? {
|
||||
var fixedNode: (nodeIndex: Int, index: Int, frame: CGRect)?
|
||||
|
||||
if let (fixedIndex, _) = self.scrollPosition {
|
||||
for i in 0 ..< self.nodes.count {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index , index == fixedIndex {
|
||||
fixedNode = (i, index, node.frame)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if fixedNode == nil {
|
||||
return ListViewInsertionPoint(index: fixedIndex, point: CGPoint(), direction: .Down)
|
||||
}
|
||||
}
|
||||
|
||||
var fixedNodeIsStationary = false
|
||||
if fixedNode == nil {
|
||||
if let (fixedIndex, _) = self.stationaryOffset {
|
||||
for i in 0 ..< self.nodes.count {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index , index == fixedIndex {
|
||||
fixedNode = (i, index, node.frame)
|
||||
fixedNodeIsStationary = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fixedNode == nil {
|
||||
for i in 0 ..< self.nodes.count {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index , node.frame.maxY >= self.insets.top {
|
||||
fixedNode = (i, index, node.frame)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fixedNode == nil && self.nodes.count != 0 {
|
||||
for i in (0 ..< self.nodes.count).reversed() {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index {
|
||||
fixedNode = (i, index, node.frame)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let fixedNode = fixedNode {
|
||||
var currentUpperNode = fixedNode
|
||||
for i in (0 ..< fixedNode.nodeIndex).reversed() {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index {
|
||||
if index != currentUpperNode.index - 1 {
|
||||
if currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne {
|
||||
var directionHint: ListViewInsertionOffsetDirection?
|
||||
if let hint = insertDirectionHints[currentUpperNode.index - 1] , currentUpperNode.frame.minY > self.insets.top - CGFloat.ulpOfOne {
|
||||
directionHint = ListViewInsertionOffsetDirection(hint)
|
||||
}
|
||||
return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
currentUpperNode = (i, index, node.frame)
|
||||
}
|
||||
}
|
||||
|
||||
if currentUpperNode.index != 0 && currentUpperNode.frame.minY > -self.invisibleInset - CGFloat.ulpOfOne {
|
||||
var directionHint: ListViewInsertionOffsetDirection?
|
||||
if let hint = insertDirectionHints[currentUpperNode.index - 1] {
|
||||
if currentUpperNode.frame.minY >= self.insets.top - CGFloat.ulpOfOne {
|
||||
directionHint = ListViewInsertionOffsetDirection(hint)
|
||||
}
|
||||
} else if currentUpperNode.frame.minY >= self.insets.top - CGFloat.ulpOfOne && !fixedNodeIsStationary {
|
||||
directionHint = .Down
|
||||
}
|
||||
|
||||
return ListViewInsertionPoint(index: currentUpperNode.index - 1, point: CGPoint(x: 0.0, y: currentUpperNode.frame.minY), direction: directionHint ?? .Up)
|
||||
}
|
||||
|
||||
var currentLowerNode = fixedNode
|
||||
if fixedNode.nodeIndex + 1 < self.nodes.count {
|
||||
for i in (fixedNode.nodeIndex + 1) ..< self.nodes.count {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index {
|
||||
if index != currentLowerNode.index + 1 {
|
||||
if currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne {
|
||||
var directionHint: ListViewInsertionOffsetDirection?
|
||||
if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne {
|
||||
directionHint = ListViewInsertionOffsetDirection(hint)
|
||||
}
|
||||
return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
currentLowerNode = (i, index, node.frame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentLowerNode.index != itemCount - 1 && currentLowerNode.frame.maxY < self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne {
|
||||
var directionHint: ListViewInsertionOffsetDirection?
|
||||
if let hint = insertDirectionHints[currentLowerNode.index + 1] , currentLowerNode.frame.maxY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne {
|
||||
directionHint = ListViewInsertionOffsetDirection(hint)
|
||||
}
|
||||
return ListViewInsertionPoint(index: currentLowerNode.index + 1, point: CGPoint(x: 0.0, y: currentLowerNode.frame.maxY), direction: directionHint ?? .Down)
|
||||
}
|
||||
} else if itemCount != 0 {
|
||||
return ListViewInsertionPoint(index: 0, point: CGPoint(x: 0.0, y: self.insets.top), direction: .Down)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
mutating func removeInvisibleNodes(_ operations: inout [ListViewStateOperation]) {
|
||||
var i = 0
|
||||
var visibleItemNodeHeight: CGFloat = 0.0
|
||||
while i < self.nodes.count {
|
||||
visibleItemNodeHeight += self.nodes[i].frame.height
|
||||
i += 1
|
||||
}
|
||||
|
||||
if visibleItemNodeHeight > (self.visibleSize.height + self.invisibleInset + self.invisibleInset) {
|
||||
i = self.nodes.count - 1
|
||||
while i >= 0 {
|
||||
let itemNode = self.nodes[i]
|
||||
let frame = itemNode.frame
|
||||
//print("node \(i) frame \(frame)")
|
||||
if frame.maxY < -self.invisibleInset || frame.origin.y > self.visibleSize.height + self.invisibleInset {
|
||||
//print("remove invisible 1 \(i) frame \(frame)")
|
||||
operations.append(.Remove(index: i, offsetDirection: frame.maxY < -self.invisibleInset ? .Down : .Up))
|
||||
self.nodes.remove(at: i)
|
||||
}
|
||||
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
|
||||
let upperBound = -self.invisibleInset + CGFloat.ulpOfOne
|
||||
for i in 0 ..< self.nodes.count {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index , node.frame.maxY > upperBound {
|
||||
if i != 0 {
|
||||
var previousIndex = index
|
||||
for j in (0 ..< i).reversed() {
|
||||
if self.nodes[j].frame.maxY < upperBound {
|
||||
if let index = self.nodes[j].index {
|
||||
if index != previousIndex - 1 {
|
||||
//print("remove monotonity \(j) (\(index))")
|
||||
operations.append(.Remove(index: j, offsetDirection: .Down))
|
||||
self.nodes.remove(at: j)
|
||||
} else {
|
||||
previousIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let lowerBound = self.visibleSize.height + self.invisibleInset - CGFloat.ulpOfOne
|
||||
for i in (0 ..< self.nodes.count).reversed() {
|
||||
let node = self.nodes[i]
|
||||
if let index = node.index , node.frame.minY < lowerBound {
|
||||
if i != self.nodes.count - 1 {
|
||||
var previousIndex = index
|
||||
var removeIndices: [Int] = []
|
||||
for j in (i + 1) ..< self.nodes.count {
|
||||
if self.nodes[j].frame.minY > lowerBound {
|
||||
if let index = self.nodes[j].index {
|
||||
if index != previousIndex + 1 {
|
||||
removeIndices.append(j)
|
||||
} else {
|
||||
previousIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !removeIndices.isEmpty {
|
||||
for i in removeIndices.reversed() {
|
||||
//print("remove monotonity \(i) (\(self.nodes[i].index!))")
|
||||
operations.append(.Remove(index: i, offsetDirection: .Up))
|
||||
self.nodes.remove(at: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nodeInsertionPointAndIndex(_ itemIndex: Int) -> (CGPoint, Int) {
|
||||
if self.nodes.count == 0 {
|
||||
return (CGPoint(x: 0.0, y: self.insets.top), 0)
|
||||
} else {
|
||||
var index = 0
|
||||
var lastNodeWithIndex = -1
|
||||
for node in self.nodes {
|
||||
if let nodeItemIndex = node.index {
|
||||
if nodeItemIndex > itemIndex {
|
||||
break
|
||||
}
|
||||
lastNodeWithIndex = index
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
lastNodeWithIndex += 1
|
||||
return (CGPoint(x: 0.0, y: lastNodeWithIndex == 0 ? self.nodes[0].frame.minY : self.nodes[lastNodeWithIndex - 1].frame.maxY), lastNodeWithIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func continuousHeightRelativeToNodeIndex(_ fixedNodeIndex: Int) -> CGFloat {
|
||||
let fixedIndex = self.nodes[fixedNodeIndex].index!
|
||||
|
||||
var height: CGFloat = 0.0
|
||||
|
||||
if fixedNodeIndex != 0 {
|
||||
var upperIndex = fixedIndex
|
||||
for i in (0 ..< fixedNodeIndex).reversed() {
|
||||
if let index = self.nodes[i].index {
|
||||
if index == upperIndex - 1 {
|
||||
height += self.nodes[i].frame.size.height
|
||||
upperIndex = index
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fixedNodeIndex != self.nodes.count - 1 {
|
||||
var lowerIndex = fixedIndex
|
||||
for i in (fixedNodeIndex + 1) ..< self.nodes.count {
|
||||
if let index = self.nodes[i].index {
|
||||
if index == lowerIndex + 1 {
|
||||
height += self.nodes[i].frame.size.height
|
||||
lowerIndex = index
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
mutating func insertNode(_ itemIndex: Int, node: QueueLocalObject<ListViewItemNode>, layout: ListViewItemNodeLayout, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, operations: inout [ListViewStateOperation], itemCount: Int) {
|
||||
let (insertionOrigin, insertionIndex) = self.nodeInsertionPointAndIndex(itemIndex)
|
||||
|
||||
let nodeOrigin: CGPoint
|
||||
switch offsetDirection {
|
||||
case .Up:
|
||||
nodeOrigin = CGPoint(x: insertionOrigin.x, y: insertionOrigin.y - (animated ? 0.0 : layout.size.height))
|
||||
case .Down:
|
||||
nodeOrigin = insertionOrigin
|
||||
}
|
||||
|
||||
let nodeFrame = CGRect(origin: nodeOrigin, size: CGSize(width: layout.size.width, height: animated ? 0.0 : layout.size.height))
|
||||
|
||||
operations.append(.InsertNode(index: insertionIndex, offsetDirection: offsetDirection, animated: animated, node: node, layout: layout, apply: apply))
|
||||
self.nodes.insert(.Node(index: itemIndex, frame: nodeFrame, referenceNode: nil), at: insertionIndex)
|
||||
|
||||
if !animated {
|
||||
switch offsetDirection {
|
||||
case .Up:
|
||||
var i = insertionIndex - 1
|
||||
while i >= 0 {
|
||||
var frame = self.nodes[i].frame
|
||||
frame.origin.y -= nodeFrame.size.height
|
||||
self.nodes[i].frame = frame
|
||||
i -= 1
|
||||
}
|
||||
case .Down:
|
||||
var i = insertionIndex + 1
|
||||
while i < self.nodes.count {
|
||||
var frame = self.nodes[i].frame
|
||||
frame.origin.y += nodeFrame.size.height
|
||||
self.nodes[i].frame = frame
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var previousIndex: Int?
|
||||
for node in self.nodes {
|
||||
if let index = node.index {
|
||||
if let currentPreviousIndex = previousIndex {
|
||||
if index <= currentPreviousIndex {
|
||||
print("index <= previousIndex + 1")
|
||||
break
|
||||
}
|
||||
previousIndex = index
|
||||
} else {
|
||||
previousIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = self.scrollPosition {
|
||||
self.fixScrollPosition(itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func removeNodeAtIndex(_ index: Int, direction: ListViewItemOperationDirectionHint?, animated: Bool, operations: inout [ListViewStateOperation]) {
|
||||
let node = self.nodes[index]
|
||||
if case let .Node(_, _, referenceNode) = node {
|
||||
let nodeFrame = node.frame
|
||||
self.nodes.remove(at: index)
|
||||
let offsetDirection: ListViewInsertionOffsetDirection
|
||||
if let direction = direction {
|
||||
offsetDirection = ListViewInsertionOffsetDirection(direction)
|
||||
} else {
|
||||
if nodeFrame.maxY < self.insets.top + CGFloat.ulpOfOne {
|
||||
offsetDirection = .Down
|
||||
} else {
|
||||
offsetDirection = .Up
|
||||
}
|
||||
}
|
||||
operations.append(.Remove(index: index, offsetDirection: offsetDirection))
|
||||
|
||||
if let referenceNode = referenceNode , animated {
|
||||
self.nodes.insert(.Placeholder(frame: nodeFrame), at: index)
|
||||
operations.append(.InsertDisappearingPlaceholder(index: index, referenceNode: referenceNode, offsetDirection: offsetDirection.inverted()))
|
||||
} else {
|
||||
if nodeFrame.maxY > self.insets.top - CGFloat.ulpOfOne {
|
||||
if let direction = direction , direction == .Down && node.frame.minY < self.visibleSize.height - self.insets.bottom + CGFloat.ulpOfOne {
|
||||
for i in (0 ..< index).reversed() {
|
||||
var frame = self.nodes[i].frame
|
||||
frame.origin.y += nodeFrame.size.height
|
||||
self.nodes[i].frame = frame
|
||||
}
|
||||
} else {
|
||||
for i in index ..< self.nodes.count {
|
||||
var frame = self.nodes[i].frame
|
||||
frame.origin.y -= nodeFrame.size.height
|
||||
self.nodes[i].frame = frame
|
||||
}
|
||||
}
|
||||
} else if index != 0 {
|
||||
for i in (0 ..< index).reversed() {
|
||||
var frame = self.nodes[i].frame
|
||||
frame.origin.y += nodeFrame.size.height
|
||||
self.nodes[i].frame = frame
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
mutating func updateNodeAtItemIndex(_ itemIndex: Int, layout: ListViewItemNodeLayout, direction: ListViewItemOperationDirectionHint?, animation: ListViewItemUpdateAnimation, apply: @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void), operations: inout [ListViewStateOperation]) {
|
||||
var i = -1
|
||||
for node in self.nodes {
|
||||
i += 1
|
||||
if node.index == itemIndex {
|
||||
switch animation {
|
||||
case .None:
|
||||
let offsetDirection: ListViewInsertionOffsetDirection
|
||||
if let direction = direction {
|
||||
offsetDirection = ListViewInsertionOffsetDirection(direction)
|
||||
} else {
|
||||
if node.frame.maxY < self.insets.top + CGFloat.ulpOfOne {
|
||||
offsetDirection = .Down
|
||||
} else {
|
||||
offsetDirection = .Up
|
||||
}
|
||||
}
|
||||
|
||||
switch offsetDirection {
|
||||
case .Up:
|
||||
let offsetDelta = -(layout.size.height - node.frame.size.height)
|
||||
var updatedFrame = node.frame
|
||||
updatedFrame.origin.y += offsetDelta
|
||||
updatedFrame.size.height = layout.size.height
|
||||
self.nodes[i].frame = updatedFrame
|
||||
|
||||
for j in 0 ..< i {
|
||||
var frame = self.nodes[j].frame
|
||||
frame.origin.y += offsetDelta
|
||||
self.nodes[j].frame = frame
|
||||
}
|
||||
case .Down:
|
||||
let offsetDelta = layout.size.height - node.frame.size.height
|
||||
var updatedFrame = node.frame
|
||||
updatedFrame.size.height = layout.size.height
|
||||
self.nodes[i].frame = updatedFrame
|
||||
|
||||
for j in i + 1 ..< self.nodes.count {
|
||||
var frame = self.nodes[j].frame
|
||||
frame.origin.y += offsetDelta
|
||||
self.nodes[j].frame = frame
|
||||
}
|
||||
}
|
||||
|
||||
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
|
||||
case .System:
|
||||
operations.append(.UpdateLayout(index: i, layout: layout, apply: apply))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ListViewStateOperation {
|
||||
case InsertNode(index: Int, offsetDirection: ListViewInsertionOffsetDirection, animated: Bool, node: QueueLocalObject<ListViewItemNode>, layout: ListViewItemNodeLayout, apply: () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void))
|
||||
case InsertDisappearingPlaceholder(index: Int, referenceNode: QueueLocalObject<ListViewItemNode>, offsetDirection: ListViewInsertionOffsetDirection)
|
||||
case Remove(index: Int, offsetDirection: ListViewInsertionOffsetDirection)
|
||||
case Remap([Int: Int])
|
||||
case UpdateLayout(index: Int, layout: ListViewItemNodeLayout, apply: () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void))
|
||||
}
|
71
submodules/Display/Display/ListViewItem.swift
Normal file
71
submodules/Display/Display/ListViewItem.swift
Normal file
@ -0,0 +1,71 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public enum ListViewItemUpdateAnimation {
|
||||
case None
|
||||
case System(duration: Double)
|
||||
|
||||
public var isAnimated: Bool {
|
||||
if case .None = self {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListViewItemConfigureNodeFlags: OptionSet {
|
||||
public var rawValue: Int32
|
||||
|
||||
public init() {
|
||||
self.rawValue = 0
|
||||
}
|
||||
|
||||
public init(rawValue: Int32) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public static let preferSynchronousResourceLoading = ListViewItemConfigureNodeFlags(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
public struct ListViewItemApply {
|
||||
public let isOnScreen: Bool
|
||||
|
||||
public init(isOnScreen: Bool) {
|
||||
self.isOnScreen = isOnScreen
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ListViewItem {
|
||||
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void)
|
||||
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void)
|
||||
|
||||
var accessoryItem: ListViewAccessoryItem? { get }
|
||||
var headerAccessoryItem: ListViewAccessoryItem? { get }
|
||||
var selectable: Bool { get }
|
||||
var approximateHeight: CGFloat { get }
|
||||
|
||||
func selected(listView: ListView)
|
||||
}
|
||||
|
||||
public extension ListViewItem {
|
||||
var accessoryItem: ListViewAccessoryItem? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var headerAccessoryItem: ListViewAccessoryItem? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var selectable: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var approximateHeight: CGFloat {
|
||||
return 44.0
|
||||
}
|
||||
|
||||
func selected(listView: ListView) {
|
||||
}
|
||||
}
|
152
submodules/Display/Display/ListViewItemHeader.swift
Normal file
152
submodules/Display/Display/ListViewItemHeader.swift
Normal file
@ -0,0 +1,152 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if BUCK
|
||||
import DisplayPrivate
|
||||
#endif
|
||||
|
||||
public enum ListViewItemHeaderStickDirection {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
public typealias ListViewItemHeaderId = Int64
|
||||
|
||||
public protocol ListViewItemHeader: class {
|
||||
var id: ListViewItemHeaderId { get }
|
||||
var stickDirection: ListViewItemHeaderStickDirection { get }
|
||||
var height: CGFloat { get }
|
||||
|
||||
func node() -> ListViewItemHeaderNode
|
||||
}
|
||||
|
||||
open class ListViewItemHeaderNode: ASDisplayNode {
|
||||
private final var spring: ListViewItemSpring?
|
||||
let wantsScrollDynamics: Bool
|
||||
let isRotated: Bool
|
||||
final private(set) var internalStickLocationDistanceFactor: CGFloat = 0.0
|
||||
final var internalStickLocationDistance: CGFloat = 0.0
|
||||
private var isFlashingOnScrolling = false
|
||||
|
||||
func updateInternalStickLocationDistanceFactor(_ factor: CGFloat, animated: Bool) {
|
||||
self.internalStickLocationDistanceFactor = factor
|
||||
}
|
||||
|
||||
final func updateFlashingOnScrollingInternal(_ isFlashingOnScrolling: Bool, animated: Bool) {
|
||||
if self.isFlashingOnScrolling != isFlashingOnScrolling {
|
||||
self.isFlashingOnScrolling = isFlashingOnScrolling
|
||||
self.updateFlashingOnScrolling(isFlashingOnScrolling, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
open func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) {
|
||||
}
|
||||
|
||||
public init(layerBacked: Bool = false, dynamicBounce: Bool = false, isRotated: Bool = false, seeThrough: Bool = false) {
|
||||
self.wantsScrollDynamics = dynamicBounce
|
||||
self.isRotated = isRotated
|
||||
if dynamicBounce {
|
||||
self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85)
|
||||
}
|
||||
|
||||
if seeThrough {
|
||||
if (layerBacked) {
|
||||
super.init()
|
||||
self.setLayerBlock({
|
||||
return CASeeThroughTracingLayer()
|
||||
})
|
||||
} else {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return CASeeThroughTracingView()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
super.init()
|
||||
|
||||
self.isLayerBacked = layerBacked
|
||||
}
|
||||
}
|
||||
|
||||
open func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
|
||||
if self.spring != nil && internalStickLocationDistanceFactor.isZero {
|
||||
let bounds = self.bounds
|
||||
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + scrollingOffset), size: bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
public func animate(_ timestamp: Double) -> Bool {
|
||||
var continueAnimations = false
|
||||
|
||||
if let _ = self.spring {
|
||||
let bounds = self.bounds
|
||||
var offset = bounds.origin.y
|
||||
let currentOffset = offset
|
||||
|
||||
let frictionConstant: CGFloat = testSpringFriction
|
||||
let springConstant: CGFloat = testSpringConstant
|
||||
let time: CGFloat = 1.0 / 60.0
|
||||
|
||||
// friction force = velocity * friction constant
|
||||
let frictionForce = self.spring!.velocity * frictionConstant
|
||||
// spring force = (target point - current position) * spring constant
|
||||
let springForce = -currentOffset * springConstant
|
||||
// force = spring force - friction force
|
||||
let force = springForce - frictionForce
|
||||
|
||||
// velocity = current velocity + force * time / mass
|
||||
self.spring!.velocity = self.spring!.velocity + force * time
|
||||
// position = current position + velocity * time
|
||||
offset = currentOffset + self.spring!.velocity * time
|
||||
|
||||
offset = offset.isNaN ? 0.0 : offset
|
||||
|
||||
let epsilon: CGFloat = 0.1
|
||||
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
|
||||
offset = 0.0
|
||||
self.spring!.velocity = 0.0
|
||||
} else {
|
||||
continueAnimations = true
|
||||
}
|
||||
|
||||
if abs(offset) > 250.0 {
|
||||
offset = offset < 0.0 ? -250.0 : 250.0
|
||||
}
|
||||
|
||||
self.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: bounds.size)
|
||||
}
|
||||
|
||||
return continueAnimations
|
||||
}
|
||||
|
||||
open func animateRemoved(duration: Double) {
|
||||
self.alpha = 0.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false)
|
||||
self.layer.animateScale(from: 1.0, to: 0.2, duration: duration, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
private var cachedLayout: (CGSize, CGFloat, CGFloat)?
|
||||
|
||||
func updateLayoutInternal(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
var update = false
|
||||
if let cachedLayout = self.cachedLayout {
|
||||
if cachedLayout.0 != size || cachedLayout.1 != leftInset || cachedLayout.2 != rightInset {
|
||||
update = true
|
||||
}
|
||||
} else {
|
||||
update = true
|
||||
}
|
||||
if update {
|
||||
self.cachedLayout = (size, leftInset, rightInset)
|
||||
self.updateLayout(size: size, leftInset: leftInset, rightInset: rightInset)
|
||||
}
|
||||
}
|
||||
|
||||
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
}
|
||||
}
|
549
submodules/Display/Display/ListViewItemNode.swift
Normal file
549
submodules/Display/Display/ListViewItemNode.swift
Normal file
@ -0,0 +1,549 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
|
||||
#if BUCK
|
||||
import DisplayPrivate
|
||||
#endif
|
||||
|
||||
var testSpringFrictionLimits: (CGFloat, CGFloat) = (3.0, 60.0)
|
||||
var testSpringFriction: CGFloat = 31.8211269378662
|
||||
|
||||
var testSpringConstantLimits: (CGFloat, CGFloat) = (3.0, 450.0)
|
||||
var testSpringConstant: CGFloat = 443.704223632812
|
||||
|
||||
var testSpringResistanceFreeLimits: (CGFloat, CGFloat) = (0.05, 1.0)
|
||||
var testSpringFreeResistance: CGFloat = 0.676197171211243
|
||||
|
||||
var testSpringResistanceScrollingLimits: (CGFloat, CGFloat) = (0.1, 1.0)
|
||||
var testSpringScrollingResistance: CGFloat = 0.6721
|
||||
|
||||
struct ListViewItemSpring {
|
||||
let stiffness: CGFloat
|
||||
let damping: CGFloat
|
||||
let mass: CGFloat
|
||||
var velocity: CGFloat = 0.0
|
||||
|
||||
init(stiffness: CGFloat, damping: CGFloat, mass: CGFloat) {
|
||||
self.stiffness = stiffness
|
||||
self.damping = damping
|
||||
self.mass = mass
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListViewItemNodeLayout {
|
||||
public let contentSize: CGSize
|
||||
public let insets: UIEdgeInsets
|
||||
|
||||
public init() {
|
||||
self.contentSize = CGSize()
|
||||
self.insets = UIEdgeInsets()
|
||||
}
|
||||
|
||||
public init(contentSize: CGSize, insets: UIEdgeInsets) {
|
||||
self.contentSize = contentSize
|
||||
self.insets = insets
|
||||
}
|
||||
|
||||
public var size: CGSize {
|
||||
return CGSize(width: self.contentSize.width + self.insets.left + self.insets.right, height: self.contentSize.height + self.insets.top + self.insets.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
public enum ListViewItemNodeVisibility: Equatable {
|
||||
case none
|
||||
case visible(CGFloat)
|
||||
|
||||
public static func ==(lhs: ListViewItemNodeVisibility, rhs: ListViewItemNodeVisibility) -> Bool {
|
||||
switch lhs {
|
||||
case .none:
|
||||
if case .none = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .visible(fraction):
|
||||
if case .visible(fraction) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ListViewItemLayoutParams {
|
||||
public let width: CGFloat
|
||||
public let leftInset: CGFloat
|
||||
public let rightInset: CGFloat
|
||||
|
||||
public init(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
self.width = width
|
||||
self.leftInset = leftInset
|
||||
self.rightInset = rightInset
|
||||
}
|
||||
}
|
||||
|
||||
open class ListViewItemNode: ASDisplayNode {
|
||||
let rotated: Bool
|
||||
final var index: Int?
|
||||
|
||||
public var isHighlightedInOverlay: Bool = false
|
||||
|
||||
public private(set) var accessoryItemNode: ListViewAccessoryItemNode?
|
||||
|
||||
func setAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode?, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
self.accessoryItemNode = accessoryItemNode
|
||||
if let accessoryItemNode = accessoryItemNode {
|
||||
self.layoutAccessoryItemNode(accessoryItemNode, leftInset: leftInset, rightInset: rightInset)
|
||||
}
|
||||
}
|
||||
|
||||
final var headerAccessoryItemNode: ListViewAccessoryItemNode? {
|
||||
didSet {
|
||||
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
|
||||
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final var spring: ListViewItemSpring?
|
||||
private final var animations: [(String, ListViewAnimation)] = []
|
||||
|
||||
final let wantsScrollDynamics: Bool
|
||||
|
||||
public final var wantsTrailingItemSpaceUpdates: Bool = false
|
||||
|
||||
public final var scrollPositioningInsets: UIEdgeInsets = UIEdgeInsets()
|
||||
|
||||
public final var canBeUsedAsScrollToItemAnchor: Bool = true
|
||||
|
||||
open var visibility: ListViewItemNodeVisibility = .none
|
||||
|
||||
open var canBeSelected: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
open var canBeLongTapped: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open var preventsTouchesToOtherItems: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func touchesToOtherItemsPrevented() {
|
||||
|
||||
}
|
||||
|
||||
open func tapped() {
|
||||
}
|
||||
|
||||
open func longTapped() {
|
||||
}
|
||||
|
||||
public final var insets: UIEdgeInsets = UIEdgeInsets() {
|
||||
didSet {
|
||||
let effectiveInsets = self.insets
|
||||
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: self.contentSize.width, height: self.contentSize.height + effectiveInsets.top + effectiveInsets.bottom))
|
||||
let bounds = self.bounds
|
||||
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
private final var _contentSize: CGSize = CGSize()
|
||||
public final var contentSize: CGSize {
|
||||
get {
|
||||
return self._contentSize
|
||||
} set(value) {
|
||||
let effectiveInsets = self.insets
|
||||
self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: value.width, height: value.height + effectiveInsets.top + effectiveInsets.bottom))
|
||||
}
|
||||
}
|
||||
|
||||
private var contentOffset: CGFloat = 0.0 {
|
||||
didSet {
|
||||
let effectiveInsets = self.insets
|
||||
let bounds = self.bounds
|
||||
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
public var transitionOffset: CGFloat = 0.0 {
|
||||
didSet {
|
||||
let effectiveInsets = self.insets
|
||||
let bounds = self.bounds
|
||||
self.bounds = CGRect(origin: CGPoint(x: bounds.origin.x, y: -effectiveInsets.top + self.contentOffset + self.transitionOffset), size: bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
public var layout: ListViewItemNodeLayout {
|
||||
var insets = self.insets
|
||||
var contentSize = self.contentSize
|
||||
|
||||
if let animation = self.animationForKey("insets") {
|
||||
insets = animation.to as! UIEdgeInsets
|
||||
}
|
||||
|
||||
if let animation = self.animationForKey("apparentHeight") {
|
||||
contentSize.height = (animation.to as! CGFloat) - insets.top - insets.bottom
|
||||
}
|
||||
|
||||
return ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
}
|
||||
|
||||
public var displayResourcesReady: Signal<Void, NoError> {
|
||||
return .complete()
|
||||
}
|
||||
|
||||
public init(layerBacked: Bool, dynamicBounce: Bool = true, rotated: Bool = false, seeThrough: Bool = false) {
|
||||
if dynamicBounce {
|
||||
self.spring = ListViewItemSpring(stiffness: -280.0, damping: -24.0, mass: 0.85)
|
||||
}
|
||||
self.wantsScrollDynamics = dynamicBounce
|
||||
|
||||
self.rotated = rotated
|
||||
|
||||
if seeThrough {
|
||||
if (layerBacked) {
|
||||
super.init()
|
||||
self.setLayerBlock({
|
||||
return CASeeThroughTracingLayer()
|
||||
})
|
||||
} else {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
return CASeeThroughTracingView()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
super.init()
|
||||
|
||||
self.isLayerBacked = layerBacked
|
||||
}
|
||||
}
|
||||
|
||||
var apparentHeight: CGFloat = 0.0
|
||||
private var _bounds: CGRect = CGRect()
|
||||
private var _position: CGPoint = CGPoint()
|
||||
|
||||
open override var frame: CGRect {
|
||||
get {
|
||||
return CGRect(origin: CGPoint(x: self._position.x - self._bounds.width / 2.0, y: self._position.y - self._bounds.height / 2.0), size: self._bounds.size)
|
||||
} set(value) {
|
||||
let previousSize = self._bounds.size
|
||||
|
||||
super.frame = value
|
||||
self._bounds.size = value.size
|
||||
self._position = CGPoint(x: value.midX, y: value.midY)
|
||||
let effectiveInsets = self.insets
|
||||
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
|
||||
|
||||
if previousSize != value.size {
|
||||
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
|
||||
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open override var bounds: CGRect {
|
||||
get {
|
||||
return self._bounds
|
||||
} set(value) {
|
||||
let previousSize = self._bounds.size
|
||||
|
||||
super.bounds = value
|
||||
self._bounds = value
|
||||
let effectiveInsets = self.insets
|
||||
self._contentSize = CGSize(width: value.size.width, height: value.size.height - effectiveInsets.top - effectiveInsets.bottom)
|
||||
|
||||
if previousSize != value.size {
|
||||
if let headerAccessoryItemNode = self.headerAccessoryItemNode {
|
||||
self.layoutHeaderAccessoryItemNode(headerAccessoryItemNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var contentBounds: CGRect {
|
||||
let bounds = self.bounds
|
||||
let effectiveInsets = self.insets
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: bounds.origin.y + effectiveInsets.top), size: CGSize(width: bounds.size.width, height: bounds.size.height - effectiveInsets.top - effectiveInsets.bottom))
|
||||
}
|
||||
|
||||
open override var position: CGPoint {
|
||||
get {
|
||||
return self._position
|
||||
} set(value) {
|
||||
super.position = value
|
||||
self._position = value
|
||||
}
|
||||
}
|
||||
|
||||
public final var apparentFrame: CGRect {
|
||||
var frame = self.frame
|
||||
frame.size.height = self.apparentHeight
|
||||
return frame
|
||||
}
|
||||
|
||||
public final var apparentContentFrame: CGRect {
|
||||
var frame = self.frame
|
||||
let insets = self.insets
|
||||
frame.origin.y += insets.top
|
||||
frame.size.height = self.apparentHeight - insets.top - insets.bottom
|
||||
return frame
|
||||
}
|
||||
|
||||
public final var apparentBounds: CGRect {
|
||||
var bounds = self.bounds
|
||||
bounds.size.height = self.apparentHeight
|
||||
return bounds
|
||||
}
|
||||
|
||||
open func layoutAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode, leftInset: CGFloat, rightInset: CGFloat) {
|
||||
}
|
||||
|
||||
open func layoutHeaderAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
|
||||
}
|
||||
|
||||
open func reuse() {
|
||||
}
|
||||
|
||||
final func addScrollingOffset(_ scrollingOffset: CGFloat) {
|
||||
if self.spring != nil {
|
||||
self.contentOffset += scrollingOffset
|
||||
}
|
||||
}
|
||||
|
||||
func initializeDynamicsFromSibling(_ itemView: ListViewItemNode, additionalOffset: CGFloat) {
|
||||
if let itemViewSpring = itemView.spring {
|
||||
self.contentOffset = itemView.contentOffset + additionalOffset
|
||||
self.spring?.velocity = itemViewSpring.velocity
|
||||
}
|
||||
}
|
||||
|
||||
public func animate(_ timestamp: Double) -> Bool {
|
||||
var continueAnimations = false
|
||||
|
||||
if let _ = self.spring {
|
||||
var offset = self.contentOffset
|
||||
|
||||
let frictionConstant: CGFloat = testSpringFriction
|
||||
let springConstant: CGFloat = testSpringConstant
|
||||
let time: CGFloat = 1.0 / 60.0
|
||||
|
||||
// friction force = velocity * friction constant
|
||||
let frictionForce = self.spring!.velocity * frictionConstant
|
||||
// spring force = (target point - current position) * spring constant
|
||||
let springForce = -self.contentOffset * springConstant
|
||||
// force = spring force - friction force
|
||||
let force = springForce - frictionForce
|
||||
|
||||
// velocity = current velocity + force * time / mass
|
||||
self.spring!.velocity = self.spring!.velocity + force * time
|
||||
// position = current position + velocity * time
|
||||
offset = self.contentOffset + self.spring!.velocity * time
|
||||
|
||||
offset = offset.isNaN ? 0.0 : offset
|
||||
|
||||
let epsilon: CGFloat = 0.1
|
||||
if abs(offset) < epsilon && abs(self.spring!.velocity) < epsilon {
|
||||
offset = 0.0
|
||||
self.spring!.velocity = 0.0
|
||||
} else {
|
||||
continueAnimations = true
|
||||
}
|
||||
|
||||
if abs(offset) > 250.0 {
|
||||
offset = offset < 0.0 ? -250.0 : 250.0
|
||||
}
|
||||
self.contentOffset = offset
|
||||
}
|
||||
|
||||
var i = 0
|
||||
var animationCount = self.animations.count
|
||||
while i < animationCount {
|
||||
let (_, animation) = self.animations[i]
|
||||
animation.applyAt(timestamp)
|
||||
|
||||
if animation.completeAt(timestamp) {
|
||||
animations.remove(at: i)
|
||||
animationCount -= 1
|
||||
i -= 1
|
||||
} else {
|
||||
continueAnimations = true
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
if let accessoryItemNode = self.accessoryItemNode {
|
||||
if (accessoryItemNode.animate(timestamp)) {
|
||||
continueAnimations = true
|
||||
}
|
||||
}
|
||||
|
||||
return continueAnimations
|
||||
}
|
||||
|
||||
open func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
}
|
||||
|
||||
public func animationForKey(_ key: String) -> ListViewAnimation? {
|
||||
for (animationKey, animation) in self.animations {
|
||||
if animationKey == key {
|
||||
return animation
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public final func setAnimationForKey(_ key: String, animation: ListViewAnimation?) {
|
||||
for i in 0 ..< self.animations.count {
|
||||
let (currentKey, currentAnimation) = self.animations[i]
|
||||
if currentKey == key {
|
||||
self.animations.remove(at: i)
|
||||
currentAnimation.cancel()
|
||||
break
|
||||
}
|
||||
}
|
||||
if let animation = animation {
|
||||
self.animations.append((key, animation))
|
||||
}
|
||||
}
|
||||
|
||||
public final func removeAllAnimations() {
|
||||
let previousAnimations = self.animations
|
||||
self.animations.removeAll()
|
||||
|
||||
for (_, animation) in previousAnimations {
|
||||
animation.cancel()
|
||||
}
|
||||
|
||||
self.accessoryItemNode?.removeAllAnimations()
|
||||
}
|
||||
|
||||
public func addInsetsAnimationToValue(_ value: UIEdgeInsets, duration: Double, beginAt: Double) {
|
||||
let animation = ListViewAnimation(from: self.insets, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
|
||||
if let strongSelf = self {
|
||||
strongSelf.insets = currentValue
|
||||
}
|
||||
})
|
||||
self.setAnimationForKey("insets", animation: animation)
|
||||
}
|
||||
|
||||
public func addHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
|
||||
let animation = ListViewAnimation(from: self.bounds.height, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in
|
||||
if let strongSelf = self {
|
||||
let frame = strongSelf.frame
|
||||
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
|
||||
if let update = update {
|
||||
update(progress, currentValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
self.setAnimationForKey("height", animation: animation)
|
||||
}
|
||||
|
||||
func copyHeightAndApparentHeightAnimations(to otherNode: ListViewItemNode) {
|
||||
if let animation = self.animationForKey("apparentHeight") {
|
||||
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
|
||||
if let strongSelf = otherNode {
|
||||
let frame = strongSelf.frame
|
||||
strongSelf.frame = CGRect(origin: frame.origin, size: CGSize(width: frame.width, height: currentValue))
|
||||
}
|
||||
})
|
||||
otherNode.setAnimationForKey("height", animation: updatedAnimation)
|
||||
}
|
||||
|
||||
if let animation = self.animationForKey("apparentHeight") {
|
||||
let updatedAnimation = ListViewAnimation(copying: animation, update: { [weak otherNode] (progress: CGFloat, currentValue: CGFloat) -> Void in
|
||||
if let strongSelf = otherNode {
|
||||
strongSelf.apparentHeight = currentValue
|
||||
}
|
||||
})
|
||||
otherNode.setAnimationForKey("apparentHeight", animation: updatedAnimation)
|
||||
}
|
||||
}
|
||||
|
||||
public func addApparentHeightAnimation(_ value: CGFloat, duration: Double, beginAt: Double, update: ((CGFloat, CGFloat) -> Void)? = nil) {
|
||||
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] progress, currentValue in
|
||||
if let strongSelf = self {
|
||||
strongSelf.apparentHeight = currentValue
|
||||
if let update = update {
|
||||
update(progress, currentValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
self.setAnimationForKey("apparentHeight", animation: animation)
|
||||
}
|
||||
|
||||
public func modifyApparentHeightAnimation(_ value: CGFloat, beginAt: Double) {
|
||||
if let previousAnimation = self.animationForKey("apparentHeight") {
|
||||
var duration = previousAnimation.startTime + previousAnimation.duration - beginAt
|
||||
if abs(self.apparentHeight - value) < CGFloat.ulpOfOne {
|
||||
duration = 0.0
|
||||
}
|
||||
|
||||
let animation = ListViewAnimation(from: self.apparentHeight, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
|
||||
if let strongSelf = self {
|
||||
strongSelf.apparentHeight = currentValue
|
||||
}
|
||||
})
|
||||
|
||||
self.setAnimationForKey("apparentHeight", animation: animation)
|
||||
}
|
||||
}
|
||||
|
||||
public func removeApparentHeightAnimation() {
|
||||
self.setAnimationForKey("apparentHeight", animation: nil)
|
||||
}
|
||||
|
||||
public func addTransitionOffsetAnimation(_ value: CGFloat, duration: Double, beginAt: Double) {
|
||||
let animation = ListViewAnimation(from: self.transitionOffset, to: value, duration: duration, curve: listViewAnimationCurveSystem, beginAt: beginAt, update: { [weak self] _, currentValue in
|
||||
if let strongSelf = self {
|
||||
strongSelf.transitionOffset = currentValue
|
||||
}
|
||||
})
|
||||
self.setAnimationForKey("transitionOffset", animation: animation)
|
||||
}
|
||||
|
||||
open func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
||||
}
|
||||
|
||||
open func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
}
|
||||
|
||||
open func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
}
|
||||
|
||||
open func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
|
||||
}
|
||||
|
||||
open func isReorderable(at point: CGPoint) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func animateFrameTransition(_ progress: CGFloat, _ currentValue: CGFloat) {
|
||||
|
||||
}
|
||||
|
||||
open func shouldAnimateHorizontalFrameTransition() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
open func header() -> ListViewItemHeader? {
|
||||
return nil
|
||||
}
|
||||
|
||||
open func updateTrailingItemSpace(_ height: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
}
|
||||
|
||||
override open func accessibilityElementDidBecomeFocused() {
|
||||
(self.supernode as? ListView)?.ensureItemNodeVisible(self, animated: false, overflow: 22.0)
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class ListViewOverscrollBackgroundNode: ASDisplayNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
|
||||
var color: UIColor {
|
||||
didSet {
|
||||
self.backgroundNode.backgroundColor = color
|
||||
}
|
||||
}
|
||||
|
||||
init(color: UIColor) {
|
||||
self.color = color
|
||||
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.backgroundColor = color
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
}
|
||||
|
||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class ListViewReorderingGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> Bool
|
||||
private let ended: () -> Void
|
||||
private let moved: (CGFloat) -> Void
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
|
||||
init(shouldBegin: @escaping (CGPoint) -> Bool, ended: @escaping () -> Void, moved: @escaping (CGFloat) -> Void) {
|
||||
self.shouldBegin = shouldBegin
|
||||
self.ended = ended
|
||||
self.moved = moved
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
}
|
||||
|
||||
override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.initialLocation = nil
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.state == .possible {
|
||||
if let location = touches.first?.location(in: self.view), self.shouldBegin(location) {
|
||||
self.initialLocation = location
|
||||
self.state = .began
|
||||
} else {
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||
self.state = .changed
|
||||
let offset = location.y - initialLocation.y
|
||||
self.moved(offset)
|
||||
}
|
||||
}
|
||||
}
|
50
submodules/Display/Display/ListViewReorderingItemNode.swift
Normal file
50
submodules/Display/Display/ListViewReorderingItemNode.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class ListViewReorderingItemNode: ASDisplayNode {
|
||||
weak var itemNode: ListViewItemNode?
|
||||
|
||||
var currentState: (Int, Int)?
|
||||
|
||||
private let copyView: UIView?
|
||||
private let initialLocation: CGPoint
|
||||
|
||||
init(itemNode: ListViewItemNode, initialLocation: CGPoint) {
|
||||
self.itemNode = itemNode
|
||||
self.copyView = itemNode.view.snapshotView(afterScreenUpdates: false)
|
||||
self.initialLocation = initialLocation
|
||||
|
||||
super.init()
|
||||
|
||||
if let copyView = self.copyView {
|
||||
self.view.addSubview(copyView)
|
||||
copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: copyView.bounds.size)
|
||||
copyView.bounds = itemNode.bounds
|
||||
}
|
||||
}
|
||||
|
||||
func updateOffset(offset: CGFloat) {
|
||||
if let copyView = self.copyView {
|
||||
copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y + offset), size: copyView.bounds.size)
|
||||
}
|
||||
}
|
||||
|
||||
func currentOffset() -> CGFloat? {
|
||||
if let copyView = self.copyView {
|
||||
return copyView.center.y
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func animateCompletion(completion: @escaping () -> Void) {
|
||||
if let copyView = self.copyView, let itemNode = self.itemNode {
|
||||
itemNode.isHidden = false
|
||||
itemNode.transitionOffset = itemNode.apparentFrame.midY - copyView.frame.midY
|
||||
itemNode.addTransitionOffsetAnimation(0.0, duration: 0.2, beginAt: CACurrentMediaTime())
|
||||
completion()
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
31
submodules/Display/Display/ListViewScroller.swift
Normal file
31
submodules/Display/Display/ListViewScroller.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import UIKit
|
||||
|
||||
class ListViewScroller: UIScrollView, UIGestureRecognizerDelegate {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
#if os(iOS)
|
||||
self.scrollsToTop = false
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if otherGestureRecognizer is ListViewTapGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public final class ListViewTapGestureRecognizer: UITapGestureRecognizer {
|
||||
|
||||
}
|
4
submodules/Display/Display/ListViewTempItemNode.swift
Normal file
4
submodules/Display/Display/ListViewTempItemNode.swift
Normal file
@ -0,0 +1,4 @@
|
||||
import Foundation
|
||||
|
||||
final class ListViewTempItemNode: ListViewItemNode {
|
||||
}
|
65
submodules/Display/Display/ListViewTransactionQueue.swift
Normal file
65
submodules/Display/Display/ListViewTransactionQueue.swift
Normal file
@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
|
||||
public typealias ListViewTransaction = (@escaping () -> Void) -> Void
|
||||
|
||||
public final class ListViewTransactionQueue {
|
||||
private var transactions: [ListViewTransaction] = []
|
||||
public final var transactionCompleted: () -> Void = { }
|
||||
|
||||
public init() {
|
||||
}
|
||||
|
||||
public func addTransaction(_ transaction: @escaping ListViewTransaction) {
|
||||
precondition(Thread.isMainThread)
|
||||
let beginTransaction = self.transactions.count == 0
|
||||
self.transactions.append(transaction)
|
||||
|
||||
if beginTransaction {
|
||||
transaction({ [weak self] in
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
if Thread.isMainThread {
|
||||
if let strongSelf = self {
|
||||
strongSelf.endTransaction()
|
||||
}
|
||||
} else {
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func endTransaction() {
|
||||
precondition(Thread.isMainThread)
|
||||
Queue.mainQueue().async {
|
||||
self.transactionCompleted()
|
||||
if !self.transactions.isEmpty {
|
||||
let _ = self.transactions.removeFirst()
|
||||
}
|
||||
|
||||
if let nextTransaction = self.transactions.first {
|
||||
nextTransaction({ [weak self] in
|
||||
precondition(Thread.isMainThread)
|
||||
|
||||
if Thread.isMainThread {
|
||||
if let strongSelf = self {
|
||||
strongSelf.endTransaction()
|
||||
}
|
||||
} else {
|
||||
Queue.mainQueue().async {
|
||||
if let strongSelf = self {
|
||||
strongSelf.endTransaction()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class MinimizeKeyboardGestureRecognizer: UISwipeGestureRecognizer, UIGestureRecognizerDelegate {
|
||||
override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.cancelsTouchesInView = false
|
||||
self.delaysTouchesBegan = false
|
||||
self.delaysTouchesEnded = false
|
||||
self.delegate = self
|
||||
|
||||
self.direction = [.left, .right]
|
||||
self.numberOfTouchesRequired = 2
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
9
submodules/Display/Display/NSBag.h
Normal file
9
submodules/Display/Display/NSBag.h
Normal file
@ -0,0 +1,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSBag : NSObject
|
||||
|
||||
- (NSInteger)addItem:(id)item;
|
||||
- (void)enumerateItems:(void (^)(id))block;
|
||||
- (void)removeItem:(NSInteger)key;
|
||||
|
||||
@end
|
64
submodules/Display/Display/NSBag.m
Normal file
64
submodules/Display/Display/NSBag.m
Normal file
@ -0,0 +1,64 @@
|
||||
#import "NSBag.h"
|
||||
|
||||
@interface NSBag ()
|
||||
{
|
||||
NSInteger _nextKey;
|
||||
NSMutableArray *_items;
|
||||
NSMutableArray *_itemKeys;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSBag
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self != nil)
|
||||
{
|
||||
_items = [[NSMutableArray alloc] init];
|
||||
_itemKeys = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSInteger)addItem:(id)item
|
||||
{
|
||||
if (item == nil)
|
||||
return -1;
|
||||
|
||||
NSInteger key = _nextKey;
|
||||
[_items addObject:item];
|
||||
[_itemKeys addObject:@(key)];
|
||||
_nextKey++;
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
- (void)enumerateItems:(void (^)(id))block
|
||||
{
|
||||
if (block)
|
||||
{
|
||||
for (id item in _items)
|
||||
{
|
||||
block(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeItem:(NSInteger)key
|
||||
{
|
||||
NSUInteger index = 0;
|
||||
for (NSNumber *itemKey in _itemKeys)
|
||||
{
|
||||
if ([itemKey integerValue] == key)
|
||||
{
|
||||
[_items removeObjectAtIndex:index];
|
||||
[_itemKeys removeObjectAtIndex:index];
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
9
submodules/Display/Display/NSWeakReference.h
Normal file
9
submodules/Display/Display/NSWeakReference.h
Normal file
@ -0,0 +1,9 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSWeakReference : NSObject
|
||||
|
||||
@property (nonatomic, weak) id value;
|
||||
|
||||
- (instancetype)initWithValue:(id)value;
|
||||
|
||||
@end
|
13
submodules/Display/Display/NSWeakReference.m
Normal file
13
submodules/Display/Display/NSWeakReference.m
Normal file
@ -0,0 +1,13 @@
|
||||
#import "NSWeakReference.h"
|
||||
|
||||
@implementation NSWeakReference
|
||||
|
||||
- (instancetype)initWithValue:(id)value {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
self.value = value;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
417
submodules/Display/Display/NativeWindowHostView.swift
Normal file
417
submodules/Display/Display/NativeWindowHostView.swift
Normal file
@ -0,0 +1,417 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
|
||||
private let orientationChangeDuration: Double = UIDevice.current.userInterfaceIdiom == .pad ? 0.4 : 0.3
|
||||
|
||||
private let defaultOrientations: UIInterfaceOrientationMask = {
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
return .all
|
||||
} else {
|
||||
return .allButUpsideDown
|
||||
}
|
||||
}()
|
||||
|
||||
public final class PreviewingHostViewDelegate {
|
||||
public let controllerForLocation: (UIView, CGPoint) -> (UIViewController, CGRect)?
|
||||
public let commitController: (UIViewController) -> Void
|
||||
|
||||
public init(controllerForLocation: @escaping (UIView, CGPoint) -> (UIViewController, CGRect)?, commitController: @escaping (UIViewController) -> Void) {
|
||||
self.controllerForLocation = controllerForLocation
|
||||
self.commitController = commitController
|
||||
}
|
||||
}
|
||||
|
||||
public protocol PreviewingHostView {
|
||||
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
|
||||
var previewingDelegate: PreviewingHostViewDelegate? { get }
|
||||
}
|
||||
|
||||
private func tracePreviewingHostView(view: UIView, point: CGPoint) -> (UIView & PreviewingHostView, CGPoint)? {
|
||||
if let view = view as? UIView & PreviewingHostView {
|
||||
return (view, point)
|
||||
}
|
||||
if let superview = view.superview {
|
||||
if let result = tracePreviewingHostView(view: superview, point: superview.convert(point, from: view)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private final class WindowRootViewControllerView: UIView {
|
||||
override var frame: CGRect {
|
||||
get {
|
||||
return super.frame
|
||||
} set(value) {
|
||||
var value = value
|
||||
value.size.height += value.minY
|
||||
value.origin.y = 0.0
|
||||
super.frame = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class WindowRootViewController: UIViewController, UIViewControllerPreviewingDelegate {
|
||||
private var voiceOverStatusObserver: AnyObject?
|
||||
private var registeredForPreviewing = false
|
||||
|
||||
var presentController: ((UIViewController, PresentationSurfaceLevel, Bool, (() -> Void)?) -> Void)?
|
||||
var transitionToSize: ((CGSize, Double) -> Void)?
|
||||
|
||||
var orientations: UIInterfaceOrientationMask = defaultOrientations {
|
||||
didSet {
|
||||
if oldValue != self.orientations {
|
||||
if self.orientations == .portrait {
|
||||
if UIDevice.current.orientation != .portrait {
|
||||
let value = UIInterfaceOrientation.portrait.rawValue
|
||||
UIDevice.current.setValue(value, forKey: "orientation")
|
||||
}
|
||||
} else {
|
||||
UIViewController.attemptRotationToDeviceOrientation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var gestureEdges: UIRectEdge = [] {
|
||||
didSet {
|
||||
if oldValue != self.gestureEdges {
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var preferNavigationUIHidden: Bool = false {
|
||||
didSet {
|
||||
if oldValue != self.preferNavigationUIHidden {
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.setNeedsUpdateOfHomeIndicatorAutoHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .default
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return orientations
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.extendedLayoutIncludesOpaqueBars = true
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, *) {
|
||||
self.voiceOverStatusObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.UIAccessibilityVoiceOverStatusDidChange, object: nil, queue: OperationQueue.main, using: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updatePreviewingRegistration()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let voiceOverStatusObserver = self.voiceOverStatusObserver {
|
||||
NotificationCenter.default.removeObserver(voiceOverStatusObserver)
|
||||
}
|
||||
}
|
||||
|
||||
override func preferredScreenEdgesDeferringSystemGestures() -> UIRectEdge {
|
||||
return self.gestureEdges
|
||||
}
|
||||
|
||||
override func prefersHomeIndicatorAutoHidden() -> Bool {
|
||||
return self.preferNavigationUIHidden
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
UIView.performWithoutAnimation {
|
||||
self.transitionToSize?(size, coordinator.transitionDuration)
|
||||
}
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = WindowRootViewControllerView()
|
||||
self.view.isOpaque = false
|
||||
self.view.backgroundColor = nil
|
||||
|
||||
self.updatePreviewingRegistration()
|
||||
}
|
||||
|
||||
private var previewingContext: AnyObject?
|
||||
|
||||
private func updatePreviewingRegistration() {
|
||||
var shouldRegister = false
|
||||
|
||||
var isVoiceOverRunning = false
|
||||
if #available(iOSApplicationExtension 10.0, *) {
|
||||
isVoiceOverRunning = UIAccessibility.isVoiceOverRunning
|
||||
}
|
||||
if !isVoiceOverRunning {
|
||||
shouldRegister = true
|
||||
}
|
||||
|
||||
if shouldRegister != self.registeredForPreviewing {
|
||||
self.registeredForPreviewing = shouldRegister
|
||||
if shouldRegister {
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
self.previewingContext = self.registerForPreviewing(with: self, sourceView: self.view)
|
||||
}
|
||||
} else if let previewingContext = self.previewingContext {
|
||||
self.previewingContext = nil
|
||||
if let previewingContext = previewingContext as? UIViewControllerPreviewing {
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
self.unregisterForPreviewing(withContext: previewingContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private weak var previousPreviewingHostView: (UIView & PreviewingHostView)?
|
||||
|
||||
public func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
|
||||
if UIAccessibility.isVoiceOverRunning {
|
||||
return nil
|
||||
}
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
guard let result = self.view.hitTest(location, with: nil) else {
|
||||
return nil
|
||||
}
|
||||
if let (result, resultPoint) = tracePreviewingHostView(view: result, point: self.view.convert(location, to: result)), let delegate = result.previewingDelegate {
|
||||
self.previousPreviewingHostView = result
|
||||
if let (controller, rect) = delegate.controllerForLocation(previewingContext.sourceView, resultPoint) {
|
||||
previewingContext.sourceRect = rect
|
||||
return controller
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) {
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
if let previousPreviewingHostView = self.previousPreviewingHostView, let delegate = previousPreviewingHostView.previewingDelegate {
|
||||
delegate.commitController(viewControllerToCommit)
|
||||
}
|
||||
self.previousPreviewingHostView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class NativeWindow: UIWindow, WindowHost {
|
||||
var updateSize: ((CGSize) -> Void)?
|
||||
var layoutSubviewsEvent: (() -> Void)?
|
||||
var updateIsUpdatingOrientationLayout: ((Bool) -> Void)?
|
||||
var updateToInterfaceOrientation: ((UIInterfaceOrientation) -> Void)?
|
||||
var presentController: ((ContainableController, PresentationSurfaceLevel, Bool, @escaping () -> Void) -> Void)?
|
||||
var presentControllerInGlobalOverlay: ((_ controller: ContainableController) -> Void)?
|
||||
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
|
||||
var presentNativeImpl: ((UIViewController) -> Void)?
|
||||
var invalidateDeferScreenEdgeGestureImpl: (() -> Void)?
|
||||
var invalidatePreferNavigationUIHiddenImpl: (() -> Void)?
|
||||
var cancelInteractiveKeyboardGesturesImpl: (() -> Void)?
|
||||
var forEachControllerImpl: (((ContainableController) -> Void) -> Void)?
|
||||
var getAccessibilityElementsImpl: (() -> [Any]?)?
|
||||
|
||||
override var frame: CGRect {
|
||||
get {
|
||||
return super.frame
|
||||
} set(value) {
|
||||
let sizeUpdated = super.frame.size != value.size
|
||||
|
||||
var frameTransition: ContainedViewLayoutTransition = .immediate
|
||||
if #available(iOSApplicationExtension 9.0, *) {
|
||||
let duration = UIView.inheritedAnimationDuration
|
||||
if !duration.isZero {
|
||||
frameTransition = .animated(duration: duration, curve: .easeInOut)
|
||||
}
|
||||
}
|
||||
if sizeUpdated, case let .animated(duration, curve) = frameTransition {
|
||||
let previousFrame = super.frame
|
||||
super.frame = value
|
||||
self.layer.animateFrame(from: previousFrame, to: value, duration: duration, timingFunction: curve.timingFunction)
|
||||
} else {
|
||||
super.frame = value
|
||||
}
|
||||
|
||||
if sizeUpdated {
|
||||
self.updateSize?(value.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var bounds: CGRect {
|
||||
get {
|
||||
return super.bounds
|
||||
}
|
||||
set(value) {
|
||||
let sizeUpdated = super.bounds.size != value.size
|
||||
super.bounds = value
|
||||
|
||||
if sizeUpdated {
|
||||
self.updateSize?(value.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
if let gestureRecognizers = self.gestureRecognizers {
|
||||
for recognizer in gestureRecognizers {
|
||||
recognizer.delaysTouchesBegan = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.layoutSubviewsEvent?()
|
||||
}
|
||||
|
||||
override func _update(toInterfaceOrientation arg1: Int32, duration arg2: Double, force arg3: Bool) {
|
||||
self.updateIsUpdatingOrientationLayout?(true)
|
||||
super._update(toInterfaceOrientation: arg1, duration: arg2, force: arg3)
|
||||
self.updateIsUpdatingOrientationLayout?(false)
|
||||
|
||||
let orientation = UIInterfaceOrientation(rawValue: Int(arg1)) ?? .unknown
|
||||
self.updateToInterfaceOrientation?(orientation)
|
||||
}
|
||||
|
||||
func present(_ controller: ContainableController, on level: PresentationSurfaceLevel, blockInteraction: Bool, completion: @escaping () -> Void) {
|
||||
self.presentController?(controller, level, blockInteraction, completion)
|
||||
}
|
||||
|
||||
func presentInGlobalOverlay(_ controller: ContainableController) {
|
||||
self.presentControllerInGlobalOverlay?(controller)
|
||||
}
|
||||
|
||||
func presentNative(_ controller: UIViewController) {
|
||||
self.presentNativeImpl?(controller)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
return self.hitTestImpl?(point, event)
|
||||
}
|
||||
|
||||
func invalidateDeferScreenEdgeGestures() {
|
||||
self.invalidateDeferScreenEdgeGestureImpl?()
|
||||
}
|
||||
|
||||
func invalidatePreferNavigationUIHidden() {
|
||||
self.invalidatePreferNavigationUIHiddenImpl?()
|
||||
}
|
||||
|
||||
func cancelInteractiveKeyboardGestures() {
|
||||
self.cancelInteractiveKeyboardGesturesImpl?()
|
||||
}
|
||||
|
||||
func forEachController(_ f: (ContainableController) -> Void) {
|
||||
self.forEachControllerImpl?(f)
|
||||
}
|
||||
}
|
||||
|
||||
public func nativeWindowHostView() -> (UIWindow & WindowHost, WindowHostView) {
|
||||
let window = NativeWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
let rootViewController = WindowRootViewController()
|
||||
window.rootViewController = rootViewController
|
||||
rootViewController.viewWillAppear(false)
|
||||
rootViewController.view.frame = CGRect(origin: CGPoint(), size: window.bounds.size)
|
||||
rootViewController.viewDidAppear(false)
|
||||
|
||||
let hostView = WindowHostView(containerView: rootViewController.view, eventView: window, isRotating: {
|
||||
return window.isRotating()
|
||||
}, updateSupportedInterfaceOrientations: { orientations in
|
||||
rootViewController.orientations = orientations
|
||||
}, updateDeferScreenEdgeGestures: { edges in
|
||||
rootViewController.gestureEdges = edges
|
||||
}, updatePreferNavigationUIHidden: { value in
|
||||
rootViewController.preferNavigationUIHidden = value
|
||||
})
|
||||
|
||||
rootViewController.transitionToSize = { [weak hostView] size, duration in
|
||||
hostView?.updateSize?(size, duration)
|
||||
}
|
||||
|
||||
window.updateSize = { _ in
|
||||
}
|
||||
|
||||
window.layoutSubviewsEvent = { [weak hostView] in
|
||||
hostView?.layoutSubviews?()
|
||||
}
|
||||
|
||||
window.updateIsUpdatingOrientationLayout = { [weak hostView] value in
|
||||
hostView?.isUpdatingOrientationLayout = value
|
||||
}
|
||||
|
||||
window.updateToInterfaceOrientation = { [weak hostView] orientation in
|
||||
hostView?.updateToInterfaceOrientation?(orientation)
|
||||
}
|
||||
|
||||
window.presentController = { [weak hostView] controller, level, blockInteraction, completion in
|
||||
hostView?.present?(controller, level, blockInteraction, completion)
|
||||
}
|
||||
|
||||
window.presentControllerInGlobalOverlay = { [weak hostView] controller in
|
||||
hostView?.presentInGlobalOverlay?(controller)
|
||||
}
|
||||
|
||||
window.presentNativeImpl = { [weak hostView] controller in
|
||||
hostView?.presentNative?(controller)
|
||||
}
|
||||
|
||||
window.hitTestImpl = { [weak hostView] point, event in
|
||||
return hostView?.hitTest?(point, event)
|
||||
}
|
||||
|
||||
window.invalidateDeferScreenEdgeGestureImpl = { [weak hostView] in
|
||||
return hostView?.invalidateDeferScreenEdgeGesture?()
|
||||
}
|
||||
|
||||
window.invalidatePreferNavigationUIHiddenImpl = { [weak hostView] in
|
||||
return hostView?.invalidatePreferNavigationUIHidden?()
|
||||
}
|
||||
|
||||
window.cancelInteractiveKeyboardGesturesImpl = { [weak hostView] in
|
||||
hostView?.cancelInteractiveKeyboardGestures?()
|
||||
}
|
||||
|
||||
window.forEachControllerImpl = { [weak hostView] f in
|
||||
hostView?.forEachController?(f)
|
||||
}
|
||||
|
||||
window.getAccessibilityElementsImpl = { [weak hostView] in
|
||||
return hostView?.getAccessibilityElements?()
|
||||
}
|
||||
|
||||
rootViewController.presentController = { [weak hostView] controller, level, animated, completion in
|
||||
if let hostView = hostView {
|
||||
hostView.present?(LegacyPresentedController(legacyController: controller, presentation: .custom), level, false, completion ?? {})
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
return (window, hostView)
|
||||
}
|
BIN
submodules/Display/Display/NavigationBackArrowLight@2x.png
Normal file
BIN
submodules/Display/Display/NavigationBackArrowLight@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
149
submodules/Display/Display/NavigationBackButtonNode.swift
Normal file
149
submodules/Display/Display/NavigationBackButtonNode.swift
Normal file
@ -0,0 +1,149 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public class NavigationBackButtonNode: ASControlNode {
|
||||
private func fontForCurrentState() -> UIFont {
|
||||
return UIFont.systemFont(ofSize: 17.0)
|
||||
}
|
||||
|
||||
private func attributesForCurrentState() -> [NSAttributedStringKey : AnyObject] {
|
||||
return [
|
||||
NSAttributedStringKey.font: self.fontForCurrentState(),
|
||||
NSAttributedStringKey.foregroundColor: self.isEnabled ? self.color : self.disabledColor
|
||||
]
|
||||
}
|
||||
|
||||
let arrow: ASDisplayNode
|
||||
let label: ASTextNode
|
||||
|
||||
private let arrowSpacing: CGFloat = 4.0
|
||||
|
||||
private var _text: String = ""
|
||||
public var text: String {
|
||||
get {
|
||||
return self._text
|
||||
}
|
||||
set(value) {
|
||||
self._text = value
|
||||
self.label.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
|
||||
self.invalidateCalculatedLayout()
|
||||
}
|
||||
}
|
||||
|
||||
public var color: UIColor = UIColor(rgb: 0x007ee5) {
|
||||
didSet {
|
||||
self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState())
|
||||
}
|
||||
}
|
||||
|
||||
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
|
||||
didSet {
|
||||
self.label.attributedText = NSAttributedString(string: self._text, attributes: self.attributesForCurrentState())
|
||||
}
|
||||
}
|
||||
|
||||
private var touchCount = 0
|
||||
var pressed: () -> () = {}
|
||||
|
||||
override public init() {
|
||||
self.arrow = ASDisplayNode()
|
||||
self.label = ASTextNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.isUserInteractionEnabled = true
|
||||
self.isExclusiveTouch = true
|
||||
self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0)
|
||||
self.displaysAsynchronously = false
|
||||
|
||||
self.arrow.displaysAsynchronously = false
|
||||
self.label.displaysAsynchronously = false
|
||||
|
||||
self.addSubnode(self.arrow)
|
||||
let arrowImage = UIImage(named: "NavigationBackArrowLight", in: Bundle(for: NavigationBackButtonNode.self), compatibleWith: nil)?.precomposed()
|
||||
self.arrow.contents = arrowImage?.cgImage
|
||||
self.arrow.frame = CGRect(origin: CGPoint(), size: arrowImage?.size ?? CGSize())
|
||||
|
||||
self.addSubnode(self.label)
|
||||
}
|
||||
|
||||
public override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
self.label.measure(CGSize(width: max(0.0, constrainedSize.width - self.arrow.frame.size.width - self.arrowSpacing), height: constrainedSize.height))
|
||||
|
||||
return CGSize(width: self.arrow.frame.size.width + self.arrowSpacing + self.label.calculatedSize.width, height: max(self.arrow.frame.size.height, self.label.calculatedSize.height))
|
||||
}
|
||||
|
||||
var labelFrame: CGRect {
|
||||
get {
|
||||
return CGRect(x: self.arrow.frame.size.width + self.arrowSpacing, y: floor((self.frame.size.height - self.label.calculatedSize.height) / 2.0), width: self.label.calculatedSize.width, height: self.label.calculatedSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
public override func layout() {
|
||||
super.layout()
|
||||
|
||||
self.arrow.frame = CGRect(x: 0.0, y: floor((self.frame.size.height - arrow.frame.size.height) / 2.0), width: self.arrow.frame.size.width, height: self.arrow.frame.size.height)
|
||||
|
||||
self.label.frame = self.labelFrame
|
||||
}
|
||||
|
||||
private func touchInsideApparentBounds(_ touch: UITouch) -> Bool {
|
||||
var apparentBounds = self.bounds
|
||||
let hitTestSlop = self.hitTestSlop
|
||||
apparentBounds.origin.x += hitTestSlop.left
|
||||
apparentBounds.size.width -= hitTestSlop.left + hitTestSlop.right
|
||||
apparentBounds.origin.y += hitTestSlop.top
|
||||
apparentBounds.size.height -= hitTestSlop.top + hitTestSlop.bottom
|
||||
|
||||
return apparentBounds.contains(touch.location(in: self.view))
|
||||
}
|
||||
|
||||
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
self.touchCount += touches.count
|
||||
self.updateHighlightedState(true, animated: false)
|
||||
}
|
||||
|
||||
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true)
|
||||
}
|
||||
|
||||
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
self.updateHighlightedState(false, animated: false)
|
||||
|
||||
let previousTouchCount = self.touchCount
|
||||
self.touchCount = max(0, self.touchCount - touches.count)
|
||||
|
||||
if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && self.touchInsideApparentBounds(touches.first!) {
|
||||
self.pressed()
|
||||
}
|
||||
}
|
||||
|
||||
public override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.touchCount = max(0, self.touchCount - (touches?.count ?? 0))
|
||||
self.updateHighlightedState(false, animated: false)
|
||||
}
|
||||
|
||||
private var _highlighted = false
|
||||
private func updateHighlightedState(_ highlighted: Bool, animated: Bool) {
|
||||
if _highlighted != highlighted {
|
||||
_highlighted = highlighted
|
||||
|
||||
let alpha: CGFloat = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0)
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: 0.3, delay: 0.0, options: UIViewAnimationOptions.beginFromCurrentState, animations: { () -> Void in
|
||||
self.alpha = alpha
|
||||
}, completion: nil)
|
||||
}
|
||||
else {
|
||||
self.alpha = alpha
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1150
submodules/Display/Display/NavigationBar.swift
Normal file
1150
submodules/Display/Display/NavigationBar.swift
Normal file
File diff suppressed because it is too large
Load Diff
60
submodules/Display/Display/NavigationBarBadge.swift
Normal file
60
submodules/Display/Display/NavigationBarBadge.swift
Normal file
@ -0,0 +1,60 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class NavigationBarBadgeNode: ASDisplayNode {
|
||||
private var fillColor: UIColor
|
||||
private var strokeColor: UIColor
|
||||
private var textColor: UIColor
|
||||
|
||||
private let textNode: ASTextNode
|
||||
private let backgroundNode: ASImageNode
|
||||
|
||||
private let font: UIFont = Font.regular(13.0)
|
||||
|
||||
var text: String = "" {
|
||||
didSet {
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
|
||||
self.invalidateCalculatedLayout()
|
||||
}
|
||||
}
|
||||
|
||||
init(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
|
||||
self.fillColor = fillColor
|
||||
self.strokeColor = strokeColor
|
||||
self.textColor = textColor
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
func updateTheme(fillColor: UIColor, strokeColor: UIColor, textColor: UIColor) {
|
||||
self.fillColor = fillColor
|
||||
self.strokeColor = strokeColor
|
||||
self.textColor = textColor
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 18.0, color: fillColor, strokeColor: strokeColor, strokeWidth: 1.0)
|
||||
self.textNode.attributedText = NSAttributedString(string: self.text, font: self.font, textColor: self.textColor)
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
let badgeSize = self.textNode.measure(constrainedSize)
|
||||
let backgroundSize = CGSize(width: max(18.0, badgeSize.width + 10.0 + 1.0), height: 18.0)
|
||||
let backgroundFrame = CGRect(origin: CGPoint(), size: backgroundSize)
|
||||
self.backgroundNode.frame = backgroundFrame
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - badgeSize.width / 2.0), y: floorToScreenPixels((backgroundFrame.size.height - badgeSize.height) / 2.0)), size: badgeSize)
|
||||
|
||||
return backgroundSize
|
||||
}
|
||||
}
|
31
submodules/Display/Display/NavigationBarContentNode.swift
Normal file
31
submodules/Display/Display/NavigationBarContentNode.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public enum NavigationBarContentMode {
|
||||
case replacement
|
||||
case expansion
|
||||
}
|
||||
|
||||
open class NavigationBarContentNode: ASDisplayNode {
|
||||
open var requestContainerLayout: (ContainedViewLayoutTransition) -> Void = { _ in }
|
||||
|
||||
open var height: CGFloat {
|
||||
return self.nominalHeight
|
||||
}
|
||||
|
||||
open var clippedHeight: CGFloat {
|
||||
return self.nominalHeight
|
||||
}
|
||||
|
||||
open var nominalHeight: CGFloat {
|
||||
return 44.0
|
||||
}
|
||||
|
||||
open var mode: NavigationBarContentMode {
|
||||
return .replacement
|
||||
}
|
||||
|
||||
open func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
}
|
7
submodules/Display/Display/NavigationBarProxy.h
Normal file
7
submodules/Display/Display/NavigationBarProxy.h
Normal file
@ -0,0 +1,7 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface NavigationBarProxy : UINavigationBar
|
||||
|
||||
@property (nonatomic, copy) void (^setItemsProxy)(NSArray *, NSArray *, bool);
|
||||
|
||||
@end
|
67
submodules/Display/Display/NavigationBarProxy.m
Normal file
67
submodules/Display/Display/NavigationBarProxy.m
Normal file
@ -0,0 +1,67 @@
|
||||
#import "NavigationBarProxy.h"
|
||||
|
||||
@interface NavigationBarProxy ()
|
||||
{
|
||||
NSArray *_items;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NavigationBarProxy
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self != nil)
|
||||
{
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)pushNavigationItem:(UINavigationItem *)item animated:(BOOL)animated
|
||||
{
|
||||
[self setItems:[[self items] arrayByAddingObject:item] animated:animated];
|
||||
}
|
||||
|
||||
- (UINavigationItem *)popNavigationItemAnimated:(BOOL)animated
|
||||
{
|
||||
NSMutableArray *items = [[NSMutableArray alloc] initWithArray:[self items]];
|
||||
UINavigationItem *lastItem = [items lastObject];
|
||||
[items removeLastObject];
|
||||
[self setItems:items animated:animated];
|
||||
return lastItem;
|
||||
}
|
||||
|
||||
- (UINavigationItem *)topItem
|
||||
{
|
||||
return [[self items] lastObject];
|
||||
}
|
||||
|
||||
- (UINavigationItem *)backItem
|
||||
{
|
||||
NSLog(@"backItem");
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray *)items
|
||||
{
|
||||
if (_items == nil)
|
||||
return @[];
|
||||
return _items;
|
||||
}
|
||||
|
||||
- (void)setItems:(NSArray *)items
|
||||
{
|
||||
[self setItems:items animated:false];
|
||||
}
|
||||
|
||||
- (void)setItems:(NSArray *)items animated:(BOOL)animated
|
||||
{
|
||||
NSArray *previousItems = _items;
|
||||
_items = items;
|
||||
|
||||
if (_setItemsProxy)
|
||||
_setItemsProxy(previousItems, items, animated);
|
||||
}
|
||||
|
||||
@end
|
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
public protocol NavigationBarTitleTransitionNode {
|
||||
func makeTransitionMirrorNode() -> ASDisplayNode
|
||||
}
|
8
submodules/Display/Display/NavigationBarTitleView.swift
Normal file
8
submodules/Display/Display/NavigationBarTitleView.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
public protocol NavigationBarTitleView {
|
||||
func animateLayoutTransition()
|
||||
|
||||
func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition)
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
class NavigationBarTransitionContainer: ASDisplayNode {
|
||||
var progress: CGFloat = 0.0 {
|
||||
didSet {
|
||||
self.layout()
|
||||
}
|
||||
}
|
||||
|
||||
let transition: NavigationTransition
|
||||
let topNavigationBar: NavigationBar
|
||||
let bottomNavigationBar: NavigationBar
|
||||
|
||||
let topClippingNode: ASDisplayNode
|
||||
let bottomClippingNode: ASDisplayNode
|
||||
|
||||
let topNavigationBarSupernode: ASDisplayNode?
|
||||
let bottomNavigationBarSupernode: ASDisplayNode?
|
||||
|
||||
init(transition: NavigationTransition, topNavigationBar: NavigationBar, bottomNavigationBar: NavigationBar) {
|
||||
self.transition = transition
|
||||
|
||||
self.topNavigationBar = topNavigationBar
|
||||
self.topNavigationBarSupernode = topNavigationBar.supernode
|
||||
|
||||
self.bottomNavigationBar = bottomNavigationBar
|
||||
self.bottomNavigationBarSupernode = bottomNavigationBar.supernode
|
||||
|
||||
self.topClippingNode = ASDisplayNode()
|
||||
self.topClippingNode.clipsToBounds = true
|
||||
self.bottomClippingNode = ASDisplayNode()
|
||||
self.bottomClippingNode.clipsToBounds = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.topClippingNode.addSubnode(self.topNavigationBar)
|
||||
self.bottomClippingNode.addSubnode(self.bottomNavigationBar)
|
||||
|
||||
self.addSubnode(self.bottomClippingNode)
|
||||
self.addSubnode(self.topClippingNode)
|
||||
}
|
||||
|
||||
func complete() {
|
||||
self.topNavigationBarSupernode?.addSubnode(self.topNavigationBar)
|
||||
self.bottomNavigationBarSupernode?.addSubnode(self.bottomNavigationBar)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
let size = self.bounds.size
|
||||
|
||||
let position: CGFloat
|
||||
switch self.transition {
|
||||
case .Push:
|
||||
position = 1.0 - progress
|
||||
case .Pop:
|
||||
position = progress
|
||||
}
|
||||
|
||||
let offset = floorToScreenPixels(size.width * position)
|
||||
|
||||
self.topClippingNode.frame = CGRect(origin: CGPoint(x: offset, y: 0.0), size: size)
|
||||
|
||||
self.bottomClippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: offset, height: size.height))
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum NavigationBarTransitionRole {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
final class NavigationBarTransitionState {
|
||||
weak var navigationBar: NavigationBar?
|
||||
let transition: NavigationTransition
|
||||
let role: NavigationBarTransitionRole
|
||||
let progress: CGFloat
|
||||
|
||||
init(navigationBar: NavigationBar, transition: NavigationTransition, role: NavigationBarTransitionRole, progress: CGFloat) {
|
||||
self.navigationBar = navigationBar
|
||||
self.transition = transition
|
||||
self.role = role
|
||||
self.progress = progress
|
||||
}
|
||||
}
|
390
submodules/Display/Display/NavigationButtonNode.swift
Normal file
390
submodules/Display/Display/NavigationButtonNode.swift
Normal file
@ -0,0 +1,390 @@
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
public protocol NavigationButtonCustomDisplayNode {
|
||||
var isHighlightable: Bool { get }
|
||||
}
|
||||
|
||||
private final class NavigationButtonItemNode: ASTextNode {
|
||||
private func fontForCurrentState() -> UIFont {
|
||||
return self.bold ? UIFont.boldSystemFont(ofSize: 17.0) : UIFont.systemFont(ofSize: 17.0)
|
||||
}
|
||||
|
||||
private func attributesForCurrentState() -> [NSAttributedStringKey : AnyObject] {
|
||||
return [
|
||||
NSAttributedStringKey.font: self.fontForCurrentState(),
|
||||
NSAttributedStringKey.foregroundColor: self.isEnabled ? self.color : self.disabledColor
|
||||
]
|
||||
}
|
||||
|
||||
private var setEnabledListener: Int?
|
||||
|
||||
var item: UIBarButtonItem? {
|
||||
didSet {
|
||||
if self.item !== oldValue {
|
||||
if let oldValue = oldValue, let setEnabledListener = self.setEnabledListener {
|
||||
oldValue.removeSetEnabledListener(setEnabledListener)
|
||||
self.setEnabledListener = nil
|
||||
}
|
||||
|
||||
if let item = self.item {
|
||||
self.setEnabledListener = item.addSetEnabledListener { [weak self] value in
|
||||
self?.isEnabled = value
|
||||
}
|
||||
self.accessibilityHint = item.accessibilityHint
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _text: String?
|
||||
public var text: String {
|
||||
get {
|
||||
return _text ?? ""
|
||||
}
|
||||
set(value) {
|
||||
_text = value
|
||||
|
||||
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
|
||||
self.item?.accessibilityLabel = value
|
||||
}
|
||||
}
|
||||
|
||||
private var imageNode: ASImageNode?
|
||||
|
||||
private var _image: UIImage?
|
||||
public var image: UIImage? {
|
||||
get {
|
||||
return _image
|
||||
} set(value) {
|
||||
_image = value
|
||||
|
||||
if let _ = value {
|
||||
if self.imageNode == nil {
|
||||
let imageNode = ASImageNode()
|
||||
imageNode.displayWithoutProcessing = true
|
||||
imageNode.displaysAsynchronously = false
|
||||
self.imageNode = imageNode
|
||||
self.addSubnode(imageNode)
|
||||
}
|
||||
self.imageNode?.image = image
|
||||
} else if let imageNode = self.imageNode {
|
||||
imageNode.removeFromSupernode()
|
||||
self.imageNode = nil
|
||||
}
|
||||
|
||||
self.invalidateCalculatedLayout()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
public var node: ASDisplayNode? {
|
||||
didSet {
|
||||
if self.node !== oldValue {
|
||||
oldValue?.removeFromSupernode()
|
||||
if let node = self.node {
|
||||
self.addSubnode(node)
|
||||
self.invalidateCalculatedLayout()
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var color: UIColor = UIColor(rgb: 0x007ee5) {
|
||||
didSet {
|
||||
if let text = self._text {
|
||||
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
|
||||
didSet {
|
||||
if let text = self._text {
|
||||
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var _bold: Bool = false
|
||||
public var bold: Bool {
|
||||
get {
|
||||
return _bold
|
||||
}
|
||||
set(value) {
|
||||
if _bold != value {
|
||||
_bold = value
|
||||
|
||||
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var touchCount = 0
|
||||
public var pressed: () -> () = { }
|
||||
public var highlightChanged: (Bool) -> () = { _ in }
|
||||
|
||||
override public var isAccessibilityElement: Bool {
|
||||
get {
|
||||
return true
|
||||
} set(value) {
|
||||
super.isAccessibilityElement = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
|
||||
self.isAccessibilityElement = true
|
||||
|
||||
self.isUserInteractionEnabled = true
|
||||
self.isExclusiveTouch = true
|
||||
self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0)
|
||||
self.displaysAsynchronously = false
|
||||
|
||||
self.accessibilityTraits = UIAccessibilityTraitButton
|
||||
}
|
||||
|
||||
func updateLayout(_ constrainedSize: CGSize) -> CGSize {
|
||||
let superSize = super.calculateSizeThatFits(constrainedSize)
|
||||
|
||||
if let node = self.node {
|
||||
let nodeSize = node.measure(constrainedSize)
|
||||
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height))
|
||||
node.frame = CGRect(origin: CGPoint(), size: nodeSize)
|
||||
return size
|
||||
} else if let imageNode = self.imageNode {
|
||||
let nodeSize = imageNode.image?.size ?? CGSize()
|
||||
let size = CGSize(width: max(nodeSize.width, superSize.width), height: max(nodeSize.height, superSize.height))
|
||||
imageNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - nodeSize.width) / 2.0) + 5.0, y: floorToScreenPixels((size.height - nodeSize.height) / 2.0)), size: nodeSize)
|
||||
return size
|
||||
}
|
||||
return superSize
|
||||
}
|
||||
|
||||
private func touchInsideApparentBounds(_ touch: UITouch) -> Bool {
|
||||
var apparentBounds = self.bounds
|
||||
let hitTestSlop = self.hitTestSlop
|
||||
apparentBounds.origin.x += hitTestSlop.left
|
||||
apparentBounds.size.width += -hitTestSlop.left - hitTestSlop.right
|
||||
apparentBounds.origin.y += hitTestSlop.top
|
||||
apparentBounds.size.height += -hitTestSlop.top - hitTestSlop.bottom
|
||||
|
||||
return apparentBounds.contains(touch.location(in: self.view))
|
||||
}
|
||||
|
||||
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
self.touchCount += touches.count
|
||||
self.updateHighlightedState(true, animated: false)
|
||||
}
|
||||
|
||||
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
self.updateHighlightedState(self.touchInsideApparentBounds(touches.first!), animated: true)
|
||||
}
|
||||
|
||||
public override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
self.updateHighlightedState(false, animated: false)
|
||||
|
||||
let previousTouchCount = self.touchCount
|
||||
self.touchCount = max(0, self.touchCount - touches.count)
|
||||
|
||||
if previousTouchCount != 0 && self.touchCount == 0 && self.isEnabled && self.touchInsideApparentBounds(touches.first!) {
|
||||
self.pressed()
|
||||
}
|
||||
}
|
||||
|
||||
public override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.touchCount = max(0, self.touchCount - (touches?.count ?? 0))
|
||||
self.updateHighlightedState(false, animated: false)
|
||||
}
|
||||
|
||||
private var _highlighted = false
|
||||
private func updateHighlightedState(_ highlighted: Bool, animated: Bool) {
|
||||
if _highlighted != highlighted {
|
||||
_highlighted = highlighted
|
||||
|
||||
var shouldChangeHighlight = true
|
||||
if let node = self.node as? NavigationButtonCustomDisplayNode {
|
||||
shouldChangeHighlight = node.isHighlightable
|
||||
}
|
||||
|
||||
if shouldChangeHighlight {
|
||||
self.alpha = !self.isEnabled ? 1.0 : (highlighted ? 0.4 : 1.0)
|
||||
self.highlightChanged(highlighted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
get {
|
||||
return super.isEnabled
|
||||
}
|
||||
set(value) {
|
||||
if self.isEnabled != value {
|
||||
super.isEnabled = value
|
||||
|
||||
self.attributedText = NSAttributedString(string: text, attributes: self.attributesForCurrentState())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final class NavigationButtonNode: ASDisplayNode {
|
||||
private var nodes: [NavigationButtonItemNode] = []
|
||||
|
||||
public var pressed: (Int) -> () = { _ in }
|
||||
public var highlightChanged: (Int, Bool) -> () = { _, _ in }
|
||||
|
||||
public var color: UIColor = UIColor(rgb: 0x007ee5) {
|
||||
didSet {
|
||||
if !self.color.isEqual(oldValue) {
|
||||
for node in self.nodes {
|
||||
node.color = self.color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var disabledColor: UIColor = UIColor(rgb: 0xd0d0d0) {
|
||||
didSet {
|
||||
if !self.disabledColor.isEqual(oldValue) {
|
||||
for node in self.nodes {
|
||||
node.disabledColor = self.disabledColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public var accessibilityElements: [Any]? {
|
||||
get {
|
||||
return self.nodes
|
||||
} set(value) {
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
self.isAccessibilityElement = false
|
||||
}
|
||||
|
||||
var manualText: String {
|
||||
return self.nodes.first?.text ?? ""
|
||||
}
|
||||
|
||||
func updateManualText(_ text: String, isBack: Bool = true) {
|
||||
let node: NavigationButtonItemNode
|
||||
if self.nodes.count > 0 {
|
||||
node = self.nodes[0]
|
||||
} else {
|
||||
node = NavigationButtonItemNode()
|
||||
node.color = self.color
|
||||
node.highlightChanged = { [weak node, weak self] value in
|
||||
if let strongSelf = self, let node = node {
|
||||
if let index = strongSelf.nodes.index(where: { $0 === node }) {
|
||||
strongSelf.highlightChanged(index, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
node.pressed = { [weak self, weak node] in
|
||||
if let strongSelf = self, let node = node {
|
||||
if let index = strongSelf.nodes.index(where: { $0 === node }) {
|
||||
strongSelf.pressed(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.nodes.append(node)
|
||||
self.addSubnode(node)
|
||||
}
|
||||
node.item = nil
|
||||
node.text = text
|
||||
|
||||
/*if isBack {
|
||||
node.accessibilityHint = "Back button"
|
||||
node.accessibilityTraits = 0
|
||||
} else {
|
||||
node.accessibilityHint = nil
|
||||
node.accessibilityTraits = UIAccessibilityTraitButton
|
||||
}*/
|
||||
|
||||
node.image = nil
|
||||
node.bold = false
|
||||
node.isEnabled = true
|
||||
node.node = nil
|
||||
|
||||
if 1 < self.nodes.count {
|
||||
for i in 1 ..< self.nodes.count {
|
||||
self.nodes[i].removeFromSupernode()
|
||||
}
|
||||
self.nodes.removeSubrange(1...)
|
||||
}
|
||||
}
|
||||
|
||||
func updateItems(_ items: [UIBarButtonItem]) {
|
||||
for i in 0 ..< items.count {
|
||||
let node: NavigationButtonItemNode
|
||||
if self.nodes.count > i {
|
||||
node = self.nodes[i]
|
||||
} else {
|
||||
node = NavigationButtonItemNode()
|
||||
node.color = self.color
|
||||
node.highlightChanged = { [weak node, weak self] value in
|
||||
if let strongSelf = self, let node = node {
|
||||
if let index = strongSelf.nodes.index(where: { $0 === node }) {
|
||||
strongSelf.highlightChanged(index, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
node.pressed = { [weak self, weak node] in
|
||||
if let strongSelf = self, let node = node {
|
||||
if let index = strongSelf.nodes.index(where: { $0 === node }) {
|
||||
strongSelf.pressed(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.nodes.append(node)
|
||||
self.addSubnode(node)
|
||||
}
|
||||
node.item = items[i]
|
||||
node.text = items[i].title ?? ""
|
||||
node.image = items[i].image
|
||||
node.bold = items[i].style == .done
|
||||
node.isEnabled = items[i].isEnabled
|
||||
node.node = items[i].customDisplayNode
|
||||
}
|
||||
if items.count < self.nodes.count {
|
||||
for i in items.count ..< self.nodes.count {
|
||||
self.nodes[i].removeFromSupernode()
|
||||
}
|
||||
self.nodes.removeSubrange(items.count...)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(constrainedSize: CGSize) -> CGSize {
|
||||
var nodeOrigin = CGPoint()
|
||||
var totalSize = CGSize()
|
||||
for node in self.nodes {
|
||||
if !totalSize.width.isZero {
|
||||
totalSize.width += 16.0
|
||||
nodeOrigin.x += 16.0
|
||||
}
|
||||
var nodeSize = node.updateLayout(constrainedSize)
|
||||
nodeSize.width = ceil(nodeSize.width)
|
||||
nodeSize.height = ceil(nodeSize.height)
|
||||
totalSize.width += nodeSize.width
|
||||
totalSize.height = max(totalSize.height, nodeSize.height)
|
||||
node.frame = CGRect(origin: nodeOrigin, size: nodeSize)
|
||||
nodeOrigin.x += node.bounds.width
|
||||
}
|
||||
return totalSize
|
||||
}
|
||||
}
|
1120
submodules/Display/Display/NavigationController.swift
Normal file
1120
submodules/Display/Display/NavigationController.swift
Normal file
File diff suppressed because it is too large
Load Diff
7
submodules/Display/Display/NavigationControllerProxy.h
Normal file
7
submodules/Display/Display/NavigationControllerProxy.h
Normal file
@ -0,0 +1,7 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface NavigationControllerProxy : UINavigationController
|
||||
|
||||
- (instancetype)init;
|
||||
|
||||
@end
|
15
submodules/Display/Display/NavigationControllerProxy.m
Normal file
15
submodules/Display/Display/NavigationControllerProxy.m
Normal file
@ -0,0 +1,15 @@
|
||||
#import "NavigationControllerProxy.h"
|
||||
|
||||
#import "NavigationBarProxy.h"
|
||||
|
||||
@implementation NavigationControllerProxy
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super initWithNavigationBarClass:[NavigationBarProxy class] toolbarClass:[UIToolbar class]];
|
||||
if (self != nil) {
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user