KeyboardAvoidingView + React Navigation + 安全区域视图 = 不工作

Posted

技术标签:

【中文标题】KeyboardAvoidingView + React Navigation + 安全区域视图 = 不工作【英文标题】:KeyboardAvoidingView + React Navigation + Safe Area View = not working 【发布时间】:2020-10-20 22:29:42 【问题描述】:

我遇到的问题是,在使用嵌套在选项卡导航器中的react-navigation 堆栈导航器以获得基本的聊天 UI 外观时,键盘隐藏了底部的聊天消息输入字段。所以我尝试KeyboardAvoidingView 将键盘移到可见位置,但键盘没有显示。我尝试了一个解决方案,包括将headerHeight 添加到keyboardVerticalOffset 道具,但它似乎减少了 50px 左右。例如,如果我将headerHeight + 50 添加到keyboardVerticalOffset,一切看起来都很棒,但如果我将设备切换到 iPhone 5 或其他设备,屏幕更小,SafeArea 插入不同等,键盘将再次处于错误位置。

我不确定罪魁祸首究竟是什么,但我现在认为它是顶部和/或底部的 SafeArea 填充,我了解到这是“插图”。我正在尝试使用useSafeAreaInsets,但所有值都返回 0!我想使用这些插图添加到 keyboardVerticalOffset 道具,以便避免视图正常工作。

我现在喜欢标签栏的样式,所以我想保持它的高度、填充和字体大小增加,但也许我在反应原生导航时做错了?也许我不能像 React Native 那样拥有这个标签栏和堆栈导航器样式?无论如何,我相信插图应该返回一个值,所以我认为这就是问题所在。

请注意,如果我将<SafeAreaView> 块移动到<TouchableWithoutFeedback> 周围,而不是围绕<NavigationContainer> 块,并删除添加到keyboardVerticalOffset 的50 个额外像素,那么键盘会正确向上推动输入字段,但 iPhone 11 上的标签栏图标被压扁。在我写这篇文章时,我注意到这个变化现在让底部/顶部填充变量返回值?如果我随后删除 tabBarOptions,我会看到可以工作的选项卡的基本外观,但我更喜欢我最初的选项卡的设计。

如何保持当前标签栏的样式并让键盘避开每台设备上的聊天输入字段?

(注意:下面的useEffect 用法是我尝试使用本期中概述的解决方案:https://github.com/th3rdwave/react-native-safe-area-context/issues/54)

App.js:

import React,  Component, useEffect, useState  from 'react';
import  View, KeyboardAvoidingView, TextInput, Text, Platform, TouchableWithoutFeedback, Keyboard, ActivityIndicator, SafeAreaView, ScrollView, Button, StatusBar  from 'react-native';
import  NavigationContainer  from '@react-navigation/native';
import  createBottomTabNavigator  from '@react-navigation/bottom-tabs';
import  createStackNavigator, useHeaderHeight  from '@react-navigation/stack';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import  useSafeAreaInsets, useSafeAreaFrame  from 'react-native-safe-area-context';
import  Dimensions  from 'react-native';

const Stack = createStackNavigator();

function TicketStack() 
  return (
    <Stack.Navigator
    screenOptions=
      headerStyle:  
        backgroundColor: "dodgerblue",
        elevation: 0, // remove shadow on android
        shadowOpacity: 0, // remove shadow on ios
      ,
      headerTintColor: "#fff",
      headerTitleStyle: 
        fontWeight: "900",
        fontSize: 26,
      ,

    >
      <Stack.Screen name="Ticket">
        (props) => <TicketScreen ...props />
      </Stack.Screen>
      <Stack.Screen name="Chat">
        (props) => <ChatScreen ...props />
      </Stack.Screen>
    </Stack.Navigator>
  );


function HomeScreen() 
  return (
    <View style= flex: 1, justifyContent: 'center', alignItems: 'center' >
      <Text><FontAwesome5 name="home" size=20 color="dodgerblue" /> Home screen!</Text>
    </View>
  );


function TicketScreen(props)
  return (
    <View style= flex: 1, justifyContent: 'center', alignItems: 'center' >
      <Text>Ticket screen :)!</Text>
      <Button title="Go to Chat" onPress=() => props.navigation.navigate('Chat') />
    </View>
  );


