diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt index b62626efe7c0cb..2192fcbe0922bb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactClippingViewGroup.kt @@ -32,7 +32,7 @@ public interface ReactClippingViewGroup { */ public fun updateClippingRect() - public fun updateClippingRect(excludedView: Set?) + public fun updateClippingRect(excludedViews: Set?) /** * Get rectangular bounds to which view is currently clipped to. Called only on views that has set diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index c5e80376b0af14..288301cc8825a9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -177,11 +177,13 @@ public open class ReactViewGroup public constructor(context: Context?) : internal open fun recycleView() { recycleCount++ + // Remove dangling listeners + val allChildren = allChildren if (allChildren != null && childrenLayoutChangeListener != null) { childrenLayoutChangeListener?.shutdown() for (i in 0..(max(12, allChildrenCount)) childrenLayoutChangeListener = ChildrenLayoutChangeListener(this) for (i in 0..?) { + override fun updateClippingRect(excludedViews: Set?) { if (!_removeClippedSubviews) { return } - checkNotNull(allChildren) - - calculateClippingRect(this, checkNotNull(clippingRect)) - updateClippingToRect(checkNotNull(clippingRect), excludedView) + val clippingRect = checkNotNull(clippingRect) + calculateClippingRect(this, clippingRect) + updateClippingToRect(clippingRect, excludedViews) } override fun endViewTransition(view: View) { @@ -415,58 +419,58 @@ public open class ReactViewGroup public constructor(context: Context?) : childrenRemovedWhileTransitioning?.contains(child.id) == true private fun updateClippingToRect(clippingRect: Rect, excludedViewsSet: Set? = null) { - checkNotNull(allChildren) + val childArray = checkNotNull(allChildren) inSubviewClippingLoop = true var clippedSoFar = 0 for (i in 0.. = HashSet() var j = 0 while (j < i) { - realClippedSoFar += if (isViewClipped(checkNotNull(allChildren?.get(j)), null)) 1 else 0 - uniqueViews.add(allChildren?.get(j)) + realClippedSoFar += if (isViewClipped(childArray[j], j)) 1 else 0 + uniqueViews.add(childArray[j]) j++ } throw IllegalStateException( - "Invalid clipping state. i=$i clippedSoFar=$clippedSoFar count=$childCount allChildrenCount=$allChildrenCount recycleCount=$recycleCount realClippedSoFar=$realClippedSoFar uniqueViewsCount=${uniqueViews.size}", - error) + "Invalid clipping state. i=$i clippedSoFar=$clippedSoFar count=$childCount allChildrenCount=$allChildrenCount recycleCount=$recycleCount realClippedSoFar=$realClippedSoFar uniqueViewsCount=${uniqueViews.size} excludedViews=${excludedViewsSet?.size ?: 0}", + ex) } - if (isViewClipped(checkNotNull(allChildren?.get(i)), i)) { + if (isViewClipped(childArray[i], i)) { clippedSoFar++ } + if (i - clippedSoFar > childCount) { + throw IllegalStateException( + "Invalid clipping state. i=$i clippedSoFar=$clippedSoFar count=$childCount allChildrenCount=$allChildrenCount recycleCount=$recycleCount excludedViews=${excludedViewsSet?.size ?: 0}") + } } inSubviewClippingLoop = false } - private fun updateSubviewClipStatus(clippingRect: Rect?, idx: Int, clippedSoFar: Int) { - val nonNullClippingRect = checkNotNull(clippingRect) - updateSubviewClipStatus(nonNullClippingRect, idx, clippedSoFar, null) - } - private fun updateSubviewClipStatus( clippingRect: Rect, idx: Int, clippedSoFar: Int, - excludedViewsSet: Set? + excludedViewsSet: Set? = null ) { assertOnUiThread() val child = checkNotNull(allChildren?.get(idx)) val intersects = clippingRect.intersects(child.left, child.top, child.right, child.bottom) var needUpdateClippingRecursive = false + // We never want to clip children that are being animated, as this can easily break layout : // when layout animation changes size and/or position of views contained inside a listview that // clips offscreen children, we need to ensure that, when view exits the viewport, final size // and position is set prior to removing the view from its listview parent. // Otherwise, when view gets re-attached again, i.e when it re-enters the viewport after scroll, // it won't be size and located properly. - val animation = child.animation - val isAnimating = animation != null && !animation.hasEnded() - val shouldSkipView = excludedViewsSet != null && excludedViewsSet.contains(child.id) + val isAnimating = child.animation?.hasEnded() == false + + val shouldSkipView = excludedViewsSet?.contains(child.id) == true if (excludedViewsSet != null) { needUpdateClippingRecursive = true } @@ -492,12 +496,10 @@ public open class ReactViewGroup public constructor(context: Context?) : // If there is any intersection we need to inform the child to update its clipping rect needUpdateClippingRecursive = true } + if (needUpdateClippingRecursive) { - if (child is ReactClippingViewGroup) { - val clippingChild = child as ReactClippingViewGroup - if (clippingChild.removeClippedSubviews) { - clippingChild.updateClippingRect(excludedViewsSet) - } + if ((child as? ReactClippingViewGroup)?.removeClippedSubviews == true) { + child.updateClippingRect(excludedViewsSet) } } } @@ -507,12 +509,12 @@ public open class ReactViewGroup public constructor(context: Context?) : return } - val nonNullClippingRect = checkNotNull(clippingRect) - checkNotNull(allChildren) + val clippingRect = checkNotNull(clippingRect) + val allChildren = checkNotNull(allChildren) // do fast check whether intersect state changed val intersects = - nonNullClippingRect.intersects(subview.left, subview.top, subview.right, subview.bottom) + clippingRect.intersects(subview.left, subview.top, subview.right, subview.bottom) // If it was intersecting before, should be attached to the parent val oldIntersects = !isViewClipped(subview, null) @@ -521,11 +523,11 @@ public open class ReactViewGroup public constructor(context: Context?) : inSubviewClippingLoop = true var clippedSoFar = 0 for (i in 0.. = checkNotNull(allChildren) + val allChildren = checkNotNull(allChildren) inSubviewClippingLoop = true var clippedSoFar = 0 for (i in 0.. = checkNotNull(allChildren) + val allChildren = checkNotNull(allChildren) view.removeOnLayoutChangeListener(childrenLayoutChangeListener) val index = indexOfChildInAllChildren(view) - if (!isViewClipped(checkNotNull(childArray[index]), index)) { + if (!isViewClipped(allChildren[index], index)) { var clippedSoFar = 0 for (i in 0..(size + ARRAY_CAPACITY_INCREMENT) + System.arraycopy(childArray, 0, allChildren, 0, size) + childArray = allChildren + this.allChildren = childArray } childArray[allChildrenCount++] = child } else if (index < count) { if (size == count) { - allChildren = arrayOfNulls(size + ARRAY_CAPACITY_INCREMENT) - System.arraycopy(childArray, 0, checkNotNull(allChildren), 0, index) - System.arraycopy(childArray, index, checkNotNull(allChildren), index + 1, count - index) - childArray = checkNotNull(allChildren) + val allChildren = arrayOfNulls(size + ARRAY_CAPACITY_INCREMENT) + System.arraycopy(childArray, 0, allChildren, 0, index) + System.arraycopy(childArray, index, allChildren, index + 1, count - index) + childArray = allChildren + this.allChildren = childArray } else { System.arraycopy(childArray, index, childArray, index + 1, count - index) }