diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.kt index 2f50b0be04f975..7d2bc40957450d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/NativeAnimatedModule.kt @@ -80,7 +80,7 @@ import kotlin.concurrent.Volatile */ @OptIn(UnstableReactNativeAPI::class) @ReactModule(name = NativeAnimatedModuleSpec.NAME) -public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : +public class NativeAnimatedModule(reactContext: ReactApplicationContext) : NativeAnimatedModuleSpec(reactContext), LifecycleEventListener, UIManagerListener { // For `queueAndExecuteBatchedOperations` @@ -163,12 +163,13 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : // Due to a race condition, we manually "carry-over" a polled item from previous batch // instead of peeking the queue itself for consistency. // TODO(T112522554): Clean up the queue access + val peekedOperation = peekedOperation if (peekedOperation != null) { - if (checkNotNull(peekedOperation).batchNumber > maxBatchNumber) { + if (peekedOperation.batchNumber > maxBatchNumber) { break } - operations.add(checkNotNull(peekedOperation)) - peekedOperation = null + operations.add(peekedOperation) + this.peekedOperation = null } val polledOperation = @@ -179,7 +180,7 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : if (polledOperation.batchNumber > maxBatchNumber) { // Because the operation is already retrieved from the queue, there's no way of placing it // back as the head element, so we remember it manually here - peekedOperation = polledOperation + this.peekedOperation = polledOperation break } operations.add(polledOperation) @@ -189,8 +190,7 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : } } - private val animatedFrameCallback: GuardedFrameCallback - private val reactChoreographer: ReactChoreographer? = ReactChoreographer.getInstance() + private val reactChoreographer: ReactChoreographer = ReactChoreographer.getInstance() private val operations = ConcurrentOperationQueue() private val preOperations = ConcurrentOperationQueue() @@ -205,7 +205,6 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : private var initializedForFabric = false private var initializedForNonFabric = false - private var enqueuedAnimationOnFrame = false @UIManagerType private var uiManagerType = UIManagerType.LEGACY private var numFabricAnimations = 0 @@ -302,8 +301,8 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : } } - checkNotNull(preOperations).executeBatch(batchNumber, nodesManager) - checkNotNull(operations).executeBatch(batchNumber, nodesManager) + preOperations.executeBatch(batchNumber, nodesManager) + operations.executeBatch(batchNumber, nodesManager) } // For non-FabricUIManager only @@ -322,11 +321,9 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : // might be stripped out. val frameNo = currentBatchNumber++ - val preOperationsUIBlock = UIBlock { - checkNotNull(preOperations).executeBatch(frameNo, nodesManager) - } + val preOperationsUIBlock = UIBlock { preOperations.executeBatch(frameNo, nodesManager) } - val operationsUIBlock = UIBlock { checkNotNull(operations).executeBatch(frameNo, nodesManager) } + val operationsUIBlock = UIBlock { operations.executeBatch(frameNo, nodesManager) } assert(uiManager is UIManagerModule) val uiManagerModule = uiManager as UIManagerModule @@ -366,41 +363,34 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : nodesManagerRef.set(nodesManager) } - init { - animatedFrameCallback = - object : GuardedFrameCallback(checkNotNull(reactContext)) { - override fun doFrameGuarded(frameTimeNanos: Long) { - try { - enqueuedAnimationOnFrame = false - val nodesManager = nodesManager - if (nodesManager?.hasActiveAnimations() == true) { - nodesManager.runUpdates(frameTimeNanos) - } - // This is very unlikely to ever be hit. - if (nodesManager == null || reactChoreographer == null) { - return - } - - enqueueFrameCallback() - } catch (ex: Exception) { - throw RuntimeException(ex) + private var enqueuedAnimationOnFrame = false + private val animatedFrameCallback = + object : GuardedFrameCallback(reactContext) { + override fun doFrameGuarded(frameTimeNanos: Long) { + try { + enqueuedAnimationOnFrame = false + val nodesManager = nodesManager ?: return + if (nodesManager.hasActiveAnimations()) { + nodesManager.runUpdates(frameTimeNanos) } + + enqueueFrameCallback() + } catch (ex: Exception) { + throw RuntimeException(ex) } } - } + } private fun clearFrameCallback() { - checkNotNull(reactChoreographer) - .removeFrameCallback( - ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, animatedFrameCallback) + reactChoreographer.removeFrameCallback( + ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, animatedFrameCallback) enqueuedAnimationOnFrame = false } private fun enqueueFrameCallback() { if (!enqueuedAnimationOnFrame) { - checkNotNull(reactChoreographer) - .postFrameCallback( - ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, animatedFrameCallback) + reactChoreographer.postFrameCallback( + ReactChoreographer.CallbackType.NATIVE_ANIMATED_MODULE, animatedFrameCallback) enqueuedAnimationOnFrame = true } } @@ -431,8 +421,9 @@ public class NativeAnimatedModule(reactContext: ReactApplicationContext?) : } // Subscribe to UIManager (Fabric or non-Fabric) lifecycle events if we haven't yet - if (if (uiManagerType == UIManagerType.FABRIC) initializedForFabric - else initializedForNonFabric) { + val initialized = + if (uiManagerType == UIManagerType.FABRIC) initializedForFabric else initializedForNonFabric + if (initialized) { return } 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) }