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

fix(Android,Fabric): pressable on Screen loses focus on pointer movement #2292

Merged
merged 22 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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 @@ -24,7 +24,7 @@ abstract class FabricEnabledViewGroup(
protected fun updateScreenSizeFabric(
width: Int,
height: Int,
headerHeight: Double,
headerHeight: Int,
) {
updateState(width, height, headerHeight)
}
Expand All @@ -33,10 +33,11 @@ abstract class FabricEnabledViewGroup(
fun updateState(
width: Int,
height: Int,
headerHeight: Double,
headerHeight: Int,
) {
val realWidth: Float = PixelUtil.toDIPFromPixel(width.toFloat())
val realHeight: Float = PixelUtil.toDIPFromPixel(height.toFloat())
val realHeaderHeight: Float = PixelUtil.toDIPFromPixel(headerHeight.toFloat())

// Check incoming state values. If they're already the correct value, return early to prevent
// infinite UpdateState/SetState loop.
Expand All @@ -54,7 +55,7 @@ abstract class FabricEnabledViewGroup(
putDouble("frameWidth", realWidth.toDouble())
putDouble("frameHeight", realHeight.toDouble())
putDouble("contentOffsetX", 0.0)
putDouble("contentOffsetY", headerHeight)
putDouble("contentOffsetY", realHeaderHeight.toDouble())
}
mStateWrapper?.updateState(map)
}
Expand Down
36 changes: 3 additions & 33 deletions android/src/main/java/com/swmansion/rnscreens/Screen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.content.pm.ActivityInfo
import android.graphics.Paint
import android.os.Parcelable
import android.util.SparseArray
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
Expand All @@ -15,7 +14,6 @@ import androidx.core.view.children
import androidx.fragment.app.Fragment
import com.facebook.react.bridge.GuardedRunnable
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.UIManagerModule
import com.facebook.react.uimanager.events.EventDispatcher
Expand Down Expand Up @@ -141,17 +139,14 @@ class Screen(
val width = r - l
val height = b - t

val headerHeight = calculateHeaderHeight()
val totalHeight =
headerHeight.first + headerHeight.second // action bar height + status bar height
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
updateScreenSizeFabric(width, height, totalHeight)
updateScreenSizeFabric(width, height, t)
} else {
updateScreenSizePaper(width, height)
}

footer?.onParentLayout(changed, l, t, r, b, container!!.height)
notifyHeaderHeightChange(totalHeight)
notifyHeaderHeightChange(t)
}
}

Expand Down Expand Up @@ -400,32 +395,7 @@ class Screen(
}
}

private fun calculateHeaderHeight(): Pair<Double, Double> {
val actionBarTv = TypedValue()
val resolvedActionBarSize =
context.theme.resolveAttribute(android.R.attr.actionBarSize, actionBarTv, true)

// Check if it's possible to get an attribute from theme context and assign a value from it.
// Otherwise, the default value will be returned.
val actionBarHeight =
TypedValue
.complexToDimensionPixelSize(actionBarTv.data, resources.displayMetrics)
.takeIf { resolvedActionBarSize && headerConfig?.isHeaderHidden != true && headerConfig?.isHeaderTranslucent != true }
?.let { PixelUtil.toDIPFromPixel(it.toFloat()).toDouble() } ?: 0.0

val statusBarHeight =
context.resources
.getIdentifier("status_bar_height", "dimen", "android")
// Count only status bar when action bar is visible and status bar is not hidden
.takeIf { it > 0 && isStatusBarHidden != true && actionBarHeight > 0 }
?.let { (context.resources::getDimensionPixelSize)(it) }
?.let { PixelUtil.toDIPFromPixel(it.toFloat()).toDouble() }
?: 0.0

return actionBarHeight to statusBarHeight
}

