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

Flashlist is not scrolling #2582

Closed
xts-bit opened this issue Sep 4, 2023 · 25 comments
Closed

Flashlist is not scrolling #2582

xts-bit opened this issue Sep 4, 2023 · 25 comments
Labels
Missing repro Platform: Android This issue is specific to Android

Comments

@xts-bit
Copy link

xts-bit commented Sep 4, 2023

Description

THE ISSUE IS ONLY ON ANDROID

Flashlist is not scrolling when I wrap the List component to BottomSheet component, However when I comment out the BottomSheet component it works fine. So the issue is related to the BottomSheet component.

The List component itself is been rendered inside a flashlist, This is how I'm using it

<GestureHandlerRootView style={{  height: '100%', width: '100%', position: 'absolute' }}>
                            <View style={{ position: 'absolute', height: '100%', width: '100%', }}>
                                <BottomSheet ref={ref}>
                                    <List />
                                </BottomSheet>
                            </View>
 </GestureHandlerRootView>

BottomSheet component:

import { Dimensions, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useImperativeHandle } from 'react';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

const { height: SCREEN_HEIGHT } = Dimensions.get('window');

const MAX_TRANSLATE_Y = -SCREEN_HEIGHT + 50;

type BottomSheetProps = {
  children?: React.ReactNode;
};

