Krisna
Krisna

Reputation: 3453

React native: Android soft keyboard push the View up

I have made one custom animated bottom sheet. It has two snap points. It starts top of the screen, if scroll the Animated card down it will come middle of the screen , again scroll down the card it will come bottom of the screen. If user scroll the Animated Card hard down then it will go all the way bottom. I made this component as a re-useable component.

I have made re-useable Search component.

I imported both of the components to the main-app component. Inside the Animated-Bottom-sheet I put the Search-component.

I have made condition for keyboard and Animated.View

My this logic works perfectly in IOS but in Android, it push all elements top of the screen.

There is lots of suggestion about KeyboardAvoidingView but in my case, it does not work.

In react-native's android's AndroidManifest.xml file. I made android:windowSoftInputMode="adjustPan" and it listen the Keyboard.addListener('keyboardDidHide' and push all the elements top of the screen, if I make android:windowSoftInputMode="adjustPan|adjustResize" then it does not listen the Event-lister. As a result, My logic does not work and it hides the Animated View under Android Keyboard. According to RN-documentation, Event-lister only listen adjustResize or adjustPan.

Issue:

I REALLY DON'T KNOW HOW TO FIX THIS ISSUE

Android Device behavior

DEMO

Code-Demo

expo-snacks

This is is my Animated View component

import React, { useState } from "react";
import { StyleSheet, Dimensions, Platform, View, Keyboard } from "react-native";
import {
  PanGestureHandler,
  PanGestureHandlerGestureEvent,
} from "react-native-gesture-handler";

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

interface Props {
  children: React.ReactNode;
}

const { height: SCREEN_HEIGHT } = Dimensions.get("screen");

const IPHONE_DEVICE_MIDDLE_SCREEN = Platform.OS === "ios" ? 4 : 4;
const IPHONE_DEVICE_BOTTOM_SCREEN = Platform.OS === "ios" ? 7.6 : 7.36;
const ANIMATED_DURATION = 300;

const LoadingContainer = styled.View`
  height: ${SCREEN_HEIGHT - 300}px;
  background-color: #fff;
  justify-content: center;
`;

const ScrollBottomSheet = ({ children }: Props) => {
  const contentTop = useSharedValue(SCREEN_HEIGHT);
  const [bottomSheetState, setBottomSheetState] = useState("top");
  // Event-listener
  Keyboard.addListener("keyboardDidShow", () => {
    if (bottomSheetState === "bottom") {
      contentTop.value = withSpring(
        SCREEN_HEIGHT * IPHONE_DEVICE_MIDDLE_SCREEN
      );
    }
  });
  const animatedStyle = useAnimatedStyle(() => {
    "worklet";
    return {
      top: contentTop.value * 0.1,
      bottom: 0,
    };
  });

  const gestureHandler = useAnimatedGestureHandler(
    {
      onStart(_, context) {
        context.translateY = contentTop.value;
      },
      onActive(event, context) {
        contentTop.value = context.translateY + event.translationY;
      },
      onEnd(event, _) {
        if (event.y > 0 && event.y < 200) {
          // MIDDLE SCREEN LOGIC
          contentTop.value = withTiming(
            SCREEN_HEIGHT * IPHONE_DEVICE_MIDDLE_SCREEN,
            {
              duration: ANIMATED_DURATION,
              easing: Easing.inOut(Easing.ease),
            }
          );
          runOnJS(setBottomSheetState)("middle");
          runOnJS(Keyboard.dismiss)(); // dismiss Keyboard
        } else if (event.y > 200) {
          // BOTTOM SCREEN LOGIC
          contentTop.value = withTiming(
            SCREEN_HEIGHT * IPHONE_DEVICE_BOTTOM_SCREEN,
            {
              duration: ANIMATED_DURATION,
              easing: Easing.inOut(Easing.ease),
            }
          ); 
         runOnJS(Keyboard.dismiss)();
          runOnJS(setBottomSheetState)("bottom");
        } else if (event.translationY < 0) {
          contentTop.value = withTiming(SCREEN_HEIGHT, {
            duration: ANIMATED_DURATION,
            easing: Easing.inOut(Easing.ease),
          });
          runOnJS(setBottomSheetState)("top");
        }
      },
    },
    [contentTop]
  );

  return (
    <PanGestureHandler onGestureEvent={gestureHandler}>
      <Animated.View style={[styles.container, animatedStyle]}>
        <View style={styles.grbber} />
        {children}
      </Animated.View>
    </PanGestureHandler>
  );
};

export default ScrollBottomSheet;

const styles = StyleSheet.create({
  container: {
    position: "absolute",
    left: 0,
    right: 0,
    top: 0,
    backgroundColor: "#fff",
    shadowOffset: {
      height: -6,
      width: 0,
    },
    shadowOpacity: 0.1,
    shadowRadius: 5,
    borderTopEndRadius: 15,
    borderTopLeftRadius: 15,
  },
  grbber: {
    width: 80,
    height: 5,
    marginBottom: 4,
    alignSelf: "center",
    marginTop: 5,
    borderTopWidth: 5,
    borderTopColor: "#aaa",
  },
});

Search Component

import React, { useRef, useEffect } from "react";
import {
  Animated,
  KeyboardTypeOptions,
  TextInputProps,
  TextInput,
  Text,
  Keyboard,
} from "react-native";

import styled from "styled-components/native";

type Props = TextInputProps & {
  text: string,
  hint: string,
  onChangeText?: ((text: string) => void) | undefined,
  onClearText: () => void,
  animatedStyle?: { height: Animated.AnimatedInterpolation },
  keyboardType?: KeyboardTypeOptions,

  autoFocus?: boolean,
};

const Container = styled(Animated.View)`
  flex-direction: row;
  background-color: grey;
  border-radius: 6px;
  align-items: center;
`;

const IconTouchableOpacity = styled.TouchableOpacity`
  padding: 16px;
`;

const SearchInput = styled.TextInput`
  flex: 1;
  padding-right: 16px;
  color: #fff;
  font-size: 16px;
  line-height: 20px;
  height: 44px;
`;

const SearchBar = ({
  text,
  hint,
  onChangeText,
  onClearText,
  animatedStyle,
  keyboardType,
  maxLength,
  autoFocus,
}: Props) => {
  const searchInput = useRef(null);

  const onSearchPress = () => {
    searchInput?.current?.focus();
  };

  useEffect(() => {
    if (autoFocus) {
      onSearchPress();
    }
  }, [autoFocus]);

  return (
    <Container accessible={false} style={animatedStyle}>
      <SearchInput
        ref={searchInput}
        onChangeText={onChangeText}
        value={text}
        placeholder={hint}
        maxLength={maxLength}
        underlineColorAndroid={"transparent"}
        placeholderTextColor={"grey"}
        keyboardType={keyboardType}
        autoFocus={autoFocus}
        onKeyPress={() => hideKeyBoard}
      />
    </Container>
  );
};

export default SearchBar;

**App Component

import React, { useState, useEffect } from "react";
import { StyleSheet, Button } from "react-native";
import { TouchableHighlight } from "react-native-gesture-handler";
import MapView from "react-native-maps";
import styled from "styled-components";
import ScrollBottomSheet from "./components/ActionSheet";
import SearchBar from "./components/SearchBar";

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

export default function App() {
  return (
    <>
      <MapView style={styles.mapStyle} initialRegion={initialRegion} />
      <ScrollBottomSheet>
        <SearchContainer>
          <SearchBar hint={"search"} />
        </SearchContainer>
      </ScrollBottomSheet>
    </>
  );
}

const styles = StyleSheet.create({
  mapStyle: {
    height: "100%",
  },
});

const SearchContainer = styled.View`
  padding: 10px;
`;

Upvotes: 5

Views: 3487

Answers (1)

zelda11
zelda11

Reputation: 433

Have you tried to use Animated.ScrollView instead Animated.View?

Upvotes: 0

Related Questions