function CustomKeyboardAvoidingView( children, style ) 
    const headerHeight = useHeaderHeight();
    console.log("headerHeight: " + headerHeight)
    console.log("StatusBar.currentHeight: " + StatusBar.currentHeight)

    const insets = useSafeAreaInsets();
    console.log("insets.top: " + topPadding)
    console.log("insets.bottom: " + bottomPadding)

    const [bottomPadding, setBottomPadding] = useState(insets.bottom)
    const [topPadding, setTopPadding] = useState(insets.top)

    useEffect(() => 
      setBottomPadding(insets.bottom)
      setTopPadding(insets.top)

      console.log("topPadding: " + topPadding)
      console.log("bottomPadding: " + bottomPadding)
    , [insets.bottom, insets.top])

    // const frame = useSafeAreaFrame();
    // const windowHeight = Dimensions.get('window').height  
    // console.log("frame.height: " + frame.height)
    // console.log("windowHeight: " + windowHeight)
    // const safeAreaHeight = windowHeight - frame.height
    // console.log("safeAreaHeight: " + safeAreaHeight)
    // safeAreaHeight is too much, needs to just be bottom or top padding from safearea
    return (
        <KeyboardAvoidingView
            style=style
            behavior=Platform.OS == "ios" ? "padding" : "height"
            keyboardVerticalOffset=headerHeight + 50
        >
            children
        </KeyboardAvoidingView>
    );


function ChatScreen()
  return(
      <TouchableWithoutFeedback onPress=Keyboard.dismiss>
        <CustomKeyboardAvoidingView style=backgroundColor: "#fff", flex: 1, flexDirection: "column", justifyContent: "space-between" >
          <View style=backgroundColor: "dodgerblue", paddingVertical: 15>
              <View style= margin: 10, marginBottom: 15>
                  <ActivityIndicator size="large" style=marginBottom: 10/>
                  <Text>Waiting for more info here....</Text>
              </View>
          </View>

          <ScrollView style=backgroundColor: "tomato", paddingVertical: 15>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
          </ScrollView>
          
          <View style=backgroundColor: "yellow", paddingVertical: 15>
              <TextInput placeholder="Type your message here..." />
          </View>
        </CustomKeyboardAvoidingView>
      </TouchableWithoutFeedback>
  )


const Tab = createBottomTabNavigator();

// TODO:
// - removing safeareaview makes tabs squished and icon nearly invisible, but chat message input fields avoids keyboard properly (squished can be fixed by removing tabBarOptions)
// - having safeareaview makes tabs look good, but chat message input field is hidden by keyboard
// - safeareainsets? why are they 0? i would be adding the bottom or top padding of insets to the vertical offset of the keyboard avoiding view.
export default class App extends Component 
  render()
    return (
      <SafeAreaView style=flex: 1, backgroundColor: "dodgerblue">
        <NavigationContainer>
          <Tab.Navigator
            screenOptions=( route ) => (
              tabBarIcon: ( color, size ) => 
                let iconName;

                if (route.name === 'Home') 
                  iconName = 'home';
                 else if (route.name === 'Ticket') 
                  iconName = 'question';
                
                return <FontAwesome5 name=iconName size=size color=color />;
              ,
            )
            tabBarOptions=
              style: 
                height: 70
              , 
              activeTintColor: "#fff",
              inactiveTintColor: "dodgerblue",
              inactiveBackgroundColor: "#fff",
              activeBackgroundColor: "dodgerblue",                               
              tabStyle: 
                paddingTop: 10,
                paddingBottom: 10
              ,
              labelStyle: 
                fontSize: 14
              ,
            >
            <Tab.Screen name="Home">
              (props) => <HomeScreen ...props />
            </Tab.Screen>
            <Tab.Screen name="Ticket">
              (props) => <TicketStack ...props />
            </Tab.Screen>
          </Tab.Navigator>
        </NavigationContainer>
      </SafeAreaView>
    );
  

截图:

