This repository has been archived by the owner on Feb 17, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 265
/
LayoutArrangement.swift
183 lines (159 loc) · 7.8 KB
/
LayoutArrangement.swift
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// Copyright 2016 LinkedIn Corp.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import CoreGraphics
/**
The frame of a layout and the frames of its sublayouts.
*/
public struct LayoutArrangement {
public let layout: Layout
public let frame: CGRect
public let sublayouts: [LayoutArrangement]
public init(layout: Layout, frame: CGRect, sublayouts: [LayoutArrangement]) {
self.layout = layout
self.frame = frame
self.sublayouts = sublayouts
}
/**
Creates the views for the layout and adds them as subviews to the provided view.
Existing subviews of the provided view will be removed.
If no view is provided, then a new one is created and returned.
MUST be run on the main thread.
- parameter view: The layout's views will be added as subviews to this view, if provided.
- parameter direction: The natural direction of the layout (default: .LeftToRight).
If it does not match the user's language direction, then the layout's views will be flipped horizontally.
Only provide this parameter if you want to test the flipped version of your layout,
or if your layouts are declared for right-to-left languages and you want them to get flipped for left-to-right languages.
- returns: The root view. If a view was provided, then the same view will be returned, otherwise, a new one will be created.
*/
@discardableResult
public func makeViews(in view: View? = nil, direction: UserInterfaceLayoutDirection = .leftToRight) -> View {
return makeViews(in: view, direction: direction, prepareAnimation: false)
}
/**
Prepares the view to be animated to this arrangement.
Call `prepareAnimation(for:direction)` before the animation block.
Call the returned animation's `apply()` method inside the animation block.
```
let animation = nextLayout.arrangement().prepareAnimation(for: rootView, direction: .RightToLeft)
View.animateWithDuration(5.0, animations: {
animation.apply()
})
```
Subviews are reparented for the new arrangement, if necessary, but frames are adjusted so locations don't change.
No frames or configurations of the new arrangement are applied until `apply()` is called on the returned animation object.
MUST be run on the main thread.
*/
public func prepareAnimation(for view: View, direction: UserInterfaceLayoutDirection = .leftToRight) -> Animation {
makeViews(in: view, direction: direction, prepareAnimation: true)
return Animation(arrangement: self, rootView: view, direction: direction)
}
/**
Helper function for `makeViews(in:direction:)` and `prepareAnimation(for:direction:)`.
See the documentation for those two functions.
*/
@discardableResult
private func makeViews(in view: View? = nil, direction: UserInterfaceLayoutDirection, prepareAnimation: Bool) -> View {
let recycler = ViewRecycler(rootView: view)
let views = makeSubviews(from: recycler, prepareAnimation: prepareAnimation)
let rootView: View
if let view = view {
for subview in views {
view.addSubview(subview, maintainCoordinates: prepareAnimation)
}
rootView = view
// In this case, the `rootView` is the view that was passed in. It is not created for this layout arrangement
// but merely hosts it. Therefore, the subview(s) that are being added to it are the root-most views from
// the LayoutKit view recycling perspective.
recycler.markViewsAsRoot(views)
} else if let view = views.first, views.count == 1 {
// We have a single view so it is our root view.
rootView = view
recycler.markViewsAsRoot(views)
} else {
// We have multiple views so create a root view.
rootView = View(frame: frame)
for subview in views {
if !prepareAnimation {
// Unapply the offset that was applied in makeSubviews()
subview.frame = subview.frame.offsetBy(dx: -frame.origin.x, dy: -frame.origin.y)
}
rootView.addSubview(subview)
}
// The generated root view that's being returned is the root-most one that is created by LayoutKit,
// so it is the one that should be marked as the root by the recycler.
recycler.markViewsAsRoot([rootView])
}
recycler.purgeViews()
if !prepareAnimation {
// Horizontally flip the view frames if direction does not match the root view's language direction.
if rootView.userInterfaceLayoutDirection != direction {
flipSubviewsHorizontally(rootView)
}
}
return rootView
}
/// Flips the right and left edges of the view's subviews.
private func flipSubviewsHorizontally(_ view: View) {
for subview in view.subviews {
subview.frame.origin.x = view.frame.width - subview.frame.maxX
flipSubviewsHorizontally(subview)
}
}
/// Returns the views for the layout and all of its sublayouts.
private func makeSubviews(from recycler: ViewRecycler, prepareAnimation: Bool) -> [View] {
let subviews = sublayouts.flatMap({ (sublayout: LayoutArrangement) -> [View] in
return sublayout.makeSubviews(from: recycler, prepareAnimation: prepareAnimation)
})
// If we are preparing an animation, then we don't want to update frames or configure views.
if layout.needsView, let view = recycler.makeOrRecycleView(havingViewReuseId: layout.viewReuseId, viewProvider: layout.makeView) {
if !prepareAnimation {
view.frame = frame
layout.configure(baseTypeView: view)
}
for subview in subviews {
// If a view gets reparented and we are preparing an animation, then
// make sure that its absolute position on the screen does not change.
view.addSubview(subview, maintainCoordinates: prepareAnimation)
}
return [view]
} else {
if !prepareAnimation {
for subview in subviews {
subview.frame = subview.frame.offsetBy(dx: frame.origin.x, dy: frame.origin.y)
}
}
return subviews
}
}
}
extension LayoutArrangement: CustomDebugStringConvertible {
public var debugDescription: String {
return _debugDescription(0)
}
private func _debugDescription(_ indent: Int) -> String {
let t = String(repeatElement(" ", count: indent * 2))
let sublayoutsString = sublayouts.map { $0._debugDescription(indent + 1) }.joined()
let layoutName = String(describing: layout).components(separatedBy: ".").last!
return"\(t)\(layoutName): \(frame)\n\(sublayoutsString)"
}
}
extension View {
/**
Similar to `addSubview()` except if `maintainCoordinates` is true, then the view's frame
will be adjusted so that its absolute position on the screen does not change.
*/
fileprivate func addSubview(_ view: View, maintainCoordinates: Bool) {
if maintainCoordinates {
let frame = view.convertToAbsoluteCoordinates(view.frame)
addSubview(view)
view.frame = view.convertFromAbsoluteCoordinates(frame)
} else {
addSubview(view)
}
}
}