【React Native】KeyboardLayout 集成输入框、功能面板和键盘管理的布局容器

组件概述

KeyboardLayout 是一个集成输入框、功能面板和键盘管理的布局容器,适用于聊天、评论等需要动态输入交互的场景。支持键盘与功能面板的切换、消息发送及自定义功能项。

核心功能

  • 输入消息:内置输入框,支持文本输入与发送。

  • 功能面板:点击“+”按钮展开功能面板(如表情、图片),支持自定义功能项。

  • 键盘自适应:自动监听键盘高度,键盘弹出时隐藏功能面板。

  • 动态切换:支持键盘与功能面板的互斥切换(展开面板时隐藏键盘)。

效果图

键盘输入

Props 属性说明

属性名 类型 必填 默认值 描述

  • children React.ReactNode 是 无 布局主体内容(如消息列表)。
  • style StyleProp 否 无 根容器自定义样式。
  • options FunOption[] 否 [] 功能项数组,定义功能面板中的图标、名称和类型。
  • sendMessage (message: string) => void 否 无 消息发送回调,输入框内容非空时触发。
  • onItemPress (item: FunOption) => void 否 无 功能项点击回调,返回被点击的功能项数据。

FunOption 结构:

interface FunOption {
  icon: ImageSourcePropType; // 图标资源(本地或网络)
  name: string;              // 功能名称(如“表情”)
  type?: string | number;   // 可选类型标识(用于区分不同功能)

使用示例

使用前需要改变键盘显示模式,让键盘覆盖在页面的上面

例如:
鸿蒙

