Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class MainApplication : Application(), ReactApplication {
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(CustomViewPackage())
}

override fun getJSMainModuleName(): String = "index"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.fabricexample

import android.annotation.SuppressLint
import android.util.Log
import android.view.View
import androidx.core.view.doOnDetach
import com.facebook.react.ReactPackage
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.views.view.ReactViewGroup
import com.facebook.react.views.view.ReactViewManager

@ReactModule(name = ReactViewManager.REACT_CLASS, canOverrideExistingModule = true)
class FixedReactViewManager : ReactViewManager() {

override fun removeViewAt(parent: ReactViewGroup, index: Int) {
val child = parent.getChildAt(index)
if (child is MyCustomView) {
Log.d("HannoDebug", """
FixedReactViewManager: removeViewAt: ${child}
→ parent: $parent
""".trimIndent())
}
super.removeViewAt(parent, index)
}

override fun addView(parent: ReactViewGroup, child: View, index: Int) {
if (child.parent == null) {
super.addView(parent, child, index)
return
}

Log.d("HannoDebug", """)
FixedReactViewManager: addView: child has parent, waiting for detach: $child, id: ${child.id}
→ current parent: ${child.parent}
→ new parent: $parent
""".trimIndent())
// When the child-parent relation is removed, onDetachedFromWindow will be called:
child.doOnDetach {
// Looking at how endViewTransition is implemented, dispatchDetachedFromWindow
// gets called _before_ the parent relation is removed, so we need to post this to the end of the frame:
child.post {
Log.d("HannoDebug", """
FixedReactViewManager: addView: child doOnDetach called: $child, id: ${child.id}
→ parent: ${child.parent} (should be null)
→ new parent: $parent
""".trimIndent())
super.addView(parent, child, index)
}
}
}
}

@SuppressLint("ViewConstructor")
class MyCustomView(context: ThemedReactContext) : View(context) {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
Log.d("HannoDebug", "MyCustomView: onAttachedToWindow called for view: $this, id: ${this.id}, to: ${this.parent}")
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
Log.d("HannoDebug", "MyCustomView: onDetachedFromWindow called for view: $this, id: ${this.id}, from: ${this.parent}")
// print callstack:
Exception().printStackTrace()
}
}

@ReactModule(name = "CustomView")
class CustomViewManager : SimpleViewManager<MyCustomView>() {
override fun getName() = "CustomView"

// companion object {
// @SuppressLint("StaticFieldLeak")
// var viewInstance: MyCustomView? = null
// }

init {
mRecyclableViews = HashMap()
}

override fun createViewInstance(reactContext: ThemedReactContext): MyCustomView {
Log.d("HannoDebug", "CustomViewManager: createViewInstance called")
// if (viewInstance == null) {
val viewInstance = MyCustomView(reactContext)
viewInstance.setBackgroundColor(0xFFFF0000.toInt()) // Red background for visibility
// }
return viewInstance
}

override fun onDropViewInstance(view: MyCustomView) {
super.onDropViewInstance(view)
prepareToRecycleView(view.context as ThemedReactContext, view)
Log.d("HannoDebug", "CustomViewManager: onDropViewInstance called ${view}")
}
}

class CustomViewPackage : ReactPackage {
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<in Nothing, in Nothing>> {
return listOf(CustomViewManager(), FixedReactViewManager())
}

}
9 changes: 9 additions & 0 deletions FabricExample/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@ extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> ex.autol
rootProject.name = 'FabricExample'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')

includeBuild('../node_modules/react-native') {
dependencySubstitution {
substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid"))
substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid"))
substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine"))
substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine"))
}
}
51 changes: 51 additions & 0 deletions FabricExample/patches/react-native+0.81.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java
index dee20e3..2f2c87c 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.java
@@ -379,9 +379,9 @@ public class SurfaceMountingManager {
+ actualParentId
+ "] "
+ " Parent: "
- + viewParent.getClass().getSimpleName()
+ + viewParent
+ " View: "
- + view.getClass().getSimpleName()));
+ + view));

// We've hit an error case, and `addView` will crash below
// if we don't take evasive action (it is an error to add a View
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt
index 2089157..c8e1bf5 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/shell/MainReactPackage.kt
@@ -151,7 +151,7 @@ constructor(private val config: MainPackageConfig? = null) :
ReactTextInputManager(),
if (ReactNativeFeatureFlags.enablePreparedTextLayout()) PreparedLayoutTextViewManager()
else ReactTextViewManager(),
- ReactViewManager(),
+// ReactViewManager(),
ReactVirtualTextViewManager(),
ReactUnimplementedViewManager())

@@ -191,7 +191,7 @@ constructor(private val config: MainPackageConfig? = null) :
PreparedLayoutTextViewManager()
else ReactTextViewManager()
},
- ReactViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactViewManager() },
+// ReactViewManager.REACT_CLASS to ModuleSpec.viewManagerSpec { ReactViewManager() },
ReactVirtualTextViewManager.REACT_CLASS to
ModuleSpec.viewManagerSpec { ReactVirtualTextViewManager() },
ReactUnimplementedViewManager.REACT_CLASS to
diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java
index 28e12d6..abc62c8 100644
--- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java
+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManager.java
@@ -54,7 +54,7 @@ public abstract class ViewManager<T extends View, C extends ReactShadowNode>
* null signals that View Recycling is disabled. `enableViewRecycling` must be explicitly called
* in a concrete constructor to enable View Recycling per ViewManager.
*/
- @Nullable private HashMap<Integer, Stack<T>> mRecyclableViews = null;
+ @Nullable public HashMap<Integer, Stack<T>> mRecyclableViews = null;