package.json:


  "name": "ReactNativeTest",
  "version": "0.0.1",
  "private": true,
  "scripts": 
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  ,
  "dependencies": 
    "@react-native-community/masked-view": "^0.1.10",
    "@react-navigation/bottom-tabs": "^5.9.2",
    "@react-navigation/native": "^5.7.6",
    "@react-navigation/stack": "^5.9.3",
    "react": "16.13.1",
    "react-native": "0.63.3",
    "react-native-gesture-handler": "^1.8.0",
    "react-native-reanimated": "^1.13.1",
    "react-native-safe-area-context": "^3.1.8",
    "react-native-screens": "^2.11.0",
    "react-native-vector-icons": "^7.1.0"
  ,
  "devDependencies": 
    "@babel/core": "7.11.6",
    "@babel/runtime": "7.11.2",
    "@react-native-community/eslint-config": "1.1.0",
    "babel-jest": "25.5.1",
    "eslint": "6.8.0",
    "jest": "25.5.4",
    "metro-react-native-babel-preset": "0.59.0",
    "react-test-renderer": "16.13.1"
  ,
  "jest": 
    "preset": "react-native"
  

【问题讨论】:

【参考方案1】:

正如 Alex 在 Github issue 中指出的那样,将应用程序包装在 SafeAreaProvider 中可以修复插入不正确返回值的问题。

标签栏仍然被SafeAreaView 包裹NavigationContainer 压扁,我通过将高度70 添加到tabStyleTab.Navigator 属性来解决这个问题。然后出现了另一个问题,TicketScreen 的 Stack Header 太大,并且通过将 headerStatusBarHeight: 0 添加到 Stack.Navigator screenOptions 属性从 Github issue 修复。插图仅在 useEffect 内正确返回并使用 setState 使用,然后我使用 topPadding 值添加到 keyboardVerticalOffset 道具并且键盘弹出而不隐藏 iPhone 11 上的输入字段和iPhone 6s!

更新说明:确保还将keyboardHidesTabBar: (Platform.OS == "ios" ? false : true) 设置为Tab.Navigator 上的道具。

这是完整的工作代码:

import React,  Component, useEffect, useState  from 'react';
import  View, KeyboardAvoidingView, TextInput, Text, Platform, TouchableWithoutFeedback, Keyboard, ActivityIndicator, SafeAreaView, ScrollView, Button, StatusBar  from 'react-native';
import  NavigationContainer  from '@react-navigation/native';
import  createBottomTabNavigator  from '@react-navigation/bottom-tabs';
import  createStackNavigator, useHeaderHeight  from '@react-navigation/stack';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import  useSafeAreaInsets, SafeAreaProvider  from 'react-native-safe-area-context';

const Stack = createStackNavigator();

function TicketStack() 
  return (
    <Stack.Navigator
      screenOptions=
        headerStatusBarHeight: 0, // Header had increased size with SafeArea for some reason (https://github.com/react-navigation/react-navigation/issues/5936)
        headerStyle:  
          backgroundColor: "dodgerblue",
          elevation: 0, // remove shadow on Android
          shadowOpacity: 0, // remove shadow on iOS
        ,
        headerTintColor: "#fff",
        headerTitleStyle: 
          fontWeight: "900",
          fontSize: 26,
        ,
      >
      <Stack.Screen name="Ticket">
        (props) => <TicketScreen ...props />
      </Stack.Screen>
      <Stack.Screen name="Chat">
        (props) => <ChatScreen ...props />
      </Stack.Screen>
    </Stack.Navigator>
  );


function HomeScreen() 
  return (
    <View style= flex: 1, alignItems: 'center' >
      <Text><FontAwesome5 name="home" size=20 color="dodgerblue" />Home screen</Text>
    </View>
  );


function TicketScreen(props)
  return (
    <View style= flex: 1, justifyContent: 'center', alignItems: 'center' >
      <Text>Ticket screen</Text>
      <Button title="Go to Chat" onPress=() => props.navigation.navigate('Chat') />
    </View>
  );


