Krisna
Krisna

Reputation: 3423

React native Reanimated Conditional Animated View Movement

I am really new to React-native-reanimated. I am trying to create one custom bottom-sheet like this app. I am using PanGestureHandler from react-native-gesture-handler for move the Animated View to go up and down. For gestureHandler I am using useAnimatedGestureHandler props from react-native-reanimated. I want to move the Animated View from start point to middle screen and bottom of screen. This is My Bottom sheet start point image, when scroll the card down it should come middle of the screen like this image, again scroll down the card it will come bottom like this image.

I am having difficulties with the conditional useAnimatedGestureHandler onEnd movement. Currently I am tracking onEnd's event.translationY and make a condition out of it.

This is how it works currently:

When the App start, the Animated View is top of the screen, if I move the card scroll to bottom it goes middle of the screen and it does not go down from middle of the screen, I can move it to up from middle of the screen or if I scroll hard to bottom it goes all the way to bottom and if I try scroll the View up it does not go middle, it just goes up to start View.

I am trying to make the condition based screen size but I don't know how to make it.

I shared my code in expo-snacks

This is my all code

import React, { useState, useEffect } from "react";
import { StyleSheet, useWindowDimensions, RefreshControl } from "react-native";
import MapView from "react-native-maps";
import styled from "styled-components";

import {
  PanGestureHandler,
  PanGestureHandlerGestureEvent,
  FlatList,
} from "react-native-gesture-handler";

import Animated, {
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  Easing,
  withSpring,
} from "react-native-reanimated";

const initialRegion = {
  latitudeDelta: 15,
  longitudeDelta: 15,
  latitude: 60.1098678,
  longitude: 24.7385084,
};

const api =
  "http://open-api.myhelsinki.fi/v1/events/?distance_filter=60.1699%2C24.9384%2C10&language_filter=en&limit=50";

export default function App() {
  const { height } = useWindowDimensions();
  const top = useSharedValue(height);
  const [event, setEvent] = useState([]);
  const [loading, setLoading] = useState(false);
  const prevTop = useSharedValue(height * 0.5);
  // This is Fetch Data
  const fetchData = async () => {
    try {
      setLoading(true);
      const response = await fetch(api);
      const data = await response.json();
      setEvent(data.data);
      setLoading(false);
    } catch (error) {
      console.log("erro", error);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  const animatedStyle = useAnimatedStyle(() => {
    return {
      top: top.value * 0.2,
      bottom: 0,
    };
  });

  const gestureHandler = useAnimatedGestureHandler(
    {
      onStart(_, context) {
        context.translateY = top.value;
      },
      onActive(event, context) {
        top.value = context.translateY + event.translationY;
      },
      onEnd(event, _) {
        // THIS IS MY CONDITION OF ANIMATED VIEW
        if (event.translationY > 0 && event.translationY < 400) {
          console.log("middle-top", top.value);
          console.log("middle-height", height);

          top.value = withSpring(height * 2.5, {
            duration: 500,
            easing: Easing.inOut(Easing.ease),
          });
        } else if (event.translationY > 450 && event.translationY < 800) {
          console.log("bottom-top", top.value);
          console.log("bottom-height", height);
          top.value = withSpring(height * 4, {
            duration: 500,
            easing: Easing.inOut(Easing.ease),
          });
        } else if (event.translationY < 0) {
          console.log("start-top", top.value);
          console.log("start-height", height);
          top.value = withSpring(height, {
            duration: 500,
            easing: Easing.inOut(Easing.ease),
          });
        }
      },
    },
    [top]
  );

  return (
    <>
      <MapView style={styles.mapStyle} initialRegion={initialRegion} />
      <PanGestureHandler onGestureEvent={gestureHandler}>
        <Animated.View style={[styles.container, animatedStyle]}>
          <Title>I am scroll sheet</Title>
          <HeroFlatList
            data={event}
            refreshControl={
              <RefreshControl
                enabled={true}
                refreshing={loading}
                onRefresh={fetchData}
              />
            }
            keyExtractor={(_, index) => index.toString()}
            renderItem={({ item, index }) => {
              const image = item?.description.images.map((img) => img.url);
              const startDate = item?.event_dates?.starting_day;
              return (
                <EventContainer key={index}>
                  <EventImage
                    source={{
                      uri:
                        image[0] ||
                        "https://res.cloudinary.com/drewzxzgc/image/upload/v1631085536/zma1beozwbdc8zqwfhdu.jpg",
                    }}
                  />
                  <DescriptionContainer>
                    <Title ellipsizeMode="tail" numberOfLines={1}>
                      {item?.name?.en}
                    </Title>
                    <DescriptionText>
                      {item?.description?.intro || "No description available"}
                    </DescriptionText>
                    <DateText>{startDate}</DateText>
                  </DescriptionContainer>
                </EventContainer>
              );
            }}
          />
        </Animated.View>
      </PanGestureHandler>
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    position: "absolute",
    left: 0,
    right: 0,
    top: 0,

    backgroundColor: "white",
    shadowOffset: {
      height: -6,
      width: 0,
    },
    shadowOpacity: 0.1,
    shadowRadius: 5,
    borderTopEndRadius: 15,
    borderTopLeftRadius: 15,
  },
  mapStyle: {
    flex: 1,
  },
});

const HeroFlatList = styled(FlatList).attrs({
  contentContainerStyle: {
    flexGrow: 1,
  },
})`
  padding: 12px;
`;

const Title = styled.Text`
  font-size: 16px;
  font-weight: 700;
  margin-bottom: 10px;
  align-self: center;
  padding: 10px;
`;

const DescriptionText = styled.Text`
  font-size: 14px;
  opacity: 0.7;
`;

const DateText = styled.Text`
  font-size: 14px;
  opacity: 0.8;
  color: #0099cc;
`;

const EventImage = styled.Image`
  width: 70px;
  height: 70px;
  border-radius: 70px;
  margin-right: 20px;
`;

const DescriptionContainer = styled.View`
  width: 200px;
`;

const EventContainer = styled(Animated.View)`
  flex-direction: row;
  padding: 20px;
  margin-bottom: 10px;
  border-radius: 20px;
  background-color: #fff;
  shadow-color: #000;
  shadow-opacity: 0.3;
  shadow-radius: 20px;
  shadow-offset: 0 10px;
`;

Tech information

Tech Version
react-native-gesture-handler ^1.10.3
react-native-reanimated ^2.2.0

Upvotes: 3

Views: 3821

Answers (1)

Nilesh Patel
Nilesh Patel

Reputation: 3317

Not the perfect solution... added a new sharedValue to track if its moving up or down.

const prevTop = useSharedValue(height * 0.5);

and respective code on gesture end.

      onEnd() {
        if (top.value > prevTop.value) {
          top.value = withTiming(height * 0.98);
        } else {
          top.value = withTiming(Math.min(200, top.value));
        }
        prevTop.value = top.value;
      },

Still there is a scope of improvement.

Upvotes: 1

Related Questions