private fun notifyHeaderHeightChange(headerHeight: Double) {
private fun notifyHeaderHeightChange(headerHeight: Int) {
val screenContext = context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(screenContext)
UIManagerHelper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import com.facebook.react.uimanager.events.Event
class HeaderHeightChangeEvent(
surfaceId: Int,
viewId: Int,
private val headerHeight: Double,
private val headerHeight: Int,
) : Event<HeaderHeightChangeEvent>(surfaceId, viewId) {
override fun getEventName() = EVENT_NAME

// As the same header height could appear twice, use header height as a coalescing key.
override fun getCoalescingKey(): Short = headerHeight.toInt().toShort()
override fun getCoalescingKey(): Short = headerHeight.toShort()

override fun getEventData(): WritableMap? =
Arguments.createMap().apply {
putDouble("headerHeight", headerHeight)
putDouble("headerHeight", headerHeight.toDouble())
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ abstract class FabricEnabledViewGroup(
protected fun updateScreenSizeFabric(
width: Int,
height: Int,
headerHeight: Double,
headerHeight: Int,
) {
// do nothing
}
Expand Down
109 changes: 109 additions & 0 deletions apps/src/tests/Test1975.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import * as React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Stack = createNativeStackNavigator();

function App() {
const [count, setCount] = React.useState(0);

return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ statusBarTranslucent: false }}>
<Stack.Screen
name="Screen"
component={Screen}
options={{
headerRight: () => (
<Pressable
onPress={() => setCount(prev => prev + 1)}
style={({ pressed }) => [
styles.pressable,
pressed && { backgroundColor: 'goldenrod' },
]}>
<Text>Press (+)</Text>
</Pressable>
),
title: count.toString(),
headerTitleAlign: 'center',
}}
/>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ navigation }) => ({
title: 'Details',
headerRight: () => (
<Pressable
onPress={navigation.goBack}
style={({ pressed }) => [
styles.pressable,
pressed && { backgroundColor: 'goldenrod' },
]}>
<Text>Go Back</Text>
</Pressable>
),
})}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

function Screen({ navigation }: any) {
return (
<View style={styles.container}>
<Pressable
onPress={() => navigation.navigate('Details')}
style={({ pressed }) => [
styles.pressable,
pressed && { backgroundColor: 'goldenrod' },
]}>
<Text>Go to Details</Text>
</Pressable>
</View>
);
}

function DetailsScreen() {
let counter = React.useRef(0);

return (
<View style={{ ...styles.container, backgroundColor: 'beige' }}>
<Pressable
onPressIn={() => {
counter.current += 1;
console.log(`[${counter.current}] Details: onPressIn`);
}}
onPress={() => {
console.log(`[${counter.current}] Details: onPress`);
}}
onPressOut={() => {
console.log(`[${counter.current}] Details: onPressOut`);
}}
style={({ pressed }) => [
styles.pressable,
pressed && { backgroundColor: 'goldenrod' },
]}>
<Text>Press me</Text>
</Pressable>
</View>
);
}

const styles = StyleSheet.create({
pressable: {
paddingVertical: 5,
paddingHorizontal: 10,
backgroundColor: 'red',
},
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 24,
},
});

export default App;
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export { default as Test1829 } from './Test1829';
export { default as Test1844 } from './Test1844';
export { default as Test1864 } from './Test1864';
export { default as Test1970 } from './Test1970';
export { default as Test1975 } from './Test1975';
export { default as Test1981 } from './Test1981';
export { default as Test2002 } from './Test2002';
export { default as Test2008 } from './Test2008';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ class RNSScreenComponentDescriptor final
public:
using ConcreteComponentDescriptor::ConcreteComponentDescriptor;

static constexpr const char *kScreenDummyLayoutHelperClass =
"com/swmansion/rnscreens/utils/ScreenDummyLayoutHelper";

