Pressable with border inside View with overflow: 'hidden' gets cut off on Android
Description
The handling of borderWidth for RNGH Pressable seems to be different than Pressable from 'react-native'. The clipping on the screenshot below is observable both with horizontal and vertical FlatLists. It also doesn't matter if FlatList from RNGH or RN is used, both produce the same results.
This issue only happens on Android and can be observed on debug/release build, both emulator and real devices consistently.
The fix seems to be to move the styles from the Pressable to a nested View.
Steps to reproduce
- Example code is provided in the gist
A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
https://gist.github.com/gigobyte/56574a3b2f95223f5bf40fb5b2d43efd
Gesture Handler version
2.27.2
React Native version
0.79.0
Platforms
Android
JavaScript runtime
None
Workflow
None
Architecture
New Architecture (Fabric)
Build type
None
Device
None
Device model
No response
Acknowledgements
Yes
Hey! 👋
The issue doesn't seem to contain a minimal reproduction.
Could you provide a snack or a link to a GitHub repository under your username that reproduces the problem?
I have narrowed down this issue to a faulty combination of border and overflow: 'hidden' because I can also reproduce this with a normal View:
<View style={{ overflow: 'hidden' }}>
<Pressable
style={{
borderWidth: 1,
width: 100,
height: 50,
}}
/>
</View>
I’m running into the same issue, and while the suggested workaround (wrapping the Pressable content inside an additional View) technically fixes the clipping, it creates problems on the architecture side in a real-world project with reusable components.
In our case, using a pattern like this becomes problematic:
const ButtonScaleComponent: FC<ButtonScaleProps> = ({
children,
style,
disabled = false,
onPress,
onLongPress,
onLayout,
}) => {
const pressed = useSharedValue(false)
const scale = useDerivedValue(() => {
if (disabled) return withTiming(1, { duration: 150 })
return pressed.value
? withTiming(0.97, { duration: 100 })
: withTiming(1, { duration: 150 })
})
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}))
return (
<AnimatedPressable
style={[animatedStyle, style]}
disabled={disabled}
onPress={onPress}
onPressIn={() => (pressed.value = true)}
onPressOut={() => (pressed.value = false)}
onLongPress={onLongPress}
onLayout={onLayout}
>
{children}
</AnimatedPressable>
)
}
If we add an extra View inside the Pressable just to avoid the bug, we’re forced to:
- split styling between the
Pressableand the inner View, - expose two different style props (e.g. pressableStyle and contentStyle),
- change the API of a component that should be simple and reusable,
- and propagate this complexity everywhere the component is used.
For small components this may seem minor, but in a larger codebase with many reusable UI primitives, it creates a lot of unnecessary overhead and breaks the internal logic of our components.
So while the workaround “works”, it’s not really practical in a scalable architecture. A proper fix would help avoid these kinds of structural issues.
@j-piasecki Sorry for tagging you, but it seems this issue may have been forgotten. Do you think someone could take a look at it or consider fixing it?
@TomCorvus We fixed it using this patch
diff --git a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt
index d51501120abdf073c852ca880cdccddfbe941684..164303f39fe5fe5b8a517baef1ca415a9fb64ede 100644
--- a/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt
+++ b/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt
@@ -13,6 +13,7 @@ import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.PaintDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.shapes.RectShape
import android.os.Build
import android.util.TypedValue
@@ -385,6 +386,11 @@ class RNGestureHandlerButtonViewManager :
}
}
+ // https://github.com/software-mansion/react-native-gesture-handler/issues/3668
+ if (borderWidth > 0f) {
+ val inset = Math.ceil((borderWidth / 2f).toDouble()).toInt()
+ return InsetDrawable(borderDrawable, inset)
+ }
return borderDrawable
}
Thanks, @gigobyte. Do you think this is a long-term solution or just a quick fix?
It's a quick fix, there are other border-related issues and it looks like a general fix was started but ultimately abandoned.
@j-piasecki Could you share some information about that PR, why has progress stalled?