Swiftgram/Display/NavigationBar.swift
2016-10-07 19:13:32 +03:00

534 lines
24 KiB
Swift

import UIKit
import AsyncDisplayKit
private func generateBackArrowImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 13.0, height: 22.0), contextGenerator: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(color.cgColor)
let _ = try? drawSvgPath(context, path: "M10.6569398,0.0 L0.0,11 L10.6569398,22 L13,19.1782395 L5.07681762,11 L13,2.82176047 Z ")
})
}
private var backArrowImageCache: [Int32: UIImage] = [:]
private func backArrowImage(color: UIColor) -> UIImage? {
var red: CGFloat = 0.0
var green: CGFloat = 0.0
var blue: CGFloat = 0.0
var alpha: CGFloat = 0.0
color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
let key = (Int32(alpha * 255.0) << 24) | (Int32(red * 255.0) << 16) | (Int32(green * 255.0) << 8) | Int32(blue * 255.0)
if let image = backArrowImageCache[key] {
return image
} else {
if let image = generateBackArrowImage(color: color) {
backArrowImageCache[key] = image
return image
} else {
return nil
}
}
}
public class NavigationBar: ASDisplayNode {
public var foregroundColor: UIColor = UIColor.black {
didSet {
if let title = self.title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: self.foregroundColor)
}
}
}
public var accentColor: UIColor = UIColor(0x1195f2) {
didSet {
self.backButtonNode.color = self.accentColor
self.leftButtonNode.color = self.accentColor
self.rightButtonNode.color = self.accentColor
self.backButtonArrow.image = backArrowImage(color: self.accentColor)
}
}
var backPressed: () -> () = { }
private var collapsed: Bool {
get {
return self.frame.size.height < (20.0 + 44.0)
}
}
private let stripeNode: ASDisplayNode
public var stripeColor: UIColor = UIColor(red: 0.6953125, green: 0.6953125, blue: 0.6953125, alpha: 1.0) {
didSet {
self.stripeNode.backgroundColor = self.stripeColor
}
}
private let clippingNode: ASDisplayNode
private var itemTitleListenerKey: Int?
private var itemTitleViewListenerKey: Int?
private var itemLeftButtonListenerKey: Int?
private var itemRightButtonListenerKey: Int?
private var _item: UINavigationItem?
var item: UINavigationItem? {
get {
return self._item
} set(value) {
if let previousValue = self._item {
if let itemTitleListenerKey = self.itemTitleListenerKey {
previousValue.removeSetTitleListener(itemTitleListenerKey)
self.itemTitleListenerKey = nil
}
if let itemLeftButtonListenerKey = self.itemLeftButtonListenerKey {
previousValue.removeSetLeftBarButtonItemListener(itemLeftButtonListenerKey)
self.itemLeftButtonListenerKey = nil
}
}
self._item = value
self.leftButtonNode.removeFromSupernode()
self.rightButtonNode.removeFromSupernode()
if let item = value {
self.title = item.title
self.itemTitleListenerKey = item.addSetTitleListener { [weak self] text in
if let strongSelf = self {
strongSelf.title = text
}
}
self.titleView = item.titleView
self.itemTitleViewListenerKey = item.addSetTitleViewListener { [weak self] titleView in
if let strongSelf = self {
strongSelf.titleView = titleView
}
}
self.itemLeftButtonListenerKey = item.addSetLeftBarButtonItemListener { [weak self] _, _ in
if let strongSelf = self {
strongSelf.updateLeftButton()
strongSelf.invalidateCalculatedLayout()
strongSelf.setNeedsLayout()
}
}
self.itemRightButtonListenerKey = item.addSetRightBarButtonItemListener { [weak self] _, _ in
if let strongSelf = self {
strongSelf.updateRightButton()
strongSelf.invalidateCalculatedLayout()
strongSelf.setNeedsLayout()
}
}
self.updateLeftButton()
self.updateRightButton()
} else {
self.title = nil
self.updateLeftButton()
self.updateRightButton()
}
self.invalidateCalculatedLayout()
}
}
private var title: String? {
didSet {
if let title = self.title {
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: self.foregroundColor)
if self.titleNode.supernode == nil {
self.clippingNode.addSubnode(self.titleNode)
}
} else {
self.titleNode.removeFromSupernode()
}
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
private var titleView: UIView? {
didSet {
if let oldValue = oldValue {
oldValue.removeFromSuperview()
}
if let titleView = self.titleView {
self.clippingNode.view.addSubview(titleView)
}
self.invalidateCalculatedLayout()
self.setNeedsLayout()
}
}
private let titleNode: ASTextNode
var previousItemListenerKey: Int?
var _previousItem: UINavigationItem?
var previousItem: UINavigationItem? {
get {
return self._previousItem
} set(value) {
if let previousValue = self._previousItem, let previousItemListenerKey = self.previousItemListenerKey {
previousValue.removeSetTitleListener(previousItemListenerKey)
self.previousItemListenerKey = nil
}
self._previousItem = value
if let previousItem = value, previousItem.backBarButtonItem == nil {
self.previousItemListenerKey = previousItem.addSetTitleListener { [weak self] text in
if let strongSelf = self {
strongSelf.backButtonNode.text = text ?? "Back"
strongSelf.invalidateCalculatedLayout()
}
}
}
self.updateLeftButton()
self.invalidateCalculatedLayout()
}
}
private func updateLeftButton() {
if let item = self.item {
if let leftBarButtonItem = item.leftBarButtonItem {
self.backButtonNode.removeFromSupernode()
self.backButtonArrow.removeFromSupernode()
self.leftButtonNode.text = leftBarButtonItem.title ?? ""
if self.leftButtonNode.supernode == nil {
self.clippingNode.addSubnode(self.leftButtonNode)
}
} else {
self.leftButtonNode.removeFromSupernode()
if let previousItem = self.previousItem {
if let backBarButtonItem = previousItem.backBarButtonItem {
self.backButtonNode.text = backBarButtonItem.title ?? "Back"
} else {
self.backButtonNode.text = previousItem.title ?? "Back"
}
if self.backButtonNode.supernode == nil {
self.clippingNode.addSubnode(self.backButtonNode)
self.clippingNode.addSubnode(self.backButtonArrow)
}
} else {
self.backButtonNode.removeFromSupernode()
}
}
} else {
self.leftButtonNode.removeFromSupernode()
self.backButtonNode.removeFromSupernode()
self.backButtonArrow.removeFromSupernode()
}
}
private func updateRightButton() {
if let item = self.item {
if let rightBarButtonItem = item.rightBarButtonItem {
self.rightButtonNode.text = rightBarButtonItem.title ?? ""
self.rightButtonNode.node = rightBarButtonItem.customDisplayNode
if self.rightButtonNode.supernode == nil {
self.clippingNode.addSubnode(self.rightButtonNode)
}
} else {
self.rightButtonNode.removeFromSupernode()
}
} else {
self.rightButtonNode.removeFromSupernode()
}
}
private let backButtonNode: NavigationButtonNode
private let backButtonArrow: ASImageNode
private let leftButtonNode: NavigationButtonNode
private let rightButtonNode: NavigationButtonNode
private var _transitionState: NavigationBarTransitionState?
var transitionState: NavigationBarTransitionState? {
get {
return self._transitionState
} set(value) {
let updateNodes = self._transitionState?.navigationBar !== value?.navigationBar
self._transitionState = value
if updateNodes {
if let transitionTitleNode = self.transitionTitleNode {
transitionTitleNode.removeFromSupernode()
self.transitionTitleNode = nil
}
if let transitionBackButtonNode = self.transitionBackButtonNode {
transitionBackButtonNode.removeFromSupernode()
self.transitionBackButtonNode = nil
}
if let transitionBackArrowNode = self.transitionBackArrowNode {
transitionBackArrowNode.removeFromSupernode()
self.transitionBackArrowNode = nil
}
if let value = value {
switch value.role {
case .top:
if let transitionTitleNode = value.navigationBar?.makeTransitionTitleNode(foregroundColor: self.foregroundColor) {
self.transitionTitleNode = transitionTitleNode
if self.leftButtonNode.supernode != nil {
self.clippingNode.insertSubnode(transitionTitleNode, belowSubnode: self.leftButtonNode)
} else if self.backButtonNode.supernode != nil {
self.clippingNode.insertSubnode(transitionTitleNode, belowSubnode: self.backButtonNode)
} else {
self.clippingNode.addSubnode(transitionTitleNode)
}
}
case .bottom:
if let transitionBackButtonNode = value.navigationBar?.makeTransitionBackButtonNode(accentColor: self.accentColor) {
self.transitionBackButtonNode = transitionBackButtonNode
self.clippingNode.addSubnode(transitionBackButtonNode)
}
if let transitionBackArrowNode = value.navigationBar?.makeTransitionBackArrowNode(accentColor: self.accentColor) {
self.transitionBackArrowNode = transitionBackArrowNode
self.clippingNode.addSubnode(transitionBackArrowNode)
}
}
}
}
self.layout()
}
}
private var transitionTitleNode: ASDisplayNode?
private var transitionBackButtonNode: NavigationButtonNode?
private var transitionBackArrowNode: ASDisplayNode?
public override init() {
self.stripeNode = ASDisplayNode()
self.titleNode = ASTextNode()
self.backButtonNode = NavigationButtonNode()
self.backButtonArrow = ASImageNode()
self.backButtonArrow.displayWithoutProcessing = true
self.backButtonArrow.displaysAsynchronously = false
self.backButtonArrow.image = backArrowImage(color: self.accentColor)
self.leftButtonNode = NavigationButtonNode()
self.rightButtonNode = NavigationButtonNode()
self.clippingNode = ASDisplayNode()
self.clippingNode.clipsToBounds = true
super.init()
self.addSubnode(self.clippingNode)
self.backgroundColor = UIColor(red: 0.968626451, green: 0.968626451, blue: 0.968626451, alpha: 1.0)
self.stripeNode.isLayerBacked = true
self.stripeNode.displaysAsynchronously = false
self.stripeNode.backgroundColor = self.stripeColor
self.addSubnode(self.stripeNode)
self.titleNode.displaysAsynchronously = false
self.titleNode.maximumNumberOfLines = 1
self.titleNode.truncationMode = .byTruncatingTail
self.titleNode.isOpaque = false
self.backButtonNode.highlightChanged = { [weak self] highlighted in
if let strongSelf = self {
strongSelf.backButtonArrow.alpha = (highlighted ? 0.4 : 1.0)
}
}
self.backButtonNode.pressed = { [weak self] in
if let backPressed = self?.backPressed {
backPressed()
}
}
self.leftButtonNode.pressed = { [weak self] in
if let item = self?.item, let leftBarButtonItem = item.leftBarButtonItem {
leftBarButtonItem.performActionOnTarget()
}
}
self.rightButtonNode.pressed = { [weak self] in
if let item = self?.item, let rightBarButtonItem = item.rightBarButtonItem {
rightBarButtonItem.performActionOnTarget()
}
}
}
public override func layout() {
var size = self.bounds.size
let leftButtonInset: CGFloat = 8.0
let backButtonInset: CGFloat = 27.0
self.clippingNode.frame = CGRect(origin: CGPoint(), size: size)
self.stripeNode.frame = CGRect(x: 0.0, y: size.height, width: size.width, height: UIScreenPixel)
var nominalHeight: CGFloat = self.collapsed ? 32.0 : 44.0
var contentVerticalOrigin = size.height - nominalHeight
var leftTitleInset: CGFloat = 8.0
var rightTitleInset: CGFloat = 8.0
if self.backButtonNode.supernode != nil {
let backButtonSize = self.backButtonNode.measure(CGSize(width: size.width, height: nominalHeight))
leftTitleInset += backButtonSize.width + backButtonInset + 8.0 + 8.0
let topHitTestSlop = (nominalHeight - backButtonSize.height) * 0.5
self.backButtonNode.hitTestSlop = UIEdgeInsetsMake(-topHitTestSlop, -27.0, -topHitTestSlop, -8.0)
if let transitionState = self.transitionState {
let progress = transitionState.progress
switch transitionState.role {
case .top:
let initialX: CGFloat = backButtonInset
let finalX: CGFloat = floor((size.width - backButtonSize.width) / 2.0) - size.width
self.backButtonNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize)
self.backButtonNode.alpha = 1.0 - progress
if let transitionTitleNode = self.transitionTitleNode {
let transitionTitleSize = transitionTitleNode.measure(CGSize(width: size.width, height: nominalHeight))
let initialX: CGFloat = backButtonInset + floor((backButtonSize.width - transitionTitleSize.width) / 2.0)
let finalX: CGFloat = floor((size.width - transitionTitleSize.width) / 2.0) - size.width
transitionTitleNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - transitionTitleSize.height) / 2.0)), size: transitionTitleSize)
transitionTitleNode.alpha = progress
}
self.backButtonArrow.frame = CGRect(origin: CGPoint(x: 8.0 - progress * size.width, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0))
self.backButtonArrow.alpha = max(0.0, 1.0 - progress * 1.3)
case .bottom:
self.backButtonNode.alpha = 1.0
self.backButtonNode.frame = CGRect(origin: CGPoint(x: backButtonInset, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize)
self.backButtonArrow.alpha = 1.0
self.backButtonArrow.frame = CGRect(origin: CGPoint(x: 8.0, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0))
}
} else {
self.backButtonNode.alpha = 1.0
self.backButtonNode.frame = CGRect(origin: CGPoint(x: backButtonInset, y: contentVerticalOrigin + floor((nominalHeight - backButtonSize.height) / 2.0)), size: backButtonSize)
self.backButtonArrow.alpha = 1.0
self.backButtonArrow.frame = CGRect(origin: CGPoint(x: 8.0, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0))
}
} else if self.leftButtonNode.supernode != nil {
let leftButtonSize = self.leftButtonNode.measure(CGSize(width: size.width, height: nominalHeight))
leftTitleInset += leftButtonSize.width + leftButtonInset + 8.0 + 8.0
self.leftButtonNode.alpha = 1.0
self.leftButtonNode.frame = CGRect(origin: CGPoint(x: leftButtonInset, y: contentVerticalOrigin + floor((nominalHeight - leftButtonSize.height) / 2.0)), size: leftButtonSize)
}
if self.rightButtonNode.supernode != nil {
let rightButtonSize = self.rightButtonNode.measure(CGSize(width: size.width, height: nominalHeight))
rightTitleInset += rightButtonSize.width + leftButtonInset + 8.0 + 8.0
self.rightButtonNode.alpha = 1.0
self.rightButtonNode.frame = CGRect(origin: CGPoint(x: size.width - leftButtonInset - rightButtonSize.width, y: contentVerticalOrigin + floor((nominalHeight - rightButtonSize.height) / 2.0)), size: rightButtonSize)
}
if let transitionState = self.transitionState {
let progress = transitionState.progress
switch transitionState.role {
case .top:
break
case .bottom:
if let transitionBackButtonNode = self.transitionBackButtonNode {
let transitionBackButtonSize = transitionBackButtonNode.measure(CGSize(width: size.width, height: nominalHeight))
let initialX: CGFloat = backButtonInset + size.width * 0.3
let finalX: CGFloat = floor((size.width - transitionBackButtonSize.width) / 2.0)
transitionBackButtonNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - transitionBackButtonSize.height) / 2.0)), size: transitionBackButtonSize)
transitionBackButtonNode.alpha = 1.0 - progress
}
if let transitionBackArrowNode = self.transitionBackArrowNode {
let initialX: CGFloat = 8.0 + size.width * 0.3
let finalX: CGFloat = 8.0
transitionBackArrowNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floor((nominalHeight - 22.0) / 2.0)), size: CGSize(width: 13.0, height: 22.0))
transitionBackArrowNode.alpha = max(0.0, 1.0 - progress * 1.3)
}
}
}
if self.titleNode.supernode != nil {
let titleSize = self.titleNode.measure(CGSize(width: max(1.0, size.width - max(leftTitleInset, rightTitleInset) * 2.0), height: nominalHeight))
if let transitionState = self.transitionState, let otherNavigationBar = transitionState.navigationBar {
let progress = transitionState.progress
switch transitionState.role {
case .top:
let initialX = floor((size.width - titleSize.width) / 2.0)
let finalX: CGFloat = leftButtonInset
self.titleNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)
self.titleNode.alpha = 1.0 - progress
case .bottom:
var initialX: CGFloat = backButtonInset
if otherNavigationBar.backButtonNode.supernode != nil {
initialX += floor((otherNavigationBar.backButtonNode.frame.size.width - titleSize.width) / 2.0)
}
initialX += size.width * 0.3
let finalX: CGFloat = floor((size.width - titleSize.width) / 2.0)
self.titleNode.frame = CGRect(origin: CGPoint(x: initialX * (1.0 - progress) + finalX * progress, y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)
self.titleNode.alpha = progress
}
} else {
self.titleNode.alpha = 1.0
self.titleNode.frame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: contentVerticalOrigin + floorToScreenPixels((nominalHeight - titleSize.height) / 2.0)), size: titleSize)
}
}
if let titleView = self.titleView {
let titleViewSize = CGSize(width: max(1.0, size.width - leftTitleInset - leftTitleInset), height: nominalHeight)
titleView.frame = CGRect(origin: CGPoint(x: leftTitleInset, y: contentVerticalOrigin), size: titleViewSize)
}
//self.effectView.frame = self.bounds
}
public func makeTransitionTitleNode(foregroundColor: UIColor) -> ASDisplayNode? {
if let title = self.title {
let node = ASTextNode()
node.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: foregroundColor)
return node
} else {
return nil
}
}
private func makeTransitionBackButtonNode(accentColor: UIColor) -> NavigationButtonNode? {
if self.backButtonNode.supernode != nil {
let node = NavigationButtonNode()
node.text = self.backButtonNode.text
node.color = accentColor
return node
} else {
return nil
}
}
private func makeTransitionBackArrowNode(accentColor: UIColor) -> ASDisplayNode? {
if self.backButtonArrow.supernode != nil {
let node = ASImageNode()
node.image = backArrowImage(color: accentColor)
node.frame = self.backButtonArrow.frame
node.displayWithoutProcessing = true
node.displaysAsynchronously = false
return node
} else {
return nil
}
}
}