public ViewManager() {
super(null);
59 changes: 40 additions & 19 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.graphics.Paint
import android.os.Parcelable
import android.util.Log
import android.util.SparseArray
import android.view.MotionEvent
import android.view.View
Expand Down Expand Up @@ -469,27 +470,23 @@ class Screen(
return
}
isBeingRemoved = false
endTransitionRecursive(this)
endViewTransition()
}

private fun endTransitionRecursive(parent: ViewGroup) {
parent.children.forEach { childView ->
parent.endViewTransition(childView)

if (childView is ScreenStackHeaderConfig) {
endTransitionRecursive(childView.toolbar)
}

if (childView is ViewGroup) {
endTransitionRecursive(childView)
}
}
}
private val inTransitionViews = mutableListOf<Pair<ViewGroup, View>>()

/**
* Called when a screen gets removed. This is to mark all children as in transition,
* so they are not removed from the view hierarchy until endRemovalTransition is called.
* This is needed for the screen transition animation to work properly (otherwise the children
* would instantly disappear from the screen).
*
* This is copied from react-native-screens BUT it manually keeps track of the views.
*/
private fun startTransitionRecursive(parent: ViewGroup?) {
parent?.let {
for (i in 0 until it.childCount) {
val child = it.getChildAt(i)
parent?.let { parentView ->
for (i in 0 until parentView.childCount) {
val child = parentView.getChildAt(i)

if (parent is SwipeRefreshLayout && child is ImageView) {
// SwipeRefreshLayout class which has CircleImageView as a child,
Expand All @@ -498,9 +495,14 @@ class Screen(
// wrong index if we called `startViewTransition` on the views on new arch.
// We add a simple View to bump the number of children to make it work.
// TODO: find a better way to handle this scenario
it.addView(View(context), i)
parentView.addView(View(context), i)
} else {
child?.let { view -> it.startViewTransition(view) }
child?.let { childView ->
Log.d("HannoDebug", "Screen: startTransitionRecursive for parent: $parentView")
Log.d("HannoDebug", " ↳ child: $childView")
parentView.startViewTransition(childView)
inTransitionViews.add(Pair(parentView, childView))
}
}

if (child is ScreenStackHeaderConfig) {
Expand All @@ -516,6 +518,25 @@ class Screen(
}
}

/**
* Called when the removal transition is finished. This will clear the transition state
* from all children and allow them to be removed from the view hierarchy and their mParent
* field to be set to null.
*/
private fun endViewTransition() {
// IMPORTANT: Reverse order is needed, inner children first!
// Otherwise parents will call dispatchOnDetachedFromWindow on all their children,
// which will cause endViewTransition to have no effect on them anymore.
for ((parent, child) in inTransitionViews.asReversed()) {
Log.d("HannoDebug", "Screen: endViewTransition for parent: $parent")
Log.d("HannoDebug", " ↳ child: $child")
// The react-native layer will have called parent.removeView(child) already,
// so endViewTransition will finally remove the child from the parent.
parent.endViewTransition(child)
}
inTransitionViews.clear()
}

// We do not want to perform any action, therefore do not need to override the associated method.
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent?): Boolean =
Expand Down
4 changes: 4 additions & 0 deletions android/src/main/java/com/swmansion/rnscreens/ScreenStack.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class ScreenStack(
}

override fun endViewTransition(view: View) {
if (view is ScreensCoordinatorLayout) {
view.fragment.screen.endRemovalTransition()
}

super.endViewTransition(view)

disappearingTransitioningChildren.remove(view)
Expand Down
66 changes: 66 additions & 0 deletions apps/src/tests/TestRecyclingViews.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import {Button, requireNativeComponent, View} from 'react-native';
import {NavigationContainer, useNavigation} from '@react-navigation/native';
import {createNativeStackNavigator} from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

let CustomView = requireNativeComponent('CustomView');

function HomeScreen() {
const navigation = useNavigation();
return (
<View
collapsable={false}
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Button title="Go to Details" onPress={() => navigation.navigate('Details')} />
<CustomView
style={{
width: 100,
height: 100,
}}
/>
</View>
);
}

function DetailsScreen() {
const navigation = useNavigation();
return (
<View
collapsable={false}
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<Button title="Go to Home" onPress={() => navigation.goBack()} />
<CustomView
style={{
width: 100,
height: 100,
}}
/>
</View>
);
}

export default function TestApp() {
return (
<View
style={{
flex: 1,
}}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
</View>
);
}
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,4 @@ export { default as TestBottomTabs } from './TestBottomTabs';
export { default as TestScreenStack } from './TestScreenStack';
export { default as TestSplitView } from './TestSplitView';
export { default as TestSafeAreaViewIOS } from './TestSafeAreaViewIOS';
export { default as TestRecyclingViews } from './TestRecyclingViews';
Loading