NavigationBackgroundNode
protocol
AnyValueProviding
1
var anyValue: ControlledTransitionProperty.AnyValue { get }
class
ControlledTransitionProperty
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
final class AnyValue: Equatable, CustomStringConvertible {
let value: Any
let nsValue: NSValue
let stringValue: () -> String
let isEqual: (AnyValue) -> Bool
let interpolate: (AnyValue, CGFloat) -> AnyValue
init(
value: Any,
nsValue: NSValue,
stringValue: @escaping () -> String,
isEqual: @escaping (AnyValue) -> Bool,
interpolate: @escaping (AnyValue, CGFloat) -> AnyValue
) {
self.value = value
self.nsValue = nsValue
self.stringValue = stringValue
self.isEqual = isEqual
self.interpolate = interpolate
}
var description: String {
return self.stringValue()
}
static func ==(lhs: AnyValue, rhs: AnyValue) -> Bool {
if lhs.isEqual(rhs) {
return true
} else {
return false
}
}
}
let layer: CALayer
let path: String
var fromValue: AnyValue
let toValue: AnyValue
private let completion: ((Bool) -> Void)?
init<T: Equatable>(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T:
AnyValueProviding {
self.layer = layer
self.path = path
self.fromValue = fromValue.anyValue
self.toValue = toValue.anyValue
self.completion = completion
self.update(at: 0.0)
}
deinit {
self.layer.removeAnimation(forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
}
func update(at fraction: CGFloat) {
let value = self.fromValue.interpolate(toValue, fraction)
let animation = CABasicAnimation(keyPath: self.path)
animation.speed = 0.0
animation.beginTime = CACurrentMediaTime() + 1000.0
animation.timeOffset = 0.01
animation.duration = 1.0
animation.fillMode = .both
animation.fromValue = value.nsValue
animation.toValue = value.nsValue
animation.timingFunction = CAMediaTimingFunction(name: .linear)
animation.isRemovedOnCompletion = false
self.layer.add(animation, forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
}
func complete(atEnd: Bool) {
self.completion?(atEnd)
}
protocol
ControlledTransitionAnimator
1
2
3
4
5
6
7
8
9
10
11
12
13
var duration: Double { get }
func startAnimation()
func setAnimationProgress(_ progress: CGFloat)
func finishAnimation()
func updateAlpha(layer: CALayer, alpha: CGFloat, completion: ((Bool) -> Void)?)
func updateScale(layer: CALayer, scale: CGFloat, completion: ((Bool) -> Void)?)
func animateScale(layer: CALayer, from fromValue: CGFloat, to toValue: CGFloat, completion: ((Bool) -> Void)?)
func updatePosition(layer: CALayer, position: CGPoint, completion: ((Bool) -> Void)?)
func updateBounds(layer: CALayer, bounds: CGRect, completion: ((Bool) -> Void)?)
func updateFrame(layer: CALayer, frame: CGRect, completion: ((Bool) -> Void)?)
func updateCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)?)
class
NavigationBackgroundNode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
private var _color: UIColor
private var enableBlur: Bool
private var effectView: UIVisualEffectView?
private let backgroundNode: ASDisplayNode
private var validLayout: (CGSize, CGFloat)?
public init(color: UIColor, enableBlur: Bool = true) {
self._color = .clear
self.enableBlur = enableBlur
self.backgroundNode = ASDisplayNode()
super.init()
self.addSubnode(self.backgroundNode)
self.updateColor(color: color, transition: .immediate)
}
public override func didLoad() {
super.didLoad()
if self.scheduledUpdate {
self.scheduledUpdate = false
self.updateBackgroundBlur(forceKeepBlur: false)
}
}
private var scheduledUpdate = false
// update background blure
private func updateBackgroundBlur(forceKeepBlur: Bool) {
guard self.isNodeLoaded else {
self.scheduledUpdate = true
return
}
if self.enableBlur && !sharedIsReduceTransparencyEnabled && ((self._color.alpha > .ulpOfOne && self._color.alpha < 0.95) || forceKeepBlur) {
if self.effectView == nil {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
// hack solution
for subview in effectView.subviews {
if subview.description.contains("VisualEffectSubview") {
subview.isHidden = true
}
}
if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters {
sublayer.backgroundColor = nil
sublayer.isOpaque = false
let allowedKeys: [String] = [
"colorSaturate",
"gaussianBlur"
]
sublayer.filters = filters.filter { filter in
guard let filter = filter as? NSObject else {
return true
}
let filterName = String(describing: filter)
if !allowedKeys.contains(filterName) {
return false
}
return true
}
}
if let (size, cornerRadius) = self.validLayout {
effectView.frame = CGRect(origin: CGPoint(), size: size)
ContainedViewLayoutTransition.immediate.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius)
effectView.clipsToBounds = !cornerRadius.isZero
}
self.effectView = effectView
self.view.insertSubview(effectView, at: 0)
}
} else if let effectView = self.effectView {
self.effectView = nil
effectView.removeFromSuperview()
}
}
// update clolor and blur
public func updateColor(color: UIColor, enableBlur: Bool? = nil, forceKeepBlur: Bool = false, transition:
ContainedViewLayoutTransition) {
let effectiveEnableBlur = enableBlur ?? self.enableBlur
if self._color.isEqual(color) && self.enableBlur == effectiveEnableBlur {
return
}
self._color = color
self.enableBlur = effectiveEnableBlur
if sharedIsReduceTransparencyEnabled {// without transparency
transition.updateBackgroundColor(node: self.backgroundNode, color: self._color.withAlphaComponent(1.0))
} else {
transition.updateBackgroundColor(node: self.backgroundNode, color: self._color)
}
self.updateBackgroundBlur(forceKeepBlur: forceKeepBlur)
}
// update size, cornerRadius
public func update(size: CGSize, cornerRadius: CGFloat = 0.0, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, cornerRadius)
let contentFrame = CGRect(origin: CGPoint(), size: size)
transition.updateFrame(node: self.backgroundNode, frame: contentFrame, beginWithCurrentState: true)
if let effectView = self.effectView, effectView.frame != contentFrame {
transition.updateFrame(layer: effectView.layer, frame: contentFrame, beginWithCurrentState: true)
if let sublayers = effectView.layer.sublayers {
for sublayer in sublayers {
transition.updateFrame(layer: sublayer, frame: contentFrame, beginWithCurrentState: true)
}
}
}
transition.updateCornerRadius(node: self.backgroundNode, cornerRadius: cornerRadius)
if let effectView = self.effectView {
transition.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius)
effectView.clipsToBounds = !cornerRadius.isZero
}
}
public func update(size: CGSize, cornerRadius: CGFloat = 0.0, animator: ControlledTransitionAnimator) {
self.validLayout = (size, cornerRadius)
let contentFrame = CGRect(origin: CGPoint(), size: size)
animator.updateFrame(layer: self.backgroundNode.layer, frame: contentFrame, completion: nil)
if let effectView = self.effectView, effectView.frame != contentFrame {
animator.updateFrame(layer: effectView.layer, frame: contentFrame, completion: nil)
if let sublayers = effectView.layer.sublayers {
for sublayer in sublayers {
animator.updateFrame(layer: sublayer, frame: contentFrame, completion: nil)
}
}
}
animator.updateCornerRadius(layer: self.backgroundNode.layer, cornerRadius: cornerRadius, completion: nil)
if let effectView = self.effectView {
animator.updateCornerRadius(layer: effectView.layer, cornerRadius: cornerRadius, completion: nil)
effectView.clipsToBounds = !cornerRadius.isZero
}
}