Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[android][ios] Upgrade @shopify/flash-list to 1.3.0 #19317

Merged
merged 2 commits into from Oct 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@ Package-specific changes not released in any SDK will be added here just before
### 📚 3rd party library updates

- Updated `@stripe/stripe-react-native` from `0.13.1` to `0.18.1` on iOS. ([#19055](https://github.com/expo/expo/pull/19055) by [@tsapeta](https://github.com/tsapeta))
- Updated `@shopify/flash-list` from `1.1.0` to `1.3.0`. ([#19317](https://github.com/expo/expo/pull/19317) by [@kudo](https://github.com/kudo))

### 🛠 Breaking changes

Expand Down
Expand Up @@ -10,6 +10,8 @@ class AutoLayoutShadow {
var blankOffsetAtStart = 0 // Tracks blank area from the top
var blankOffsetAtEnd = 0 // Tracks blank area from the bottom

var lastMaxBoundOverall = 0 // Tracks where the last pixel is drawn in the overall

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

Expand All @@ -19,6 +21,7 @@ class AutoLayoutShadow {
var maxBound = 0
var minBound = Int.MAX_VALUE
var maxBoundNeighbour = 0
lastMaxBoundOverall = 0
for (i in 0 until sortedItems.size - 1) {
val cell = sortedItems[i]
val neighbour = sortedItems[i + 1]
Expand Down Expand Up @@ -65,6 +68,8 @@ class AutoLayoutShadow {
}
}
}
lastMaxBoundOverall = kotlin.math.max(lastMaxBoundOverall, if (horizontal) cell.right else cell.bottom)
lastMaxBoundOverall = kotlin.math.max(lastMaxBoundOverall, if (horizontal) neighbour.right else neighbour.bottom)
}
lastMaxBound = maxBoundNeighbour
lastMinBound = minBound
Expand Down
Expand Up @@ -3,7 +3,11 @@ package com.shopify.reactnative.flash_list
import android.content.Context
import android.graphics.Canvas
import android.util.DisplayMetrics
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.ScrollView
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.WritableMap
Expand All @@ -24,19 +28,19 @@ class AutoLayoutView(context: Context) : ReactViewGroup(context) {
* can still cause views to overlap. Therefore, it makes sense to override draw to do correction. */
override fun dispatchDraw(canvas: Canvas?) {
fixLayout()
fixFooter()
super.dispatchDraw(canvas)

if (enableInstrumentation && parent?.parent != null) {
val parentScrollView = getParentScrollView()
if (enableInstrumentation && parentScrollView != null) {
/** Since we need to call this method with scrollOffset on the UI thread and not with the one react has we're querying parent's parent
directly which will be a ScrollView. If it isn't reported values will be incorrect but the component will not break.
RecyclerListView is expected not to change the hierarchy of children. */

val scrollContainerSize = (parent.parent as View).let {
if (alShadow.horizontal) it.width else it.height
}
val scrollOffset = (parent.parent as View).let {
if (alShadow.horizontal) it.scrollX else it.scrollY
}
val scrollContainerSize = if (alShadow.horizontal) parentScrollView.width else parentScrollView.height

val scrollOffset = if (alShadow.horizontal) parentScrollView.scrollX else parentScrollView.scrollY

val startOffset = if (alShadow.horizontal) left else top
val endOffset = if (alShadow.horizontal) right else bottom

Expand Down Expand Up @@ -66,6 +70,73 @@ class AutoLayoutView(context: Context) : ReactViewGroup(context) {
}
}

/** Fixes footer position along with rest of the items */
private fun fixFooter() {
val parentScrollView = getParentScrollView()
if (disableAutoLayout || parentScrollView == null) {
return
}
val isAutoLayoutEndVisible = if (alShadow.horizontal) right <= parentScrollView.width else bottom <= parentScrollView.height
if (!isAutoLayoutEndVisible) {
return
}
val autoLayoutParent = parent as? View
val footer = getFooter();
val diff = getFooterDiff()
if (diff == 0 || footer == null || autoLayoutParent == null) {
return
}

if (alShadow.horizontal) {
footer.offsetLeftAndRight(diff)
right += diff
autoLayoutParent.right += diff
} else {
footer.offsetTopAndBottom(diff)
bottom += diff
autoLayoutParent.bottom += diff
}
}

private fun getFooterDiff(): Int {
if (childCount == 0) {
alShadow.lastMaxBoundOverall = 0
} else if (childCount == 1) {
val firstChild = getChildAt(0)
alShadow.lastMaxBoundOverall = if (alShadow.horizontal) {
firstChild.right
} else {
firstChild.bottom
}
}
val autoLayoutEnd = if (alShadow.horizontal) right - left else bottom - top
return alShadow.lastMaxBoundOverall - autoLayoutEnd
}

private fun getFooter(): View? {
return (parent as? ViewGroup)?.let {
for (i in 0 until it.childCount) {
val view = it.getChildAt(i)
if (view is CellContainer && view.index == -1) {
return@let view
}
}
return@let null
}
}

private fun getParentScrollView(): View? {
var autoLayoutParent = parent;
while (autoLayoutParent != null) {
if (autoLayoutParent is ScrollView || autoLayoutParent is HorizontalScrollView) {
return autoLayoutParent as View
}
autoLayoutParent = autoLayoutParent.parent;
}
return null
}


/** TODO: Check migration to Fabric */
private fun emitBlankAreaEvent() {
val event: WritableMap = Arguments.createMap()
Expand Down
2 changes: 1 addition & 1 deletion apps/native-component-list/package.json
Expand Up @@ -53,7 +53,7 @@
"@react-navigation/material-bottom-tabs": "~5.3.9",
"@react-navigation/native": "~5.9.8",
"@react-navigation/stack": "~5.12.6",
"@shopify/flash-list": "1.1.0",
"@shopify/flash-list": "1.3.0",
"@shopify/react-native-skia": "0.1.137",
"@use-expo/permissions": "^2.0.0",
"date-format": "^2.0.0",
Expand Down
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Expand Up @@ -2012,7 +2012,7 @@ PODS:
- React-jsi (= 0.70.1)
- React-logger (= 0.70.1)
- React-perflogger (= 0.70.1)
- RNFlashList (1.1.0):
- RNFlashList (1.3.0):
- React-Core
- RNGestureHandler (2.5.0):
- React-Core
Expand Down Expand Up @@ -3508,7 +3508,7 @@ SPEC CHECKSUMS:
React-RCTVibration: e9164827303fb6a5cf79e4c4af4846a09956b11f
React-runtimeexecutor: a11d0c2e14140baf1e449264ca9168ae9ae6bbd0
ReactCommon: 7f86326b92009925c6dcf93f8e825060171c379f
RNFlashList: 031c182b95ead44fd0715f6029a744cd5e10d51f
RNFlashList: 5116f2de2f543f01bfc30b22d5942d5af84b43df
RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50
RNReanimated: 5c8c17e26787fd8984cd5accdc70fef2ca70aafd
RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7
Expand Down
@@ -1,6 +1,6 @@
{
"name": "RNFlashList",
"version": "1.1.0",
"version": "1.3.0",
"summary": "FlashList is a more performant FlatList replacement",
"homepage": "https://shopify.github.io/flash-list/",
"license": "MIT",
Expand All @@ -11,7 +11,7 @@
},
"source": {
"git": "https://github.com/shopify/flash-list.git",
"tag": "v#{s.version}"
"tag": "v1.3.0"
},
"source_files": "ios/Sources/**/*",
"requires_arc": true,
Expand Down
Expand Up @@ -39,17 +39,20 @@ import UIKit
private var enableInstrumentation = false
private var disableAutoLayout = false

/// Tracks where the last pixel is drawn in the overall
private var lastMaxBoundOverall: CGFloat = 0
/// 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()
fixFooter()
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 scrollView = getScrollView()
guard enableInstrumentation, let scrollView = scrollView else { return }

let scrollContainerSize = horizontal ? scrollView.frame.width : scrollView.frame.height
let currentScrollOffset = horizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y
Expand All @@ -76,6 +79,10 @@ import UIKit
)
}

func getScrollView() -> UIScrollView? {
return sequence(first: self, next: { $0.superview }).first(where: { $0 is UIScrollView }) as? UIScrollView
}

/// 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() {
Expand Down Expand Up @@ -105,7 +112,7 @@ import UIKit
var minBound: CGFloat = CGFloat(Int.max)
var maxBoundNextCell: CGFloat = 0
let correctedScrollOffset = scrollOffset - (horizontal ? frame.minX : frame.minY)

lastMaxBoundOverall = 0
cellContainers.indices.dropLast().forEach { index in
let cellContainer = cellContainers[index]
let cellTop = cellContainer.frame.minY
Expand All @@ -115,9 +122,7 @@ import UIKit

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
Expand All @@ -128,7 +133,10 @@ import UIKit
windowSize: windowSize,
isHorizontal: horizontal
)
else { return }
else {
updateLastMaxBoundOverall(currentCell: cellContainer, nextCell: nextCell)
return
}
let isNextCellVisible = isWithinBounds(
nextCell,
scrollOffset: correctedScrollOffset,
Expand All @@ -152,7 +160,7 @@ import UIKit
nextCell.frame.origin.x = maxBound
}
if isNextCellVisible {
maxBoundNextCell = max(maxBound, nextCellRight)
maxBoundNextCell = max(maxBound, nextCell.frame.maxX)
}
} else {
maxBound = max(maxBound, cellBottom)
Expand All @@ -169,15 +177,20 @@ import UIKit
nextCell.frame.origin.y = maxBound
}
if isNextCellVisible {
maxBoundNextCell = max(maxBound, nextCellBottom)
maxBoundNextCell = max(maxBound, nextCell.frame.maxY)
}
}
updateLastMaxBoundOverall(currentCell: cellContainer, nextCell: nextCell)
}

lastMaxBound = maxBoundNextCell
lastMinBound = minBound
}

private func updateLastMaxBoundOverall(currentCell: CellContainer, nextCell: CellContainer) {
lastMaxBoundOverall = max(lastMaxBoundOverall, horizontal ? currentCell.frame.maxX : currentCell.frame.maxY, horizontal ? nextCell.frame.maxX : nextCell.frame.maxY)
}

func computeBlankFromGivenOffset(
_ actualScrollOffset: CGFloat,
filledBoundMin: CGFloat,
Expand Down Expand Up @@ -215,4 +228,44 @@ import UIKit
return (cellFrame.minY >= boundsStart || cellFrame.maxY >= boundsStart) && (cellFrame.minY <= boundsEnd || cellFrame.maxY <= boundsEnd)
}
}

/// Fixes footer position along with rest of the items
private func fixFooter() {
guard !disableAutoLayout, let parentScrollView = getScrollView() else {
return
}

let isAutoLayoutEndVisible = horizontal ? frame.maxX <= parentScrollView.frame.width : frame.maxY <= parentScrollView.frame.height
guard isAutoLayoutEndVisible, let footer = footer() else {
return
}

let diff = footerDiff()
guard diff != 0 else { return }

if horizontal {
footer.frame.origin.x += diff
frame.size.width += diff
superview?.frame.size.width += diff
} else {
footer.frame.origin.y += diff
frame.size.height += diff
superview?.frame.size.height += diff
}
}

private func footerDiff() -> CGFloat {
if subviews.count == 0 {
lastMaxBoundOverall = 0
} else if subviews.count == 1 {
let firstChild = subviews[0]
lastMaxBoundOverall = horizontal ? firstChild.frame.maxX : firstChild.frame.maxY
}
let autoLayoutEnd = horizontal ? frame.width : frame.height
return lastMaxBoundOverall - autoLayoutEnd
}

private func footer() -> UIView? {
return superview?.subviews.first(where:{($0 as? CellContainer)?.index == -1})
}
}
2 changes: 1 addition & 1 deletion packages/expo/bundledNativeModules.json
Expand Up @@ -105,6 +105,6 @@
"unimodules-app-loader": "~3.1.0",
"unimodules-image-loader-interface": "~6.1.0",
"@shopify/react-native-skia": "0.1.137",
"@shopify/flash-list": "1.1.0",
"@shopify/flash-list": "1.3.0",
"@sentry/react-native": "^4.1.3"
}