export type BottomSheetRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomSheet = React.forwardRef<BottomSheetRefProps, BottomSheetProps>(
  ({ children }, ref) => {
    const translateY = useSharedValue(0);
    const active = useSharedValue(false);

    const scrollTo = useCallback((destination: number) => {
      'worklet';
      active.value = destination !== 0;

      translateY.value = withSpring(destination, { damping: 50 });
    }, []);

    const isActive = useCallback(() => {
      return active.value;
    }, []);

    useImperativeHandle(ref, () => ({ scrollTo, isActive }), [
      scrollTo,
      isActive,
    ]);

    const context = useSharedValue({ y: 0 });
    const gesture = Gesture.Pan()
      .onStart(() => {
        context.value = { y: translateY.value };
      })
      .onUpdate((event) => {
        translateY.value = event.translationY + context.value.y;
        translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
      })
      .onEnd(() => {
        if (translateY.value > -SCREEN_HEIGHT / 3) {
          scrollTo(0);
        } else if (translateY.value < -SCREEN_HEIGHT / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

    const rBottomSheetStyle = useAnimatedStyle(() => {
      const borderRadius = interpolate(
        translateY.value,
        [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],
        [25, 5],
        Extrapolate.CLAMP
      );

      return {
        borderRadius,
        transform: [{ translateY: translateY.value }],
      };
    });

    return (
      <View>
        <GestureDetector gesture={gesture}>
          <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
            <View style={styles.line} />
            {children}
          </Animated.View>
        </GestureDetector>
      </View>
    );
  }
);

const styles = StyleSheet.create({
  bottomSheetContainer: {
    height: SCREEN_HEIGHT,
    width: '100%',
    backgroundColor: 'black',
    position: 'absolute',
    top: SCREEN_HEIGHT,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
});

export default BottomSheet;

List Component:

import { StyleSheet, Text, View } from 'react-native'
import React from 'react'
import { FlashList } from '@shopify/flash-list'

const List = () => {
    const data = [
        { id: '1', title: 'Item 1' },
        { id: '2', title: 'Item 2' },
        { id: '3', title: 'Item 1' },
        { id: '243', title: 'Item 2' },
        { id: '14dff3', title: 'Item 1' },
        { id: '2564ewe36', title: 'Item 2' },
        { id: '24ere3', title: 'Item 2' },
        { id: '14drereff3', title: 'Item 1' },
        { id: '25erer6436', title: 'Item 2' },
        { id: '24rereer3', title: 'Item 2' },
        { id: '14dferrf3', title: 'Item 1' },
        { id: '2564er36', title: 'Item 2' }
    ];

    return (
        <View style={styles.container}>
            <View style={{ flex: 1, maxHeight: '80%', }}>
                <FlashList
                    nestedScrollEnabled={true}
                    data={data}
                    estimatedItemSize={100}
                    contentContainerStyle={{ padding: 37 }}
                    renderItem={({ item }) => (
                        <>
                            <Text style={styles.text}>
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                                React Native
                            </Text>
                        </>
                    )}
                    keyExtractor={(item) => item.id}
                />
            </View>
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        backgroundColor: 'black',
        borderTopLeftRadius: 40,
        borderTopRightRadius: 40,
        height: '100%',
        width: '100%',
        position: 'absolute'
    },
    text: {
        color: 'gray',
        fontWeight: '500',
        fontSize: 14,
        left: 15,
        textAlign: 'left',
        width: '90%',
        marginBottom: 20
    },
})
export default List

Steps to reproduce

Take your mouse in the list and just Drag down the list it won't work on Andriod but works fine on iOS.

Snack or a link to a repository

null

Gesture Handler version

2.12.1

React Native version

0.72.4

Platforms

Android

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Fabric (New Architecture)

Build type

Debug mode

Device

Android emulator

Device model

Google Pixel 4

Acknowledgements

Yes

@github-actions
Copy link

github-actions bot commented Sep 4, 2023

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?

@github-actions github-actions bot added Missing repro Platform: Android This issue is specific to Android labels Sep 4, 2023
@bbernag
Copy link

bbernag commented Sep 5, 2023

This is because of the nested gestures, you should use waitFor in your GestureDetector

@xts-bit
Copy link
Author

xts-bit commented Sep 5, 2023

@bbernag Can you please give a example?

@xts-bit
Copy link
Author

xts-bit commented Sep 5, 2023

This doesn't work <GestureDetector waitFor={ref} gesture={gesture}>

@bbernag
Copy link

bbernag commented Sep 5, 2023

You need to pass down the ref to the FlashList, here's an example

@xts-bit
Copy link
Author

xts-bit commented Sep 5, 2023

@bbernag But I have two flashlists, One for the List component as you see in my code and another in which everything is rendered the main one. (Renders the BottomSheet which renders the List component)

I'm confused to get that ref of those.

@bbernag
Copy link

bbernag commented Sep 5, 2023

The docs says waitFor accepts an object ref or array of refs, so you need to create two refs and pass them in an array to waitFor

@xts-bit
Copy link
Author

xts-bit commented Sep 5, 2023

@bbernag So I create two refs using useRef in my BottomSheet component as pass it in waitFor in GestureDetector. Right?🙏

@xts-bit
Copy link
Author

xts-bit commented Sep 5, 2023

@bbernag Using this also works <PanGestureHandler activeOffsetX={[-10, 10]}>. But activeOffsetX prop is not there in GestureDetector, Any idea what to do?

@xts-bit
Copy link
Author

xts-bit commented Sep 5, 2023

One solution i tried is giving .activeOffsetY([-30, 0]) to my GestureDetector but it affects the animations

Can you please give me a better approach for this

@m-bert
Copy link
Contributor

m-bert commented Sep 6, 2023

Hi @xts-bit! Adding activeOffset (for example .activeOffsetY([-10,10]) should solve your problem. This is because it gives flashlist time to cancel pan. How does this affect your animations?

@xts-bit
Copy link
Author

xts-bit commented Sep 6, 2023

@m-bert .activeOffsetY([-10,10]) is not scrolling the list but when I do .activeOffsetY([-20,10]) it scrolls, However some times it stuck the animation of the BottomSheet Can you please check my code maybe I did something wrong?

Also, onPress is not working when I use this code when i comment out it works what problem with this code


<GestureHandlerRootView style={{  height: '100%', width: '100%', position: 'absolute' }}>
                            <View style={{ position: 'absolute', height: '100%', width: '100%', }}>
                                <BottomSheet ref={ref}>
                                    <List />
                                </BottomSheet>
                            </View>
 </GestureHandlerRootView>

@m-bert
Copy link
Contributor

m-bert commented Sep 6, 2023

I can check your code, but could you please prepare copy-pastable repro (or share a link to your repository)? It will be much easier to check what's going on.

@xts-bit
Copy link
Author

xts-bit commented Sep 6, 2023

@m-bert I tried to make a copy-pastable repro please check

import React, { useCallback, useEffect, useRef, useImperativeHandle } from 'react';
import { View, Text, Image, Dimensions, StyleSheet, Button } from 'react-native';
import { GestureHandlerRootView, Gesture } from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { FlashList } from '@shopify/flash-list';

const { height, width } = Dimensions.get('window');

const MAX_TRANSLATE_Y = -height + 50;

type BottomModalProps = {
  children?: React.ReactNode;
};

export type BottomModalRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomModal = React.forwardRef<BottomModalRefProps, BottomModalProps>(({ children }, ref) => {
  const translateY = useSharedValue(0);
  const active = useSharedValue(false);

  const scrollTo = useCallback((destination: number) => {
    'worklet';
    active.value = destination !== 0;
    translateY.value = withSpring(destination, { damping: 50 });
  }, []);

  const isActive = useCallback(() => {
    return active.value;
  }, []);

  useImperativeHandle(ref, () => ({ scrollTo, isActive }), [
    scrollTo,
    isActive,
  ]);

  const context = useSharedValue({ y: 0 });
  const gesture = Gesture.Pan()
    .activeOffsetY([-20, 10])
    .onStart(() => {
      context.value = { y: translateY.value };
    })
    .onUpdate((event) => {
      translateY.value = event.translationY + context.value.y;
      translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
    })
    .onEnd(() => {
      if (translateY.value > -height / 3) {
        scrollTo(0);
      } else if (translateY.value < -height / 1.5) {
        scrollTo(MAX_TRANSLATE_Y);
      }
    });

  const rBottomSheetStyle = useAnimatedStyle(() => {
    const borderRadius = interpolate(
      translateY.value,
      [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],
      [25, 5],
      Extrapolate.CLAMP
    );

    return {
      borderRadius,
      transform: [{ translateY: translateY.value }],
    };
  });

  return (
    <GestureHandlerRootView style={{ height: '100%', width: '100%', position: 'absolute' }}>
      <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]} >
        <View style={styles.line} />
        {children}
      </Animated.View >
    </GestureHandlerRootView >
  );
});

const List = () => {
  const data = [
    { id: '1', title: 'Item 1' },
    { id: '2', title: 'Item 2' },
    { id: '1234', title: 'Item 1' },
    { id: '22342', title: 'Item 2' },
    { id: '123423', title: 'Item 1' },
    { id: '1312', title: 'Item 2' },
    { id: '124234', title: 'Item 1' },
    { id: '5253464562', title: 'Item 2' },
    { id: '1234234', title: 'Item 1' },
    { id: '264563234', title: 'Item 2' },
    { id: '134534524', title: 'Item 1' },
    { id: '2342342', title: 'Item 2' },
  ];

  return (
    <View style={styles.container}>
      <View style={{ flex: 1, maxHeight: '90%', }}>
        <FlashList
          nestedScrollEnabled={true}
          data={data}
          estimatedItemSize={100}
          contentContainerStyle={{ padding: 37 }}
          renderItem={({ item }) => (
            <>
              <Text>{item.title}</Text>
            </>
          )}
          keyExtractor={(item) => item.id}
        />
      </View>
    </View>
  );
}

const Display = () => {
  const ref = useRef<BottomModalRefProps>(null);

  const HandleonPress = () => {
    const isActive = ref?.current?.isActive();
    if (isActive) {
      ref?.current?.scrollTo(0);
    } else {
      ref?.current?.scrollTo(-550);
    }
  };

  useEffect(() => {
    setTimeout(() => {
      HandleonPress();
    }, 2000);
  }, []);

  return (
    <View style={{ backgroundColor: 'gray' }}>
      <Image
        source={{ uri: 'https://i.natgeofe.com/n/4f5aaece-3300-41a4-b2a8-ed2708a0a27c/domestic-dog_thumb_square.jpg' }}
        style={styles.backgroundProduct}
      />
      <View style={{ position: 'absolute', top: '20%', alignContent: 'center' }}>
        <Button title='Press me'
          onPress={() => console.log("Hello, World")}
        />
      </View>
      <GestureHandlerRootView style={{ height: '100%', width: '100%', position: 'absolute' }}>
        <View style={{ position: 'absolute', height: '100%', width: '100%' }}>
          <BottomModal ref={ref}>
            <List />
          </BottomModal>
        </View>
      </GestureHandlerRootView>
    </View >
  );
};

const Home = () => {

  let data = [1, 2]
  const renderItem = useCallback(({ item, index }) => {
    return (
      <Display />
    );
  }, []);

  return (
    <View style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0
        }}
        estimatedItemSize={20}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  backgroundProduct: {
    height,
    width,
  },
  bottomSheetContainer: {
    height: height,
    width: '100%',
    backgroundColor: 'white',
    position: 'absolute',
    top: height,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
  container: {
    backgroundColor: 'white',
    borderTopLeftRadius: 40,
    borderTopRightRadius: 40,
    height: '70%',
  },
});

export default Home

@m-bert
Copy link
Contributor

m-bert commented Sep 7, 2023

Okay, I've looked at it and noticed few things:

  1. Use <GestureHandlerRootView> at top level of your application. Also you can treat it as standard <View> and for example apply styles to it. So instead of
 <View style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0
        }}
        estimatedItemSize={20}
      />
    </View>