function CustomKeyboardAvoidingView( children, style ) 
    const headerHeight = useHeaderHeight();
    console.log("headerHeight: " + headerHeight)
    console.log("StatusBar.currentHeight: " + StatusBar.currentHeight)

    const insets = useSafeAreaInsets();
    const [bottomPadding, setBottomPadding] = useState(insets.bottom)
    const [topPadding, setTopPadding] = useState(insets.top)

    useEffect(() => 
      // This useEffect is needed because insets are undefined at first for some reason
      // https://github.com/th3rdwave/react-native-safe-area-context/issues/54
      setBottomPadding(insets.bottom)
      setTopPadding(insets.top)

      console.log("topPadding: " + topPadding)
      console.log("bottomPadding: " + bottomPadding)
    , [insets.bottom, insets.top])

    return (
        <KeyboardAvoidingView
            style=style
            behavior=Platform.OS == "ios" ? "padding" : "height"
            keyboardVerticalOffset=headerHeight + topPadding + StatusBar.currentHeight
        >
            children
        </KeyboardAvoidingView>
    );


function ChatScreen()
  return(
      <TouchableWithoutFeedback onPress=Keyboard.dismiss>
        <CustomKeyboardAvoidingView style=backgroundColor: "#fff", flex: 1, flexDirection: "column", justifyContent: "space-between" >
          <View style=backgroundColor: "dodgerblue", paddingVertical: 15>
              <View style= margin: 10, marginBottom: 15>
                  <ActivityIndicator size="large" style=marginBottom: 10/>
                  <Text>Waiting for more info here....</Text>
              </View>
          </View>

          <ScrollView style=backgroundColor: "tomato", paddingVertical: 15>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
              <Text>Chat messages</Text>
          </ScrollView>
          
          <View style=backgroundColor: "yellow", paddingVertical: 15>
              <TextInput placeholder="Type your message here..." />
          </View>
        </CustomKeyboardAvoidingView>
      </TouchableWithoutFeedback>
  )


const Tab = createBottomTabNavigator();

export default class App extends Component 
  render()
    return (
      <SafeAreaProvider>
        <SafeAreaView style=flex: 1, backgroundColor: "dodgerblue">
          <NavigationContainer>
            <Tab.Navigator
              screenOptions=( route ) => (
                tabBarIcon: ( color, size ) => 
                  let iconName;

                  if (route.name === 'Home') 
                    iconName = 'home';
                   else if (route.name === 'Ticket') 
                    iconName = 'question';
                  
                  return <FontAwesome5 name=iconName size=size color=color />;
                ,
              )
              tabBarOptions=
                style: 
                  height: 70
                , 
                activeTintColor: "#fff",
                inactiveTintColor: "dodgerblue",
                inactiveBackgroundColor: "#fff",
                activeBackgroundColor: "dodgerblue",                               
                tabStyle: 
                  paddingTop: 10,
                  paddingBottom: 10,
                  height: 70
                ,
                labelStyle: 
                  fontSize: 14
                ,
                keyboardHidesTabBar: (Platform.OS == "ios" ? false : true)
              >
              <Tab.Screen name="Home">
                (props) => <HomeScreen ...props />
              </Tab.Screen>
              <Tab.Screen name="Ticket">
                (props) => <TicketStack ...props />
              </Tab.Screen>
            </Tab.Navigator>
          </NavigationContainer>
        </SafeAreaView>
      </SafeAreaProvider>
    );
  


【讨论】:

【参考方案2】:

就像我在相应的GitHub issue 中发布的那样,将应用程序包装在&lt;SafeAreaProvider&gt; 中可以解决问题:

export default class App extends Component 
  render()
    return (
      <SafeAreaProvider>
        ...
      </SafeAreaProvider>
    );
  

【讨论】:

感谢您指出这一点,伙计!正如我在上面的答案中概述的那样,还有一些其他问题 @douglasrcjames 哦,我不阅读完整的问题太糟糕了!很高兴您解决了所有问题。 :)

以上是关于KeyboardAvoidingView + React Navigation + 安全区域视图 = 不工作的主要内容,如果未能解决你的问题,请参考以下文章

即使删除了行为,KeyboardAvoidingView 也无法在 Android 上运行

使用KeyboardAvoidingView时,react-navigation标题后面的内容

React Native KeyboardAvoidingView 无法正常工作

KeyboardAvoidingView 在本机反应中使用过多的填充来推高内容

在 react native 中使用 KeyboardAvoidingView 时,整个内容都被向上推

无法弄清楚如何正确使用 KeyboardAvoidingView