组件概述
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',
},
});