From d4399a0eb98072802769f9dc79fce6098f4c24c9 Mon Sep 17 00:00:00 2001 From: Kudo Chien Date: Fri, 30 Sep 2022 11:22:50 +0800 Subject: [PATCH 1/2] [android][ios] Upgrade @shopify/flash-list to 1.3.0 --- .../flash_list/AutoLayoutShadow.kt | 5 ++ .../reactnative/flash_list/AutoLayoutView.kt | 85 +++++++++++++++++-- apps/native-component-list/package.json | 2 +- ios/Podfile.lock | 4 +- .../flash-list/RNFlashList.podspec.json | 4 +- .../ios/Sources/AutoLayoutView.swift | 69 +++++++++++++-- packages/expo/bundledNativeModules.json | 2 +- yarn.lock | 31 +++---- 8 files changed, 162 insertions(+), 40 deletions(-) diff --git a/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutShadow.kt b/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutShadow.kt index fe23b23184011..ce060a6c48081 100644 --- a/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutShadow.kt +++ b/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutShadow.kt @@ -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 @@ -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] @@ -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 diff --git a/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutView.kt b/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutView.kt index c722d7077ee50..6b78bd93e2446 100644 --- a/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutView.kt +++ b/android/vendored/unversioned/@shopify/flash-list/android/src/main/kotlin/com/shopify/reactnative/flash_list/AutoLayoutView.kt @@ -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 @@ -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 @@ -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() diff --git a/apps/native-component-list/package.json b/apps/native-component-list/package.json index 69cd8a3c2a16d..e4e85e22257a0 100644 --- a/apps/native-component-list/package.json +++ b/apps/native-component-list/package.json @@ -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", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2c37687ffb23f..57efc48ec8228 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 @@ -3508,7 +3508,7 @@ SPEC CHECKSUMS: React-RCTVibration: e9164827303fb6a5cf79e4c4af4846a09956b11f React-runtimeexecutor: a11d0c2e14140baf1e449264ca9168ae9ae6bbd0 ReactCommon: 7f86326b92009925c6dcf93f8e825060171c379f - RNFlashList: 031c182b95ead44fd0715f6029a744cd5e10d51f + RNFlashList: 5116f2de2f543f01bfc30b22d5942d5af84b43df RNGestureHandler: bad495418bcbd3ab47017a38d93d290ebd406f50 RNReanimated: 5c8c17e26787fd8984cd5accdc70fef2ca70aafd RNScreens: 4a1af06327774490d97342c00aee0c2bafb497b7 diff --git a/ios/vendored/unversioned/@shopify/flash-list/RNFlashList.podspec.json b/ios/vendored/unversioned/@shopify/flash-list/RNFlashList.podspec.json index ff9472e3f0012..a3223015396a1 100644 --- a/ios/vendored/unversioned/@shopify/flash-list/RNFlashList.podspec.json +++ b/ios/vendored/unversioned/@shopify/flash-list/RNFlashList.podspec.json @@ -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", @@ -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, diff --git a/ios/vendored/unversioned/@shopify/flash-list/ios/Sources/AutoLayoutView.swift b/ios/vendored/unversioned/@shopify/flash-list/ios/Sources/AutoLayoutView.swift index d1bfcfd335dd1..c2146c55831aa 100644 --- a/ios/vendored/unversioned/@shopify/flash-list/ios/Sources/AutoLayoutView.swift +++ b/ios/vendored/unversioned/@shopify/flash-list/ios/Sources/AutoLayoutView.swift @@ -39,6 +39,8 @@ 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 @@ -46,10 +48,11 @@ import UIKit 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 @@ -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() { @@ -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 @@ -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 @@ -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, @@ -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) @@ -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, @@ -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}) + } } diff --git a/packages/expo/bundledNativeModules.json b/packages/expo/bundledNativeModules.json index 5be08675eeadb..2206ce8d88671 100644 --- a/packages/expo/bundledNativeModules.json +++ b/packages/expo/bundledNativeModules.json @@ -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" } diff --git a/yarn.lock b/yarn.lock index cdec7065fe295..66809fc00f00e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3531,12 +3531,12 @@ component-type "^1.2.1" join-component "^1.1.0" -"@shopify/flash-list@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.1.0.tgz#257f3b3cf6718094c3a7adf45b111f752cf6450b" - integrity sha512-nLkMscftwLFAq9kkSLs0GObucoJqqzUrzxpW+hG/HUsuwGJ6kibTT14M9K4JvoGOaGphoykHJ2a4KSBoPVg5DQ== +"@shopify/flash-list@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.3.0.tgz#0ac09f844afb1d73371b84ef049ec9c3a5db112e" + integrity sha512-1NyLYs9AzwLBRAjdESAcq7fZLccVBGwmiq78ygh+QokcrpAcRkiQDWJAK0zfk+Wej/1thggiFxo6X2Xu1X98Pg== dependencies: - recyclerlistview "4.0.1" + recyclerlistview "4.1.2" tslib "2.4.0" "@shopify/react-native-skia@0.1.137": @@ -9672,7 +9672,7 @@ fbjs-css-vars@^1.0.0: resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== -fbjs@^0.8.4, fbjs@^0.8.9: +fbjs@^0.8.4: version "0.8.18" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.18.tgz#9835e0addb9aca2eff53295cd79ca1cfc7c9662a" integrity sha512-EQaWFK+fEPSoibjNy8IxUtaFOMXcWsY0JaVrQoZR9zC8N2Ygf9iDITPWjUTVIax95b6I742JFLqASHfsag/vKA== @@ -16814,7 +16814,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.0, prompts@^2.4.1, kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@*, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@*, prop-types@15.8.1, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -16823,13 +16823,6 @@ prop-types@*, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.0, prop-t object-assign "^4.1.1" react-is "^16.13.1" -prop-types@15.5.8: - version "15.5.8" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394" - integrity sha512-QiDx7s0lWoAVxmEmOYnn3rIZGduup2PZgj3rta5O5y0NfPKu3ApWi+GdMfTto7PmO/5+p4yamSLMZkj0jaTL4A== - dependencies: - fbjs "^0.8.9" - propagate@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" @@ -17703,13 +17696,13 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" -recyclerlistview@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.0.1.tgz#b653fc4151b2931aafe753c182908f97997c5e19" - integrity sha512-KPUK8uFkE7++hgaH9duoFh3ZQQOmyeIklcoe8RkzzrsK3PoUx8D6ABAo+UhRbwaYbgyqJTcILe2htW7wJeLBLQ== +recyclerlistview@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.1.2.tgz#3629c2faff13be3dc64bca82490e9a5efb2303aa" + integrity sha512-fvopyPoXaDY/RJGJKzroGYHgBAZoXlEdLw1D4PSi3isTRhyfbh0WNNuTyLfJSiz6ctZeb0J2ErPCInYcahFTlw== dependencies: lodash.debounce "4.0.8" - prop-types "15.5.8" + prop-types "15.8.1" ts-object-utils "0.0.5" redent@^2.0.0: From 7b914a2229dfecce5eb1ba9d2f5e561ca332969b Mon Sep 17 00:00:00 2001 From: Kudo Chien Date: Fri, 30 Sep 2022 11:26:32 +0800 Subject: [PATCH 2/2] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d6b1a6e757b..1ca846fbb79c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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