 Stack(){
   RNSurface()
 }
 .expandSafeArea([SafeAreaType.KEYBOARD])
import KeyboardLayout from './KeyboardLayout';
import { Image } from 'react-native';

const App = () => {
  // 功能项配置
  const options: FunOption[] = [
    {
      icon: {uri: 'https://reactnative.dev/img/tiny_logo.png'},
      name: '功能 1',
      type: 'type1',
    },
    {
      icon: {uri: 'https://reactnative.dev/img/tiny_logo.png'},
      name: '功能 2',
      type: 'type2',
    },
  ];

  // 消息发送回调
  const handleSend = (message: string) => {
    console.log('发送消息:', message);
  };

  // 功能项点击回调
  const handleItemPress = (item: FunOption) => {
    console.log('点击功能:', item.name);
  };

  return (
    <KeyboardLayout
      options={options}
      sendMessage={handleSend}
      onItemPress={handleItemPress}
      style={{ backgroundColor: '#f5f5f5' }}>
{/ 消息列表或其他内容 /}

      <FlatList data={messages} renderItem={...} />
    </KeyboardLayout>
  );
};

源码

import React, {useCallback, useEffect, useRef, useState} from 'react';
import {
  FlatList,
  Image,
  Keyboard,
  StyleProp,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
  ViewStyle,
} from 'react-native';
import {ImageSourcePropType} from 'react-native/Libraries/Image/Image';

interface FunOption {
  icon: ImageSourcePropType;
  name: string;
  type?: string | number;
}

type KeyboardLayoutProps = {
  children: React.ReactNode;
  style?: StyleProp<ViewStyle>;
  options?: FunOption[];
  sendMessage?: (message: string) => void;
  onItemPress?: (name: FunOption) => void;
};

let KeyboardLayoutHeight = 0;

const KeyboardLayout: React.FC<KeyboardLayoutProps> = ({
  children,
  style,
  options,
  sendMessage,
  onItemPress,
}) => {
  const [msg, setMsg] = useState('');
  const [keyboardHeight, setKeyboardHeight] = useState<number | undefined>(
    undefined,
  );
  const [isShowFunc, setIsShowFunc] = useState<boolean>(false);
  const [isShowKeyBoard, setIsShowKeyBoard] = useState<boolean>(false);
  const inputRef = useRef<TextInput>(null);

  useEffect(() => {
    if (KeyboardLayoutHeight > 0) {
      setKeyboardHeight(KeyboardLayoutHeight);
    }
    Keyboard.addListener('keyboardDidShow', e => {
      const value = e.endCoordinates.height;
      KeyboardLayoutHeight = value;
      setKeyboardHeight(value);
      setTimeout(() => {
        setIsShowFunc(false);
      }, 50);
      setIsShowKeyBoard(true);
    });
    Keyboard.addListener('keyboardDidHide', e => {
      setIsShowKeyBoard(false);
    });

    return () => {
      Keyboard.removeAllListeners('keyboardDidShow');
      Keyboard.removeAllListeners('keyboardDidHide');
    };
  }, []);

  const showFun = () => {
    if (isShowKeyBoard) {
      Keyboard.dismiss();
    } else if (isShowFunc) {
      inputRef.current?.focus();
    }
    setIsShowFunc(true);
  };

  const hideFun = () => {
    Keyboard.dismiss();
    setIsShowFunc(false);
  };

  const sendMsg = () => {
    sendMessage?.(msg);
    setMsg('');
  };

  const renderItem = useCallback(
    ({item}: {item: FunOption}) => {
      return (
        <TouchableOpacity
          style={styles.funcContainer}
          onPress={() => onItemPress?.(item)}>
          <Image source={item.icon} style={styles.funcIcon} />
          <Text style={styles.funcText}>{item.name}</Text>
        </TouchableOpacity>
      );
    },
    [onItemPress],
  );

  return (
    <TouchableOpacity
      activeOpacity={1}
      onPress={() => {
        hideFun();
      }}
      style={[styles.container, style]}>
      <View style={styles.child}>{children}</View>

      {/* 输入框 */}
      <View style={styles.inputContainer}>
        <TextInput
          ref={inputRef}
          style={styles.input}
          value={msg}
          onChangeText={setMsg}
          placeholder="请输入内容"
        />
        {(options?.length ?? 0) > 0 && (
          <TouchableOpacity
            style={styles.moreFunBtn}
            onPress={() => {
              showFun();
            }}>
            <Image
              source={{
                uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg==',
              }}
              style={styles.moreFun}
            />
          </TouchableOpacity>
        )}
        {msg.trim() && (
          <TouchableOpacity style={styles.sendButton} onPress={sendMsg}>
            <Text style={styles.sendText}>发送</Text>
          </TouchableOpacity>
        )}
      </View>
      {(isShowKeyBoard || isShowFunc) && (
        <View style={{height: keyboardHeight}}>
          {isShowFunc && (
            <View style={styles.funcContainerOut}>
              <FlatList
                data={options}
                renderItem={renderItem}
                numColumns={4}
                keyExtractor={(item, index) => index.toString()}
                contentContainerStyle={styles.funcContent}
              />
            </View>
          )}
        </View>
      )}
    </TouchableOpacity>
  );
};

export default KeyboardLayout;

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  child: {
    flex: 1,
  },
  inputContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 10,
    paddingVertical: 10,
    backgroundColor: 'white',
  },
  input: {
    flex: 1,
    height: 30,
    borderWidth: 0.5,
    borderColor: 'gray',
    borderRadius: 5,
    backgroundColor: 'white',
    fontSize: 15,
    paddingLeft: 10,
  },
  moreFunBtn: {
    borderRadius: 100,
    borderWidth: 1.5,
    width: 25,
    height: 25,
    justifyContent: 'center',
    alignItems: 'center',
    marginLeft: 10,
  },
  moreFun: {
    width: 14,
    height: 14,
  },
  sendButton: {
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'green',
    paddingHorizontal: 10,
    height: 30,
    borderRadius: 5,
    marginLeft: 10,
  },
  sendText: {
    color: '#FFFFFF',
  },

  funcContainerOut: {
    height: 280,
    backgroundColor: '#f0f0f0',
    paddingHorizontal: 10,
  },
  funcContent: {
    flexGrow: 1,
  },
  funcContainer: {
    width: '25%',
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 10,
  },
  funcIcon: {
    width: 50,
    height: 50,
    marginBottom: 10,
  },
  funcText: {
    fontSize: 13,
    color: 'black',
  },
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值