void adopt(ShadowNode &shadowNode) const override {
react_native_assert(dynamic_cast<RNSScreenShadowNode *>(&shadowNode));
auto &screenShadowNode = static_cast<RNSScreenShadowNode &>(shadowNode);
Expand All @@ -39,8 +36,8 @@ class RNSScreenComponentDescriptor final
#ifdef ANDROID
if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
// When we receive dimensions from JVM side we can remove padding used for
// correction, and we can stop applying height correction for the frame.
// We want to leave top offset correction though intact.
// correction, and we can stop applying height and offset corrections for
// the frame.
// TODO: In future, when we have dynamic header height we might want to
// update Y offset correction here.

Expand All @@ -67,36 +64,11 @@ class RNSScreenComponentDescriptor final
screenShadowNode.setPadding({0, 0, 0, 0});
screenShadowNode.getFrameCorrectionModes().unset(
FrameCorrectionModes::Mode::FrameHeightCorrection);
screenShadowNode.getFrameCorrectionModes().unset(
FrameCorrectionModes::Mode::FrameOriginCorrection);

layoutableShadowNode.setSize(
Size{stateData.frameSize.width, stateData.frameSize.height});
} else {
// This code path should be executed only on the very first (few)
// layout(s), when we haven't received state update from JVM side yet.

auto headerConfigChildOpt = findHeaderConfigChild(layoutableShadowNode);

// During creation of the shadow node children are not attached yet.
// We also do not want to set any padding in case.
if (headerConfigChildOpt) {
const auto &headerConfigChild = headerConfigChildOpt->get();
const auto &headerProps =
*std::static_pointer_cast<const RNSScreenStackHeaderConfigProps>(
headerConfigChild->getProps());

const auto headerHeight = headerProps.hidden
? 0.f
: findHeaderHeight(
headerProps.titleFontSize, headerProps.title.empty())
.value_or(0.f);

screenShadowNode.setPadding({0, 0, 0, headerHeight});
screenShadowNode.setHeaderHeight(headerHeight);
screenShadowNode.getFrameCorrectionModes().set(
FrameCorrectionModes::Mode(
FrameCorrectionModes::Mode::FrameHeightCorrection |
FrameCorrectionModes::Mode::FrameOriginCorrection));
}
}
#else
if (stateData.frameSize.width != 0 && stateData.frameSize.height != 0) {
Expand All @@ -106,72 +78,6 @@ class RNSScreenComponentDescriptor final
#endif // ANDROID
ConcreteComponentDescriptor::adopt(shadowNode);
}

std::optional<std::reference_wrapper<const ShadowNode::Shared>>
findHeaderConfigChild(
const YogaLayoutableShadowNode &screenShadowNode) const {
for (const ShadowNode::Shared &child : screenShadowNode.getChildren()) {
if (std::strcmp(
child->getComponentName(), "RNSScreenStackHeaderConfig") == 0) {
return {std::cref(child)};
}
}
return {};
}

#ifdef ANDROID
std::optional<float> findHeaderHeight(
const int fontSize,
const bool isTitleEmpty) const {
JNIEnv *env = facebook::jni::Environment::current();

if (env == nullptr) {
LOG(ERROR) << "[RNScreens] Failed to retrieve env\n";
return {};
}

jclass layoutHelperClass = env->FindClass(kScreenDummyLayoutHelperClass);

if (layoutHelperClass == nullptr) {
LOG(ERROR) << "[RNScreens] Failed to find class with id "
<< kScreenDummyLayoutHelperClass;
return {};
}

jmethodID computeDummyLayoutID =
env->GetMethodID(layoutHelperClass, "computeDummyLayout", "(IZ)F");

if (computeDummyLayoutID == nullptr) {
LOG(ERROR)
<< "[RNScreens] Failed to retrieve computeDummyLayout method ID";
return {};
}

jmethodID getInstanceMethodID = env->GetStaticMethodID(
layoutHelperClass,
"getInstance",
"()Lcom/swmansion/rnscreens/utils/ScreenDummyLayoutHelper;");

if (getInstanceMethodID == nullptr) {
LOG(ERROR) << "[RNScreens] Failed to retrieve getInstanceMethodID";
return {};
}

jobject packageInstance =
env->CallStaticObjectMethod(layoutHelperClass, getInstanceMethodID);

if (packageInstance == nullptr) {
LOG(ERROR)
<< "[RNScreens] Failed to retrieve packageInstance or the package instance was null on JVM side";
return {};
}

jfloat headerHeight = env->CallFloatMethod(
packageInstance, computeDummyLayoutID, fontSize, isTitleEmpty);

return {headerHeight};
}
#endif // ANDROID
};

} // namespace react
Expand Down
Loading
Loading