266 lines
11 KiB
Swift
266 lines
11 KiB
Swift
//
|
|
// GroupCallVideoFlowLayout.swift
|
|
// TUICallKit
|
|
//
|
|
// Created by noah on 2023/11/1.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
|
|
public var showLargeViewIndex: Int = -1
|
|
|
|
enum MosaicSegmentStyle {
|
|
case fullWidth
|
|
case fiftyFifty
|
|
case oneThird
|
|
case threeOneThirds
|
|
case twoThirdsOneThirdRight
|
|
case twoThirdsOneThirdCenter
|
|
case oneThirdTwoThirds
|
|
}
|
|
|
|
class GroupCallVideoFlowLayout: UICollectionViewFlowLayout {
|
|
private var deletingIndexPaths = [IndexPath]()
|
|
private var insertingIndexPaths = [IndexPath]()
|
|
|
|
private var contentBounds = CGRect.zero
|
|
private var cachedAttributes = [UICollectionViewLayoutAttributes]()
|
|
|
|
override func prepare() {
|
|
super.prepare()
|
|
|
|
guard let collectionView = collectionView else { return }
|
|
|
|
cachedAttributes.removeAll()
|
|
contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
|
|
|
|
let count = collectionView.numberOfItems(inSection: 0)
|
|
var currentIndex = 0
|
|
var segment: MosaicSegmentStyle = getSegment(count: count, currentIndex: currentIndex)
|
|
let cvWidth = collectionView.bounds.size.width
|
|
var lastFrame: CGRect = (count != 2 || showLargeViewIndex >= 0) ? .zero : CGRect(x: 0, y: cvWidth / 5, width: 0, height: 0)
|
|
|
|
while currentIndex < count {
|
|
var segmentRects = [CGRect]()
|
|
switch segment {
|
|
case .fullWidth:
|
|
segmentRects = [CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth)]
|
|
|
|
case .fiftyFifty:
|
|
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth / 2)
|
|
let horizontalSlices = segmentFrame.dividedIntegral(fraction: 0.5, from: .minXEdge)
|
|
segmentRects = [horizontalSlices.first, horizontalSlices.second]
|
|
|
|
case .oneThird:
|
|
segmentRects = [CGRect(x: cvWidth / 4.0, y: lastFrame.maxY + 1.0, width: cvWidth / 2.0, height: cvWidth / 2.0)]
|
|
|
|
case .threeOneThirds:
|
|
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth / 3)
|
|
let horizontalSlicesFirst = segmentFrame.dividedIntegral(fraction: 1.0 / 3, from: .minXEdge)
|
|
let horizontalSlices = horizontalSlicesFirst.second.dividedIntegral(fraction: 0.5, from: .minXEdge)
|
|
segmentRects = [horizontalSlicesFirst.first, horizontalSlices.first, horizontalSlices.second]
|
|
|
|
case .twoThirdsOneThirdRight:
|
|
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
|
|
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3), from: .minXEdge)
|
|
let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
|
|
segmentRects = [verticalSlices.first, verticalSlices.second, horizontalSlices.second]
|
|
|
|
case .twoThirdsOneThirdCenter:
|
|
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
|
|
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (1.0 / 3), from: .minXEdge)
|
|
let verticalSlices = horizontalSlices.first.dividedIntegral(fraction: 0.5, from: .minYEdge)
|
|
segmentRects = [verticalSlices.first, horizontalSlices.second, verticalSlices.second]
|
|
|
|
case .oneThirdTwoThirds:
|
|
let segmentFrame = CGRect(x: 0, y: lastFrame.maxY + 1.0, width: cvWidth, height: cvWidth * 2 / 3)
|
|
let horizontalSlices = segmentFrame.dividedIntegral(fraction: (2.0 / 3), from: .minXEdge)
|
|
let verticalSlices = horizontalSlices.second.dividedIntegral(fraction: 0.5, from: .minYEdge)
|
|
segmentRects = [horizontalSlices.first, verticalSlices.first, verticalSlices.second]
|
|
}
|
|
|
|
// Create and cache layout attributes for calculated frames.
|
|
for rect in segmentRects {
|
|
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: currentIndex, section: 0))
|
|
attributes.frame = rect
|
|
|
|
cachedAttributes.append(attributes)
|
|
contentBounds = contentBounds.union(lastFrame)
|
|
|
|
currentIndex += 1
|
|
lastFrame = rect
|
|
}
|
|
|
|
segment = getSegment(count: count, currentIndex: currentIndex)
|
|
}
|
|
}
|
|
|
|
func getSegment(count: Int, currentIndex: Int) -> MosaicSegmentStyle {
|
|
var segment: MosaicSegmentStyle
|
|
|
|
// Determine first segment style.
|
|
if currentIndex == 0 {
|
|
var segment: MosaicSegmentStyle = .threeOneThirds
|
|
|
|
if count == 1 {
|
|
segment = .fullWidth
|
|
} else if count >= 2 && count <= 4 {
|
|
if showLargeViewIndex >= 0 {
|
|
segment = .fullWidth
|
|
} else {
|
|
segment = .fiftyFifty
|
|
}
|
|
} else if showLargeViewIndex == 0 {
|
|
segment = .oneThirdTwoThirds
|
|
} else if showLargeViewIndex == 1 {
|
|
segment = .twoThirdsOneThirdCenter
|
|
} else if showLargeViewIndex == 2 {
|
|
segment = .twoThirdsOneThirdRight
|
|
}
|
|
|
|
return segment
|
|
}
|
|
|
|
// Determine the next segment style.
|
|
switch count - currentIndex {
|
|
case 1:
|
|
if count == 3 {
|
|
segment = .oneThird
|
|
} else if count > 4 && showLargeViewIndex == (count - 1) {
|
|
segment = .oneThirdTwoThirds
|
|
} else {
|
|
segment = .threeOneThirds
|
|
}
|
|
case 2:
|
|
if count == 4 {
|
|
segment = .fiftyFifty
|
|
} else if count > 4 && showLargeViewIndex == currentIndex {
|
|
segment = .oneThirdTwoThirds
|
|
} else if count > 4 && (showLargeViewIndex == currentIndex + 1) {
|
|
segment = .twoThirdsOneThirdCenter
|
|
} else if count > 4 && (showLargeViewIndex == currentIndex + 2) {
|
|
segment = .twoThirdsOneThirdRight
|
|
} else {
|
|
segment = .threeOneThirds
|
|
}
|
|
default:
|
|
if count > 4 && showLargeViewIndex == currentIndex {
|
|
segment = .oneThirdTwoThirds
|
|
} else if count > 4 && (showLargeViewIndex == currentIndex + 1) {
|
|
segment = .twoThirdsOneThirdCenter
|
|
} else if count > 4 && (showLargeViewIndex == currentIndex + 2) {
|
|
segment = .twoThirdsOneThirdRight
|
|
} else {
|
|
segment = .threeOneThirds
|
|
}
|
|
break
|
|
}
|
|
|
|
return segment
|
|
}
|
|
|
|
override var collectionViewContentSize: CGSize {
|
|
return contentBounds.size
|
|
}
|
|
|
|
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
|
guard let collectionView = collectionView else { return false }
|
|
return !newBounds.size.equalTo(collectionView.bounds.size)
|
|
}
|
|
|
|
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
return cachedAttributes[indexPath.item]
|
|
}
|
|
|
|
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
guard let attributes = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) else { return nil }
|
|
|
|
if !deletingIndexPaths.isEmpty {
|
|
if deletingIndexPaths.contains(itemIndexPath) {
|
|
attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
|
attributes.alpha = 0.0
|
|
attributes.zIndex = 0
|
|
}
|
|
}
|
|
|
|
return attributes
|
|
}
|
|
|
|
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
guard let attributes = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath) else { return nil }
|
|
|
|
if insertingIndexPaths.contains(itemIndexPath) {
|
|
attributes.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
|
|
attributes.alpha = 0.0
|
|
attributes.zIndex = 0
|
|
}
|
|
|
|
return attributes
|
|
}
|
|
|
|
override func prepare(forCollectionViewUpdates updateItems: [UICollectionViewUpdateItem]) {
|
|
super.prepare(forCollectionViewUpdates: updateItems)
|
|
|
|
for update in updateItems {
|
|
switch update.updateAction {
|
|
case .delete:
|
|
guard let indexPath = update.indexPathBeforeUpdate else { return }
|
|
deletingIndexPaths.append(indexPath)
|
|
case .insert:
|
|
guard let indexPath = update.indexPathAfterUpdate else { return }
|
|
insertingIndexPaths.append(indexPath)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
override func finalizeCollectionViewUpdates() {
|
|
super.finalizeCollectionViewUpdates()
|
|
|
|
deletingIndexPaths.removeAll()
|
|
insertingIndexPaths.removeAll()
|
|
}
|
|
|
|
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
|
var attributesArray = [UICollectionViewLayoutAttributes]()
|
|
|
|
// Find any cell that sits within the query rect.
|
|
guard let lastIndex = cachedAttributes.indices.last,
|
|
let firstMatchIndex = binSearch(rect, start: 0, end: lastIndex) else { return attributesArray }
|
|
|
|
// Starting from the match, loop up and down through the array until all the attributes
|
|
// have been added within the query rect.
|
|
for attributes in cachedAttributes[..<firstMatchIndex].reversed() {
|
|
guard attributes.frame.maxY >= rect.minY else { break }
|
|
attributesArray.append(attributes)
|
|
}
|
|
|
|
for attributes in cachedAttributes[firstMatchIndex...] {
|
|
guard attributes.frame.minY <= rect.maxY else { break }
|
|
attributesArray.append(attributes)
|
|
}
|
|
|
|
return attributesArray
|
|
}
|
|
|
|
// Perform a binary search on the cached attributes array.
|
|
func binSearch(_ rect: CGRect, start: Int, end: Int) -> Int? {
|
|
if end < start { return nil }
|
|
|
|
let mid = (start + end) / 2
|
|
let attributes = cachedAttributes[mid]
|
|
|
|
if attributes.frame.intersects(rect) {
|
|
return mid
|
|
} else {
|
|
if attributes.frame.maxY < rect.minY {
|
|
return binSearch(rect, start: (mid + 1), end: end)
|
|
} else {
|
|
return binSearch(rect, start: start, end: (mid - 1))
|
|
}
|
|
}
|
|
}
|
|
}
|