in your Home component, do:

    <GestureHandlerRootView style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0,
        }}
        estimatedItemSize={20}
      />
    </GestureHandlerRootView>

so that gestures will work properly and you won't have to use another <GestureHandlerRootView>

  1. In your repro you don't actually use GestureHandler. I assume you've overlooked that and code inside BottomModal should have <GestureDetector> instead of <GestureHandlerRootView>
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
          <View style={styles.line} />
          {children}
        </Animated.View>
      </GestureDetector>
  1. onPress is not working because you use many <View> components with position: 'absolute'; width: 100%; height:100%;. If you inspect that, they actually lie on top of your button hence it doesn't work.

  2. I've managed to fix issues mentioned above and it seems to work fine with activeOffsetY. The thing is, if you set activeOffsetY, pan won't activate unless you exceed its activation threshold. In the meantime FlashList has its opportunity to activate, therefore cancelling pan - this is why scrolling FlashList works and BottomSheet is not moving.

To summarize, I've fixed some problems with your repro and by adding activeOffsetY GestureHandler seems to work as expected. I haven't seen it affecting the animation either. Note that you can manipulate with activeOffsetY and increase its value if the one that you're using right now doesn't satisfy you.

Here is the corrected code:

import React, {
  useCallback,
  useEffect,
  useRef,
  useImperativeHandle,
} from 'react';
import {
  View,
  Text,
  Image,
  Dimensions,
  StyleSheet,
  Button,
} from 'react-native';
import {
  GestureHandlerRootView,
  Gesture,
  GestureDetector,
} from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { FlashList } from '@shopify/flash-list';

