Skip to content

Commit

Permalink
Add comments
Browse files Browse the repository at this point in the history
  • Loading branch information
Marek Fořt committed Mar 15, 2022
1 parent 9291372 commit d4adf8e
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class AutoLayoutView(context: Context) : ReactViewGroup(context) {
/** Sorts views by index and then invokes clearGaps which does the correction.
* Performance: Sort is needed. Given relatively low number of views in RecyclerListView render tree this should be a non issue.*/
private fun fixLayout() {
// Fixing layout during animation can interfere with it.
if (animation != null) {
return
}
Expand Down
51 changes: 26 additions & 25 deletions ios/Sources/AutoLayoutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,52 +7,52 @@ import UIKit
@objc class AutoLayoutView: UIView {
@objc(onBlankAreaEvent)
var onBlankAreaEvent: RCTDirectEventBlock?

@objc func setHorizontal(_ horizontal: Bool) {
self.horizontal = horizontal
}

@objc func setScrollOffset(_ scrollOffset: Int) {
self.scrollOffset = CGFloat(scrollOffset)
}

@objc func setWindowSize(_ windowSize: Int) {
self.windowSize = CGFloat(windowSize)
}

@objc func setRenderAheadOffset(_ renderAheadOffset: Int) {
self.renderAheadOffset = CGFloat(renderAheadOffset)
}

@objc func setEnableInstrumentation(_ enableInstrumentation: Bool) {
self.enableInstrumentation = enableInstrumentation
}

private var horizontal = false
private var scrollOffset: CGFloat = 0
private var windowSize: CGFloat = 0
private var renderAheadOffset: CGFloat = 0
private var enableInstrumentation = false

/// Tracks where the last pixel is drawn in the visible window
private var lastMaxBound: CGFloat = 0
/// Tracks where first pixel is drawn in the visible window
private var lastMinBound: CGFloat = 0

override func layoutSubviews() {
fixLayout()
super.layoutSubviews()

let scrollView = sequence(first: self, next: { $0.superview }).first(where: { $0 is UIScrollView })
guard enableInstrumentation, let scrollView = scrollView as? UIScrollView else { return }

let scrollContainerSize = horizontal ? scrollView.frame.width : scrollView.frame.height
let currentScrollOffset = horizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y
let startOffset = horizontal ? frame.minX : frame.minY
let endOffset = horizontal ? frame.maxX : frame.maxY
let distanceFromWindowStart = max(startOffset - currentScrollOffset, 0)
let distanceFromWindowEnd = max(currentScrollOffset + scrollContainerSize - endOffset, 0)

let (blankOffsetStart, blankOffsetEnd) = computeBlankFromGivenOffset(
currentScrollOffset - startOffset,
filledBoundMin: lastMinBound,
Expand All @@ -62,50 +62,51 @@ import UIKit
distanceFromWindowStart: distanceFromWindowStart,
distanceFromWindowEnd: distanceFromWindowEnd
)

onBlankAreaEvent?(
[
"offsetStart": blankOffsetStart,
"offsetEnd": blankOffsetEnd,
]
)
}

/// Sorts views by index and then invokes clearGaps which does the correction.
/// Performance: Sort is needed. Given relatively low number of views in RecyclerListView render tree this should be a non issue.
private func fixLayout() {
guard
subviews.count > 1,
// Fixing layout during animation can interfere with it.
layer.animationKeys()?.isEmpty == true
else { return }
let cellContainers = subviews
.compactMap { $0 as? CellContainer }
.sorted(by: { $0.index < $1.index })
clearGaps(for: cellContainers)
}

/// Checks for overlaps or gaps between adjacent items and then applies a correction.
/// Performance: RecyclerListView renders very small number of views and this is not going to trigger multiple layouts on the iOS side.
private func clearGaps(for cellContainers: [CellContainer]) {
var maxBound: CGFloat = 0
var minBound: CGFloat = CGFloat(Int.max)
var maxBoundNextCell: CGFloat = 0
let correctedScrollOffset = scrollOffset - (horizontal ? frame.minX : frame.minY)

cellContainers.indices.dropLast().forEach { index in
let cellContainer = cellContainers[index]
let cellTop = cellContainer.frame.minY
let cellBottom = cellContainer.frame.maxY
let cellLeft = cellContainer.frame.minX
let cellRight = cellContainer.frame.maxX

let nextCell = cellContainers[index + 1]
let nextCellTop = nextCell.frame.minY
let nextCellBottom = nextCell.frame.maxY
let nextCellLeft = nextCell.frame.minX
let nextCellRight = nextCell.frame.maxX


guard
isWithinBounds(
cellContainer,
Expand All @@ -122,7 +123,7 @@ import UIKit
windowSize: windowSize,
isHorizontal: horizontal
)

if horizontal {
maxBound = max(maxBound, cellRight)
minBound = min(minBound, cellLeft)
Expand Down Expand Up @@ -159,11 +160,11 @@ import UIKit
}
}
}

lastMaxBound = maxBoundNextCell
lastMinBound = minBound
}

func computeBlankFromGivenOffset(
_ actualScrollOffset: CGFloat,
filledBoundMin: CGFloat,
Expand All @@ -177,12 +178,12 @@ import UIKit
offsetEnd: CGFloat
) {
let blankOffsetStart = filledBoundMin - actualScrollOffset - distanceFromWindowStart

let blankOffsetEnd = actualScrollOffset + windowSize - renderAheadOffset - filledBoundMax - distanceFromWindowEnd

return (blankOffsetStart, blankOffsetEnd)
}

/// It's important to avoid correcting views outside the render window. An item that isn't being recycled might still remain in the view tree. If views outside get considered then gaps between unused items will cause algorithm to fail.
func isWithinBounds(
_ cellContainer: CellContainer,
Expand All @@ -194,7 +195,7 @@ import UIKit
let boundsStart = scrollOffset - renderAheadOffset
let boundsEnd = scrollOffset + windowSize
let cellFrame = cellContainer.frame

if isHorizontal {
return (cellFrame.minX >= boundsStart || cellFrame.maxX >= boundsStart) && (cellFrame.minX <= boundsEnd || cellFrame.maxX <= boundsEnd)
} else {
Expand Down

0 comments on commit d4adf8e

Please sign in to comment.