@nativescript-community/ui-collectionview-alignedflowlayout 5.3.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,494 @@
1
+ //
2
+ // AlignedCollectionViewFlowLayout.swift
3
+ //
4
+ // Created by Mischa Hildebrand on 12/04/2017.
5
+ // Copyright © 2017 Mischa Hildebrand.
6
+ //
7
+ // Licensed under the terms of the MIT license:
8
+ //
9
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ // of this software and associated documentation files (the "Software"), to deal
11
+ // in the Software without restriction, including without limitation the rights
12
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ // copies of the Software, and to permit persons to whom the Software is
14
+ // furnished to do so, subject to the following conditions:
15
+ //
16
+ // The above copyright notice and this permission notice shall be included in
17
+ // all copies or substantial portions of the Software.
18
+ //
19
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ // THE SOFTWARE.
26
+ //
27
+
28
+ import UIKit
29
+ import Foundation
30
+
31
+ // MARK: - 🦆 Type definitions
32
+
33
+ /// An abstract protocol that defines an alignment.
34
+ protocol Alignment {}
35
+
36
+ /// Defines a horizontal alignment for UI elements.
37
+ @objc public enum HorizontalAlignment: Int, Alignment {
38
+ case left
39
+ case right
40
+ case leading
41
+ case trailing
42
+ case justified
43
+ }
44
+
45
+ /// Defines a vertical alignment for UI elements.
46
+ @objc public enum VerticalAlignment: Int, Alignment {
47
+ case top
48
+ case center
49
+ case bottom
50
+ }
51
+
52
+ /// A horizontal alignment used internally by `AlignedCollectionViewFlowLayout`
53
+ /// to layout the items, after resolving layout direction specifics.
54
+ private enum EffectiveHorizontalAlignment: Alignment {
55
+ case left
56
+ case right
57
+ case justified
58
+ }
59
+
60
+ /// Describes an axis with respect to which items can be aligned.
61
+ private struct AlignmentAxis<A: Alignment> {
62
+
63
+ /// Determines how items are aligned relative to the axis.
64
+ let alignment: A
65
+
66
+ /// Defines the position of the axis.
67
+ /// * If the `Alignment` is horizontal, the alignment axis is vertical and this is the position on the `x` axis.
68
+ /// * If the `Alignment` is vertical, the alignment axis is horizontal and this is the position on the `y` axis.
69
+ let position: CGFloat
70
+ }
71
+
72
+
73
+
74
+ // MARK: - Flow Layout
75
+
76
+ /// A `UICollectionViewFlowLayout` subclass that gives you control
77
+ /// over the horizontal and vertical alignment of the cells.
78
+ /// You can use it to align the cells like words in a left- or right-aligned text
79
+ /// and you can specify how the cells are vertically aligned in their row.
80
+ @objcMembers
81
+ @objc(AlignedCollectionViewFlowLayout)
82
+ open class AlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
83
+
84
+ // MARK: - 🔶 Properties
85
+
86
+ /// Determines how the cells are horizontally aligned in a row.
87
+ /// - Note: The default is `.justified`.
88
+ public var horizontalAlignment: HorizontalAlignment = .justified
89
+
90
+ /// Determines how the cells are vertically aligned in a row.
91
+ /// - Note: The default is `.center`.
92
+ public var verticalAlignment: VerticalAlignment = .center
93
+
94
+ /// The `horizontalAlignment` with its layout direction specifics resolved,
95
+ /// i.e. `.leading` and `.trailing` alignments are mapped to `.left` or `right`,
96
+ /// depending on the current layout direction.
97
+ fileprivate var effectiveHorizontalAlignment: EffectiveHorizontalAlignment {
98
+
99
+ var trivialMapping: [HorizontalAlignment: EffectiveHorizontalAlignment] {
100
+ return [
101
+ .left: .left,
102
+ .right: .right,
103
+ .justified: .justified
104
+ ]
105
+ }
106
+
107
+ let layoutDirection = UIApplication.shared.userInterfaceLayoutDirection
108
+
109
+ switch layoutDirection {
110
+ case .leftToRight:
111
+ switch horizontalAlignment {
112
+ case .leading:
113
+ return .left
114
+ case .trailing:
115
+ return .right
116
+ default:
117
+ break
118
+ }
119
+
120
+ case .rightToLeft:
121
+ switch horizontalAlignment {
122
+ case .leading:
123
+ return .right
124
+ case .trailing:
125
+ return .left
126
+ default:
127
+ break
128
+ }
129
+ }
130
+
131
+ // It's safe to force-unwrap as `.leading` and `.trailing` are covered
132
+ // above and the `trivialMapping` dictionary contains all other keys.
133
+ return trivialMapping[horizontalAlignment]!
134
+ }
135
+
136
+ /// The vertical axis with respect to which the cells are horizontally aligned.
137
+ /// For a `justified` alignment the alignment axis is not defined and this value is `nil`.
138
+ fileprivate var alignmentAxis: AlignmentAxis<HorizontalAlignment>? {
139
+ switch effectiveHorizontalAlignment {
140
+ case .left:
141
+ return AlignmentAxis(alignment: HorizontalAlignment.left, position: sectionInset.left)
142
+ case .right:
143
+ guard let collectionViewWidth = collectionView?.frame.size.width else {
144
+ return nil
145
+ }
146
+ return AlignmentAxis(alignment: HorizontalAlignment.right, position: collectionViewWidth - sectionInset.right)
147
+ default:
148
+ return nil
149
+ }
150
+ }
151
+
152
+ /// The width of the area inside the collection view that can be filled with cells.
153
+ private var contentWidth: CGFloat? {
154
+ guard let collectionViewWidth = collectionView?.frame.size.width else {
155
+ return nil
156
+ }
157
+ return collectionViewWidth - sectionInset.left - sectionInset.right
158
+ }
159
+
160
+
161
+ // MARK: - 👶 Initialization
162
+
163
+ /// The designated initializer.
164
+ ///
165
+ /// - Parameters:
166
+ /// - horizontalAlignment: Specifies how the cells are horizontally aligned in a row. --
167
+ /// (Default: `.justified`)
168
+ /// - verticalAlignment: Specified how the cells are vertically aligned in a row. --
169
+ /// (Default: `.center`)
170
+ public init(horizontalAlignment: HorizontalAlignment = .justified, verticalAlignment: VerticalAlignment = .center) {
171
+ super.init()
172
+ self.horizontalAlignment = horizontalAlignment
173
+ self.verticalAlignment = verticalAlignment
174
+ }
175
+ override public init() {
176
+ super.init()
177
+ }
178
+
179
+ required public init?(coder aDecoder: NSCoder) {
180
+ super.init(coder: aDecoder)
181
+ }
182
+
183
+
184
+ // MARK: - 🅾️ Overrides
185
+
186
+ override open func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
187
+
188
+ // 💡 IDEA:
189
+ // The approach for computing a cell's frame is to create a rectangle that covers the current line.
190
+ // Then we check if the preceding cell's frame intersects with this rectangle.
191
+ // If it does, the current item is not the first item in the line. Otherwise it is.
192
+ // (Vice-versa for right-aligned cells.)
193
+ //
194
+ // +---------+----------------------------------------------------------------+---------+
195
+ // | | | |
196
+ // | | +------------+ | |
197
+ // | | | | | |
198
+ // | section |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| section |
199
+ // | inset | |intersection| | | line rect | inset |
200
+ // | |- - -|- - - - - - |- - - - +---------------------+ - - - - - - -| |
201
+ // | (left) | | | current item | (right) |
202
+ // | | +------------+ | |
203
+ // | | previous item | |
204
+ // +---------+----------------------------------------------------------------+---------+
205
+ //
206
+ // ℹ️ We need this rather complicated approach because the first item in a line
207
+ // is not always left-aligned and the last item in a line is not always right-aligned:
208
+ // If there is only one item in a line UICollectionViewFlowLayout will center it.
209
+
210
+ // We may not change the original layout attributes or UICollectionViewFlowLayout might complain.
211
+ guard let layoutAttributes = super.layoutAttributesForItem(at: indexPath)?.copy() as? UICollectionViewLayoutAttributes else {
212
+ return nil
213
+ }
214
+
215
+ // For a justified layout there's nothing to do here
216
+ // as UICollectionViewFlowLayout justifies the items in a line by default.
217
+ if horizontalAlignment != .justified {
218
+ layoutAttributes.alignHorizontally(collectionViewLayout: self)
219
+ }
220
+
221
+ // For a vertically centered layout there's nothing to do here
222
+ // as UICollectionViewFlowLayout center-aligns the items in a line by default.
223
+ if verticalAlignment != .center {
224
+ layoutAttributes.alignVertically(collectionViewLayout: self)
225
+ }
226
+
227
+ return layoutAttributes
228
+ }
229
+
230
+ override open func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
231
+ // We may not change the original layout attributes or UICollectionViewFlowLayout might complain.
232
+ let layoutAttributesObjects = copy(super.layoutAttributesForElements(in: rect))
233
+ layoutAttributesObjects?.forEach({ (layoutAttributes) in
234
+ setFrame(forLayoutAttributes: layoutAttributes)
235
+ })
236
+ return layoutAttributesObjects
237
+ }
238
+
239
+
240
+ // MARK: - 👷 Private layout helpers
241
+
242
+ /// Sets the frame for the passed layout attributes object by calling the `layoutAttributesForItem(at:)` function.
243
+ private func setFrame(forLayoutAttributes layoutAttributes: UICollectionViewLayoutAttributes) {
244
+ if layoutAttributes.representedElementCategory == .cell { // Do not modify header views etc.
245
+ let indexPath = layoutAttributes.indexPath
246
+ if let newFrame = layoutAttributesForItem(at: indexPath)?.frame {
247
+ layoutAttributes.frame = newFrame
248
+ }
249
+ }
250
+ }
251
+
252
+ /// A function to access the `super` implementation of `layoutAttributesForItem(at:)` externally.
253
+ ///
254
+ /// - Parameter indexPath: The index path of the item for which to return the layout attributes.
255
+ /// - Returns: The unmodified layout attributes for the item at the specified index path
256
+ /// as computed by `UICollectionViewFlowLayout`.
257
+ fileprivate func originalLayoutAttribute(forItemAt indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
258
+ return super.layoutAttributesForItem(at: indexPath)
259
+ }
260
+
261
+ /// Determines if the `firstItemAttributes`' frame is in the same line
262
+ /// as the `secondItemAttributes`' frame.
263
+ ///
264
+ /// - Parameters:
265
+ /// - firstItemAttributes: The first layout attributes object to be compared.
266
+ /// - secondItemAttributes: The second layout attributes object to be compared.
267
+ /// - Returns: `true` if the frames of the two layout attributes are in the same line, else `false`.
268
+ /// `false` is also returned when the layout's `collectionView` property is `nil`.
269
+ fileprivate func isFrame(for firstItemAttributes: UICollectionViewLayoutAttributes, inSameLineAsFrameFor secondItemAttributes: UICollectionViewLayoutAttributes) -> Bool {
270
+ guard let lineWidth = contentWidth else {
271
+ return false
272
+ }
273
+ let firstItemFrame = firstItemAttributes.frame
274
+ let lineFrame = CGRect(x: sectionInset.left,
275
+ y: firstItemFrame.origin.y,
276
+ width: lineWidth,
277
+ height: firstItemFrame.size.height)
278
+ return lineFrame.intersects(secondItemAttributes.frame)
279
+ }
280
+
281
+ /// Determines the layout attributes objects for all items displayed in the same line as the item
282
+ /// represented by the passed `layoutAttributes` object.
283
+ ///
284
+ /// - Parameter layoutAttributes: The layout attributed that represents the reference item.
285
+ /// - Returns: The layout attributes objects representing all other items in the same line.
286
+ /// The passed `layoutAttributes` object itself is always contained in the returned array.
287
+ fileprivate func layoutAttributes(forItemsInLineWith layoutAttributes: UICollectionViewLayoutAttributes) -> [UICollectionViewLayoutAttributes] {
288
+ guard let lineWidth = contentWidth else {
289
+ return [layoutAttributes]
290
+ }
291
+ var lineFrame = layoutAttributes.frame
292
+ lineFrame.origin.x = sectionInset.left
293
+ lineFrame.size.width = lineWidth
294
+ return super.layoutAttributesForElements(in: lineFrame) ?? []
295
+ }
296
+
297
+ /// Copmutes the alignment axis with which to align the items represented by the `layoutAttributes` objects vertically.
298
+ ///
299
+ /// - Parameter layoutAttributes: The layout attributes objects to be vertically aligned.
300
+ /// - Returns: The axis with respect to which the layout attributes can be aligned
301
+ /// or `nil` if the `layoutAttributes` array is empty.
302
+ private func verticalAlignmentAxisForLine(with layoutAttributes: [UICollectionViewLayoutAttributes]) -> AlignmentAxis<VerticalAlignment>? {
303
+
304
+ guard let firstAttribute = layoutAttributes.first else {
305
+ return nil
306
+ }
307
+
308
+ switch verticalAlignment {
309
+ case .top:
310
+ let minY = layoutAttributes.reduce(CGFloat.greatestFiniteMagnitude) { min($0, $1.frame.minY) }
311
+ return AlignmentAxis(alignment: .top, position: minY)
312
+
313
+ case .bottom:
314
+ let maxY = layoutAttributes.reduce(0) { max($0, $1.frame.maxY) }
315
+ return AlignmentAxis(alignment: .bottom, position: maxY)
316
+
317
+ default:
318
+ let centerY = firstAttribute.center.y
319
+ return AlignmentAxis(alignment: .center, position: centerY)
320
+ }
321
+ }
322
+
323
+ /// Computes the axis with which to align the item represented by the `currentLayoutAttributes` vertically.
324
+ ///
325
+ /// - Parameter currentLayoutAttributes: The layout attributes representing the item to be vertically aligned.
326
+ /// - Returns: The axis with respect to which the item can be aligned.
327
+ fileprivate func verticalAlignmentAxis(for currentLayoutAttributes: UICollectionViewLayoutAttributes) -> AlignmentAxis<VerticalAlignment> {
328
+ let layoutAttributesInLine = layoutAttributes(forItemsInLineWith: currentLayoutAttributes)
329
+ // It's okay to force-unwrap here because we pass a non-empty array.
330
+ return verticalAlignmentAxisForLine(with: layoutAttributesInLine)!
331
+ }
332
+
333
+ /// Creates a deep copy of the passed array by copying all its items.
334
+ ///
335
+ /// - Parameter layoutAttributesArray: The array to be copied.
336
+ /// - Returns: A deep copy of the passed array.
337
+ private func copy(_ layoutAttributesArray: [UICollectionViewLayoutAttributes]?) -> [UICollectionViewLayoutAttributes]? {
338
+ return layoutAttributesArray?.map{ $0.copy() } as? [UICollectionViewLayoutAttributes]
339
+ }
340
+
341
+ }
342
+
343
+
344
+
345
+ // MARK: - 👷 Layout attributes helpers
346
+
347
+ fileprivate extension UICollectionViewLayoutAttributes {
348
+
349
+ private var currentSection: Int {
350
+ return indexPath.section
351
+ }
352
+
353
+ private var currentItem: Int {
354
+ return indexPath.item
355
+ }
356
+
357
+ /// The index path for the item preceding the item represented by this layout attributes object.
358
+ private var precedingIndexPath: IndexPath {
359
+ return IndexPath(item: currentItem - 1, section: currentSection)
360
+ }
361
+
362
+ /// The index path for the item following the item represented by this layout attributes object.
363
+ private var followingIndexPath: IndexPath {
364
+ return IndexPath(item: currentItem + 1, section: currentSection)
365
+ }
366
+
367
+ /// Checks if the item represetend by this layout attributes object is the first item in the line.
368
+ ///
369
+ /// - Parameter collectionViewLayout: The layout for which to perform the check.
370
+ /// - Returns: `true` if the represented item is the first item in the line, else `false`.
371
+ func isRepresentingFirstItemInLine(collectionViewLayout: AlignedCollectionViewFlowLayout) -> Bool {
372
+ if currentItem <= 0 {
373
+ return true
374
+ }
375
+ else {
376
+ if let layoutAttributesForPrecedingItem = collectionViewLayout.originalLayoutAttribute(forItemAt: precedingIndexPath) {
377
+ return !collectionViewLayout.isFrame(for: self, inSameLineAsFrameFor: layoutAttributesForPrecedingItem)
378
+ }
379
+ else {
380
+ return true
381
+ }
382
+ }
383
+ }
384
+
385
+ /// Checks if the item represetend by this layout attributes object is the last item in the line.
386
+ ///
387
+ /// - Parameter collectionViewLayout: The layout for which to perform the check.
388
+ /// - Returns: `true` if the represented item is the last item in the line, else `false`.
389
+ func isRepresentingLastItemInLine(collectionViewLayout: AlignedCollectionViewFlowLayout) -> Bool {
390
+ guard let itemCount = collectionViewLayout.collectionView?.numberOfItems(inSection: currentSection) else {
391
+ return false
392
+ }
393
+
394
+ if currentItem >= itemCount - 1 {
395
+ return true
396
+ }
397
+ else {
398
+ if let layoutAttributesForFollowingItem = collectionViewLayout.originalLayoutAttribute(forItemAt: followingIndexPath) {
399
+ return !collectionViewLayout.isFrame(for: self, inSameLineAsFrameFor: layoutAttributesForFollowingItem)
400
+ }
401
+ else {
402
+ return true
403
+ }
404
+ }
405
+ }
406
+
407
+ /// Moves the layout attributes object's frame so that it is aligned horizontally with the alignment axis.
408
+ func align(toAlignmentAxis alignmentAxis: AlignmentAxis<HorizontalAlignment>) {
409
+ switch alignmentAxis.alignment {
410
+ case .left:
411
+ frame.origin.x = alignmentAxis.position
412
+ case .right:
413
+ frame.origin.x = alignmentAxis.position - frame.size.width
414
+ default:
415
+ break
416
+ }
417
+ }
418
+
419
+ /// Moves the layout attributes object's frame so that it is aligned vertically with the alignment axis.
420
+ func align(toAlignmentAxis alignmentAxis: AlignmentAxis<VerticalAlignment>) {
421
+ switch alignmentAxis.alignment {
422
+ case .top:
423
+ frame.origin.y = alignmentAxis.position
424
+ case .bottom:
425
+ frame.origin.y = alignmentAxis.position - frame.size.height
426
+ default:
427
+ center.y = alignmentAxis.position
428
+ }
429
+ }
430
+
431
+ /// Positions the frame right of the preceding item's frame, leaving a spacing between the frames
432
+ /// as defined by the collection view layout's `minimumInteritemSpacing`.
433
+ ///
434
+ /// - Parameter collectionViewLayout: The layout on which to perfom the calculations.
435
+ private func alignToPrecedingItem(collectionViewLayout: AlignedCollectionViewFlowLayout) {
436
+ let itemSpacing = collectionViewLayout.minimumInteritemSpacing
437
+
438
+ if let precedingItemAttributes = collectionViewLayout.layoutAttributesForItem(at: precedingIndexPath) {
439
+ frame.origin.x = precedingItemAttributes.frame.maxX + itemSpacing
440
+ }
441
+ }
442
+
443
+ /// Positions the frame left of the following item's frame, leaving a spacing between the frames
444
+ /// as defined by the collection view layout's `minimumInteritemSpacing`.
445
+ ///
446
+ /// - Parameter collectionViewLayout: The layout on which to perfom the calculations.
447
+ private func alignToFollowingItem(collectionViewLayout: AlignedCollectionViewFlowLayout) {
448
+ let itemSpacing = collectionViewLayout.minimumInteritemSpacing
449
+
450
+ if let followingItemAttributes = collectionViewLayout.layoutAttributesForItem(at: followingIndexPath) {
451
+ frame.origin.x = followingItemAttributes.frame.minX - itemSpacing - frame.size.width
452
+ }
453
+ }
454
+
455
+ /// Aligns the frame horizontally as specified by the collection view layout's `horizontalAlignment`.
456
+ ///
457
+ /// - Parameters:
458
+ /// - collectionViewLayout: The layout providing the alignment information.
459
+ func alignHorizontally(collectionViewLayout: AlignedCollectionViewFlowLayout) {
460
+
461
+ guard let alignmentAxis = collectionViewLayout.alignmentAxis else {
462
+ return
463
+ }
464
+
465
+ switch collectionViewLayout.effectiveHorizontalAlignment {
466
+
467
+ case .left:
468
+ if isRepresentingFirstItemInLine(collectionViewLayout: collectionViewLayout) {
469
+ align(toAlignmentAxis: alignmentAxis)
470
+ } else {
471
+ alignToPrecedingItem(collectionViewLayout: collectionViewLayout)
472
+ }
473
+
474
+ case .right:
475
+ if isRepresentingLastItemInLine(collectionViewLayout: collectionViewLayout) {
476
+ align(toAlignmentAxis: alignmentAxis)
477
+ } else {
478
+ alignToFollowingItem(collectionViewLayout: collectionViewLayout)
479
+ }
480
+
481
+ default:
482
+ return
483
+ }
484
+ }
485
+
486
+ /// Aligns the frame vertically as specified by the collection view layout's `verticalAlignment`.
487
+ ///
488
+ /// - Parameter collectionViewLayout: The layout providing the alignment information.
489
+ func alignVertically(collectionViewLayout: AlignedCollectionViewFlowLayout) {
490
+ let alignmentAxis = collectionViewLayout.verticalAlignmentAxis(for: self)
491
+ align(toAlignmentAxis: alignmentAxis)
492
+ }
493
+
494
+ }
@@ -0,0 +1,37 @@
1
+
2
+ declare class AlignedCollectionViewFlowLayout extends UICollectionViewFlowLayout {
3
+
4
+ static alloc(): AlignedCollectionViewFlowLayout; // inherited from NSObject
5
+
6
+ static new(): AlignedCollectionViewFlowLayout; // inherited from NSObject
7
+
8
+ horizontalAlignment: HorizontalAlignment;
9
+
10
+ verticalAlignment: VerticalAlignment;
11
+
12
+ constructor(o: { horizontalAlignment: HorizontalAlignment; verticalAlignment: VerticalAlignment; });
13
+
14
+ initWithHorizontalAlignmentVerticalAlignment(horizontalAlignment: HorizontalAlignment, verticalAlignment: VerticalAlignment): this;
15
+ }
16
+
17
+ declare const enum HorizontalAlignment {
18
+
19
+ Left = 0,
20
+
21
+ Right = 1,
22
+
23
+ Leading = 2,
24
+
25
+ Trailing = 3,
26
+
27
+ Justified = 4
28
+ }
29
+
30
+ declare const enum VerticalAlignment {
31
+
32
+ Top = 0,
33
+
34
+ Center = 1,
35
+
36
+ Bottom = 2
37
+ }