const { height, width } = Dimensions.get('window');

const MAX_TRANSLATE_Y = -height + 50;

type BottomModalProps = {
  children?: React.ReactNode;
};

export type BottomModalRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomModal = React.forwardRef<BottomModalRefProps, BottomModalProps>(
  ({ children }, ref) => {
    const translateY = useSharedValue(0);
    const active = useSharedValue(false);

    const scrollTo = useCallback((destination: number) => {
      'worklet';
      active.value = destination !== 0;
      translateY.value = withSpring(destination, { damping: 50 });
    }, []);

    const isActive = useCallback(() => {
      return active.value;
    }, []);

    useImperativeHandle(ref, () => ({ scrollTo, isActive }), [
      scrollTo,
      isActive,
    ]);

    const context = useSharedValue({ y: 0 });
    const gesture = Gesture.Pan()
      .activeOffsetY([-10, 10])
      .onStart(() => {
        context.value = { y: translateY.value };
      })
      .onUpdate((event) => {
        translateY.value = event.translationY + context.value.y;
        translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
      })
      .onEnd(() => {
        if (translateY.value > -height / 3) {
          scrollTo(0);
        } else if (translateY.value < -height / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

    const rBottomSheetStyle = useAnimatedStyle(() => {
      const borderRadius = interpolate(
        translateY.value,
        [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],
        [25, 5],
        Extrapolate.CLAMP
      );

      return {
        borderRadius,
        transform: [{ translateY: translateY.value }],
      };
    });

    return (
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
          <View style={styles.line} />
          {children}
        </Animated.View>
      </GestureDetector>
    );
  }
);

const List = () => {
  const generateObjects = () => {
    const objects = [];
    for (let i = 1; i <= 50; i++) {
      const object = {
        id: i.toString(),
        title: `Item ${i}`,
      };
      objects.push(object);
    }
    return objects;
  };

  return (
    <View style={styles.container}>
      <View style={{ flex: 1, maxHeight: '90%' }}>
        <FlashList
          nestedScrollEnabled={true}
          data={generateObjects()}
          estimatedItemSize={100}
          contentContainerStyle={{ padding: 37 }}
          renderItem={({ item }) => (
            <>
              <Text>{item.title}</Text>
            </>
          )}
          keyExtractor={(item) => item.id}
        />
      </View>
    </View>
  );
};

const Display = () => {
  const ref = useRef<BottomModalRefProps>(null);

  const HandleonPress = () => {
    const isActive = ref?.current?.isActive();
    if (isActive) {
      ref?.current?.scrollTo(0);
    } else {
      ref?.current?.scrollTo(-550);
    }
  };

  useEffect(() => {
    setTimeout(() => {
      HandleonPress();
    }, 2000);
  }, []);

  return (
    <View style={{ backgroundColor: 'gray' }}>
      <Image
        source={{
          uri: 'https://i.natgeofe.com/n/4f5aaece-3300-41a4-b2a8-ed2708a0a27c/domestic-dog_thumb_square.jpg',
        }}
        style={styles.backgroundProduct}
      />
      <View
        style={{ position: 'absolute', top: '20%', alignContent: 'center' }}>
        <Button title="Press me" onPress={() => console.log('Hello, World')} />
      </View>
      <View style={{ position: 'absolute', height: '100%', width: '100%' }}>
        <BottomModal ref={ref}>
          <List />
        </BottomModal>
      </View>
    </View>
  );
};

const Home = () => {
  let data = [1, 2];
  const renderItem = useCallback(({ item, index }) => {
    return <Display />;
  }, []);

  return (
    <GestureHandlerRootView style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0,
        }}
        estimatedItemSize={20}
      />
    </GestureHandlerRootView>
  );
};

const styles = StyleSheet.create({
  backgroundProduct: {
    height,
    width,
  },
  bottomSheetContainer: {
    height: height,
    width: '100%',
    backgroundColor: 'white',
    position: 'absolute',
    top: height,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
  container: {
    backgroundColor: 'white',
    borderTopLeftRadius: 40,
    borderTopRightRadius: 40,
    height: '70%',
  },
});

export default Home;

@xts-bit
Copy link
Author

xts-bit commented Sep 7, 2023

@m-bert Thanks! But still onPress is not working any idea? on iOS

@m-bert
Copy link
Contributor

m-bert commented Sep 7, 2023

Yes, look at point 3 in my previous response. If you open hierarchy view in Xcode you'll see that there is RCTView that lies on top of your button. Try changing

      <View style={{ position: 'absolute', height: '100%', width: '100%' }}>
        <BottomModal ref={ref}>
          <List />
        </BottomModal>
      </View>

to

        <BottomModal ref={ref}>
          <List />
        </BottomModal>

inside Display component, that should do the trick.

@xts-bit
Copy link
Author

xts-bit commented Sep 7, 2023

@m-bert Yep, It fixes my issue, but are you sure that .activeOffsetY([-10, 10]) won't affect my list scrolling on Andriod, in the Display component any idea how to set a state to false when the user close the BottomModal like when the value of translateY is 0

@m-bert
Copy link
Contributor

m-bert commented Sep 7, 2023

What do you mean by "affect my list scrolling"? Adding activeOffsetY changes activation threshold for Pan, therefore FlashList will have time to activate.

When it comes to setting state, you already have onEnd method on your pan, can't you do it here?

 .onEnd(() => {
        if (translateY.value > -height / 3) {
          scrollTo(0);
          // Here set desired state to false
        } else if (translateY.value < -height / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

@xts-bit
Copy link
Author

xts-bit commented Sep 7, 2023

it gives ERROR ReanimatedError: Tried to synchronously call a non-worklet function on the UI thread. error, like when user close the modal in bottommodal it set a state to false in display component

@m-bert
Copy link
Contributor

m-bert commented Sep 7, 2023

Oh, makes sense, I overlooked that it all happens on the UI thread. One way would be to add .runOnJS(true), but I guess it can break some stuff.

Btw. in your code you have active variable which is actually a SharedValue, isn't it something that you want to use? This can be modified inside onEnd.

@xts-bit
Copy link
Author

xts-bit commented Sep 7, 2023

. @m-bert Yeah but it's not working, i tried this approach but it doesn't work when the user manually closes the modal. Can you please check.

import React, {
  useCallback,
  useEffect,
  useRef,
  useImperativeHandle,
} from 'react';
import {
  View,
  Text,
  Image,
  Dimensions,
  StyleSheet,
  Button,
} from 'react-native';
import {
  GestureHandlerRootView,
  Gesture,
  GestureDetector,
} from 'react-native-gesture-handler';
import Animated, {
  Extrapolate,
  interpolate,
  useAnimatedStyle,
  useSharedValue,
  withSpring,
} from 'react-native-reanimated';
import { FlashList } from '@shopify/flash-list';

const { height, width } = Dimensions.get('window');

const MAX_TRANSLATE_Y = -height + 50;

type BottomModalProps = {
  children?: React.ReactNode;
};

export type BottomModalRefProps = {
  scrollTo: (destination: number) => void;
  isActive: () => boolean;
};

const BottomModal = React.forwardRef<BottomModalRefProps, BottomModalProps>(
  ({ children }, ref) => {
    const translateY = useSharedValue(0);
    const active = useSharedValue(false);

    const scrollTo = useCallback((destination: number) => {
      'worklet';
      active.value = destination !== 0;
      translateY.value = withSpring(destination, { damping: 50 });
    }, []);

    const isActive = useCallback(() => {
      return active.value;
    }, []);

    useImperativeHandle(ref, () => ({ scrollTo, isActive }), [
      scrollTo,
      isActive,
    ]);

    const context = useSharedValue({ y: 0 });
    const gesture = Gesture.Pan()
      .activeOffsetY([-10, 10])
      .onStart(() => {
        context.value = { y: translateY.value };
      })
      .onUpdate((event) => {
        translateY.value = event.translationY + context.value.y;
        translateY.value = Math.max(translateY.value, MAX_TRANSLATE_Y);
      })
      .onEnd(() => {
        if (translateY.value > -height / 3) {
          scrollTo(0);
        } else if (translateY.value < -height / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

    const rBottomSheetStyle = useAnimatedStyle(() => {
      const borderRadius = interpolate(
        translateY.value,
        [MAX_TRANSLATE_Y + 50, MAX_TRANSLATE_Y],
        [25, 5],
        Extrapolate.CLAMP
      );

      return {
        borderRadius,
        transform: [{ translateY: translateY.value }],
      };
    });

    return (
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.bottomSheetContainer, rBottomSheetStyle]}>
          <View style={styles.line} />
          {children}
        </Animated.View>
      </GestureDetector>
    );
  }
);

const List = () => {
  const generateObjects = () => {
    const objects = [];
    for (let i = 1; i <= 50; i++) {
      const object = {
        id: i.toString(),
        title: `Item ${i}`,
      };
      objects.push(object);
    }
    return objects;
  };

  return (
    <View style={styles.container}>
      <View style={{ flex: 1, maxHeight: '90%' }}>
        <FlashList
          nestedScrollEnabled={true}
          data={generateObjects()}
          estimatedItemSize={100}
          contentContainerStyle={{ padding: 37 }}
          renderItem={({ item }) => (
            <>
              <Text>{item.title}</Text>
            </>
          )}
          keyExtractor={(item) => item.id}
        />
      </View>
    </View>
  );
};

const Display = () => {
  const ref = useRef<BottomModalRefProps>(null);
  const [isModalActive, setIsModalActive] = React.useState(false);

  const HandleonPress = () => {
    const isActive = ref?.current?.isActive();
    if (isActive) {
      ref?.current?.scrollTo(0);
      setIsModalActive(false); 
    } else {
      ref?.current?.scrollTo(-550);
      setIsModalActive(true); 
    }
  };

  console.log(isModalActive)

  useEffect(() => {
    setTimeout(() => {
      HandleonPress();
    }, 2000);
  }, []);

  return (
    <View style={{ backgroundColor: 'gray' }}>
      <Image
        source={{
          uri: 'https://i.natgeofe.com/n/4f5aaece-3300-41a4-b2a8-ed2708a0a27c/domestic-dog_thumb_square.jpg',
        }}
        style={styles.backgroundProduct}
      />
      <View
        style={{ position: 'absolute', top: '20%', alignContent: 'center' }}>
        <Button title="Press me" onPress={() => console.log('Hello, World')} />
      </View>
      <View style={{ position: 'absolute', height: '100%', width: '100%' }}>
        <BottomModal ref={ref}>
          <List />
        </BottomModal>
         {
          isModalActive ? <Text style={{color: 'red', position: 'absolute', top: '20%'}}>Modal is Openn</Text> 
          :
          <Text style={{color: 'red', position: 'absolute', top: '20%'}}>Modal is closed</Text>
         }
      </View>
    </View>
  );
};

const Home = () => {
  let data = [1, 2];
  const renderItem = useCallback(({ item, index }) => {
    return <Display />;
  }, []);

  return (
    <GestureHandlerRootView style={{ height, width, backgroundColor: 'pink' }}>
      <FlashList
        data={data}
        renderItem={renderItem}
        nestedScrollEnabled={true}
        pagingEnabled
        viewabilityConfig={{
          itemVisiblePercentThreshold: 0,
        }}
        estimatedItemSize={20}
      />
    </GestureHandlerRootView>
  );
};

const styles = StyleSheet.create({
  backgroundProduct: {
    height,
    width,
  },
  bottomSheetContainer: {
    height: height,
    width: '100%',
    backgroundColor: 'white',
    position: 'absolute',
    top: height,
    borderRadius: 25,
  },
  line: {
    width: 75,
    height: 4,
    backgroundColor: 'grey',
    alignSelf: 'center',
    marginVertical: 15,
    borderRadius: 2,
  },
  container: {
    backgroundColor: 'white',
    borderTopLeftRadius: 40,
    borderTopRightRadius: 40,
    height: '70%',
  },
});

export default Home;

@m-bert
Copy link
Contributor

m-bert commented Sep 7, 2023

If you meant that red text is not changing, then surely it's not - it is inside another component. What you can do is try to import runOnJS from Reanimated, and then pass setState function to your BottomSheet:

import { runOnJS } from 'react-native-reanimated';

type BottomModalProps = {
  children?: React.ReactNode;
  closingFn?: () => void;
};

...

.onEnd(() => {
        if (translateY.value > -height / 3) {
          scrollTo(0);
          runOnJS(closingFn!)();
        } else if (translateY.value < -height / 1.5) {
          scrollTo(MAX_TRANSLATE_Y);
        }
      });

...

 <BottomModal ref={ref} closingFn={() => setIsModalActive(false)}>
          <List />
 </BottomModal>

Note that ! in runOnJS(closingFn!)() is present because in props it is marked with ?.

Also I think we are getting out of scope of this issue. If the problem is solved, could you please close it?

@xts-bit xts-bit closed this as completed Sep 7, 2023
@xts-bit
Copy link
Author

xts-bit commented Sep 7, 2023

Yep, I did close this, But Thanks for the help Michał :)

@m-bert
Copy link
Contributor

m-bert commented Sep 8, 2023

You're welcome! I'm glad I could help :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Missing repro Platform: Android This issue is specific to Android
Projects
None yet
Development

No branches or pull requests

3 participants