Search code examples
reactjsreact-nativeanimationexpopanresponder

PanResponder to change Card Y value and issue of moving out of screen


I'm new to react-native animation API and PanResponder API.
I want to create google maps clone UI and animations of card which is show after results.
I want to create card and when user swipe up, the card you reach to top of screen and when user swipe down, the card should resize to original position.
Demo of Google Maps that i want to achieve


I have implemented some of code for that, but i am facing problem like card is moving out of screen in top direction (swiping up after card is reach to top position). Sometimes, swipe down works but not always.


My demo

Expo Snack Link


App.js

import React, { useEffect, useState } from "react";
import {
  SafeAreaView,
  View,
  Text,
  Dimensions,
  PanResponder,
  Animated,
} from "react-native";
import { Searchbar, FAB } from "react-native-paper";
import MapView from "react-native-maps";
import * as Location from "expo-location";


const SCREEN_HEIGHT = Dimensions.get("window").height;
const SCREEN_WIDTH = Dimensions.get("window").width;

const App = () => {
  

  const [latitute, setLatitute] = useState(0);
  const [longitute, setLongitute] = useState(0);
  const [isLoading, setIsLoading] = useState(true);


  const pan = useState(
    new Animated.ValueXY({ x: 0, y: SCREEN_HEIGHT - 200 })
  )[0];


  const panResponder = useState(
    PanResponder.create({
      onMoveShouldSetPanResponder: () => true,
      onPanResponderGrant: () => {
        pan.extractOffset();
        return true;
      },
      onPanResponderMove: (e, gestureState) => {
        pan.setValue({ x: 0, y: gestureState.dy });
      },
      onPanResponderRelease: (e, gestureState) => {
        
        if (gestureState.moveY > SCREEN_HEIGHT - 200) {
          Animated.spring(pan.y, {
            toValue: 0,
            tension: 1,
            useNativeDriver: true,
          }).start();
        } else if (gestureState.moveY < 200) {
          Animated.spring(pan.y, {
            toValue: 0,
            tension: 1,
            useNativeDriver: true,
          }).start();
        } else if (gestureState.dy < 0) {
          Animated.spring(pan.y, {
            toValue: -SCREEN_HEIGHT + 200,
            tension: 1,
            useNativeDriver: true,
          }).start();
        } else if (gestureState.dy > 0) {
          Animated.spring(pan.y, {
            toValue: SCREEN_HEIGHT - 200,
            tension: 1,
            useNativeDriver: true,
          }).start();
        }
      },
    })
  )[0];

  const animatedHeight = {
    transform: pan.getTranslateTransform(),
  };

  useEffect(() => {
    (async () => {
      let { status } = await Location.requestPermissionsAsync();

      let location = await Location.getCurrentPositionAsync({});
      console.log(location);
      setLatitute(location.coords.latitude);
      setLongitute(location.coords.longitude);
      setIsLoading(false);
    })();
  }, []);

  return (
    <SafeAreaView
      style={{
        display: "flex",
        flex: 1,
      }}
    >
      {!isLoading && (
        <View>
          <MapView
            style={{ width: SCREEN_WIDTH, height: SCREEN_HEIGHT }}
            showsUserLocation
            initialRegion={{
              latitude: latitute,
              longitude: longitute,
              latitudeDelta: 0.01,
              longitudeDelta: 0.01,
            }}
          />

          <Searchbar
            placeholder="Search"
            style={{
              position: "absolute",
              top: 10,
              margin: 10,
            }}
            
            icon="menu"
            onIconPress={() => {}}
          />

          <FAB
            style={{
              position: "absolute",
              top: SCREEN_HEIGHT * 0.8,
              left: SCREEN_WIDTH * 0.8,
            }}
            icon="plus"
            onPress={() => console.log("Pressed")}
          />

          <Animated.View
            style={[
              animatedHeight,
              {
                position: "absolute",
                right: 0,
                left: 0,
                width: SCREEN_WIDTH,
                height: SCREEN_HEIGHT,
                backgroundColor: "#0af",
                borderTopLeftRadius: 25,
                borderTopRightRadius: 25,
                zIndex: 10,
              },
            ]}
            {...panResponder.panHandlers}
          >
            <View>
              <Text>Hi</Text>
            </View>
          </Animated.View>
        </View>
      )}
    </SafeAreaView>
  );
};

export default App;



Solution

  • if you wanna make bottomsheet then you can use react-native-bottomsheet-reanimated

    yarn add react-native-bottomsheet-reanimated
    
    import React, { useEffect, useState } from "react";
    import {
      SafeAreaView,
      View,
      Text,
      Dimensions,
      PanResponder,
      Animated,
      StyleSheet
    } from "react-native";
    import { Searchbar, FAB } from "react-native-paper";
    import MapView from "react-native-maps";
    import * as Location from "expo-location";
    import BottomSheet from "react-native-bottomsheet-reanimated";
    
    
    
    const SCREEN_HEIGHT = Dimensions.get("window").height;
    const SCREEN_WIDTH = Dimensions.get("window").width;
    
    const App = () => {
      
    
      const [latitute, setLatitute] = useState(0);
      const [longitute, setLongitute] = useState(0);
      const [isLoading, setIsLoading] = useState(true);
    
    
      const pan = useState(
        new Animated.ValueXY({ x: 0, y: SCREEN_HEIGHT - 200 })
      )[0];
    
      useEffect(() => {
        (async () => {
          let { status } = await Location.requestPermissionsAsync();
    
          let location = await Location.getCurrentPositionAsync({});
          console.log(location);
          setLatitute(location.coords.latitude);
          setLongitute(location.coords.longitude);
          setIsLoading(false);
        })();
      }, []);
    
      return (
        <SafeAreaView
          style={{
            display: "flex",
            flex: 1,
          }}
        >
          {!isLoading && (
            <View>
              <MapView
                style={{ width: SCREEN_WIDTH, height: SCREEN_HEIGHT }}
                showsUserLocation
                initialRegion={{
                  latitude: latitute,
                  longitude: longitute,
                  latitudeDelta: 0.01,
                  longitudeDelta: 0.01,
                }}
              />
    
              <Searchbar
                placeholder="Search"
                style={{
                  position: "absolute",
                  top: 10,
                  margin: 10,
                }}
                
                icon="menu"
                onIconPress={() => {}}
              />
    
              <FAB
                style={{
                  position: "absolute",
                  top: SCREEN_HEIGHT * 0.8,
                  left: SCREEN_WIDTH * 0.8,
                }}
                icon="plus"
                onPress={() => console.log("Pressed")}
              />
    
              <BottomSheet
              bottomSheerColor="#FFFFFF"
              initialPosition={"30%"}  //200, 300
              snapPoints={["30%","100%"]}
              isBackDropDismisByPress={true}
              isRoundBorderWithTipHeader={true}
              containerStyle={{backgroundColor:"#0af"}}
              header={
                <View>
                  <Text style={styles.text}>Header</Text>
                </View>
              }
              body={
                <View style={styles.body}>
                  <Text>Hi</Text>
                </View>
              }
            />
    
            </View>
          )}
        </SafeAreaView>
      );
    };
    
    
    export default App;
    
    const styles = StyleSheet.create({
      body:{
        justifyContent:"center",
        alignItems:"center"
      },
      text:{
        fontSize:20,
        fontWeight:"bold"
      }
    });
    

    enter image description here