react-native

mimic scrolling over a paper with rounded edges


I'm implementing a modal in React Native and want to create a unique scrolling effect where the modal has rounded corners that dynamically adjust based on scroll position. Specifically, I want the modal to appear like a scrollable page with rounded corners at both ends - when viewing the top of the content, the top corners should be rounded; when viewing the bottom, the bottom corners should be rounded; and when scrolling through the middle section, the corners should be square. This creates an effect similar to viewing a rounded card through a window, where the rounded edges are only visible when that portion is in view. The goal is to maintain the visual metaphor of a rounded card while providing a clear indication of scroll position.

So presently I have something like:

<View style={{borderRadius: 8}}>
  <ScrollView>
     // ... some content
  </ScrollView>
</View>

Which has the corners always rounded but it'd be cool if it rounded only at the top and bottom... Seems like this might be a standard effect but I haven't done much frontend previously


Solution

  • I suggest to make your whole modal scrollable and maintain the corner radius of the modal's body, but to answer your question to how can you make the corner radius dynamic by scrolling from top to bottom, here's my answer

    1. you need to listen to your scroll using ScrollView's onScroll
    2. check if your ScrollView's y offset is in the top or bottom

    here's my sample code

    
    const TestScreen = () => {
      const [borderRadiusStyle, setBorderRadiusStyle] = useState<ViewStyle>(styles.roundedTop);
    
      const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
        const heightBeforeRound = 0 // adjust this if you want your get your corner radius before reaching the tip of the scroll
    
        const offsetY = event.nativeEvent.contentOffset.y;
        const height = event.nativeEvent.contentSize.height
        const viewHeight = event.nativeEvent.layoutMeasurement.height
        
        if (offsetY <= heightBeforeRound) {
            setBorderRadiusStyle(styles.roundedTop)
        } else if (offsetY + viewHeight >= height - heightBeforeRound) {
            setBorderRadiusStyle(styles.roundedBottom)
        } else {
            setBorderRadiusStyle(styles.square)
        }
      };
    
      return (
        <View style={[styles.container]}>
          <ScrollView 
            contentContainerStyle={styles.scrollViewContent} 
            onScroll={handleScroll}
            scrollEventThrottle={16} // Adjust throttle for smoother updates
          >
            {/* Add your content here */}
        </View>
      );
    };
    
    const cornerRadius = 8 // adjust on how much corner radius do you want
    const styles = StyleSheet.create({
      container: {
        flex: 1
      },
      roundedTop: {
        borderTopLeftRadius: cornerRadius,
        borderTopRightRadius: cornerRadius,
        borderBottomLeftRadius: 0,
        borderBottomRightRadius: 0,
      },
      roundedBottom: {
        borderTopLeftRadius: 0,
        borderTopRightRadius: 0,
        borderBottomLeftRadius: cornerRadius,
        borderBottomRightRadius: cornerRadius,
      },
      square: {
        borderRadius: 0,
      },
      scrollViewContent: {
        padding: 16,
      },
    });
    

    Here's my explanation

    1. I use event.nativeEvent.contentOffset.y to get the offset y so we know where are you scrolling
    2. I use height and viewHeight to know if you're at the bottom
    3. I use the condition to check if you're at the bottom or at the top, else means you're at the center so I make it square

    Note: import those types since I use typescript, remove them if you only use javascript