在 React Native 鸿蒙化项目里,键盘处理是评论输入框、聊天输入框、搜索页和表单页都会遇到的高频问题。原文基于 React Native 0.84 + RNOH 0.84.1,在 HarmonyOS 6.0 设备上测试。核心结论很明确:鸿蒙侧 Keyboard 的 will 系列事件不触发,只有 did 系列及 keyboardDidChangeFrame 可用;keyboardDidShow 里的键盘高度可能不准;scheduleLayoutAnimation 不生效;Keyboard.isVisible() 存在延迟。下面按 API 能力、监听器管理、典型场景、鸿蒙坑位和排障建议重组。
一、Keyboard API 能做什么
Keyboard 的职责不多:监听键盘弹出和收起、获取键盘尺寸、主动收起键盘。原文提到 React Native 的 Keyboard 支持 6 种事件,但鸿蒙上和 Android 类似,will 系列无法使用。可以稳定工作的事件是 keyboardDidShow、keyboardDidHide 和 keyboardDidChangeFrame。事件回调参数为 KeyboardEvent,包含 duration、easing、startCoordinates、endCoordinates。endCoordinates 中有 height、width、screenX、screenY,聊天输入框计算偏移量时通常取 endCoordinates.height。
- import { Keyboard } from 'react-native';
- useEffect(() => {
- const showSub = Keyboard.addListener('keyboardDidShow', (e) => {
- console.log('键盘已显示,高度:', e.endCoordinates.height);
- });
- const hideSub = Keyboard.addListener('keyboardDidHide', () => {
- console.log('键盘已隐藏');
- });
- return () => {
- showSub.remove();
- hideSub.remove();
- };
- }, []);
复制代码
Keyboard.dismiss 是全局方法,调用后所有 TextInput 都会失去焦点,键盘收起。Keyboard.isVisible 用来检查键盘是否弹出,Keyboard.metrics 用来获取键盘尺寸。但在鸿蒙上 isVisible 有延迟,不建议在 keyboardDidShow 回调里立刻依赖它。
- Keyboard.addListener('keyboardDidShow', (e) => {
- const { height, width, screenX, screenY } = e.endCoordinates;
- console.log('键盘高度:', height, '宽度:', width);
- });
- const visible = Keyboard.isVisible();
- const metrics = Keyboard.metrics();
- if (metrics) {
- console.log('键盘高度:', metrics.height);
- console.log('键盘 Y:', metrics.screenY);
- }
复制代码
二、监听器注册与清理
原文里反复强调一个反面教材:不在 useEffect 中注册监听器,而是在每次渲染时都 Keyboard.addListener,旧的监听器又没有清理,最终造成内存泄漏。正确做法是在 useEffect 中注册,并在返回函数里调用 sub.remove()。
- useEffect(() => {
- const sub = Keyboard.addListener('keyboardDidShow', () => {
- setStatus('显示');
- });
- return () => sub.remove();
- }, []);
复制代码
三、聊天输入框:用 keyboardDidShow 高度撑起输入区
聊天输入框的常见做法是监听 keyboardDidShow 获取键盘高度,把输入区域的 paddingBottom 设为该高度,让输入框浮在键盘上方。发送消息后调用 Keyboard.dismiss 收起键盘。输入框高度可以随内容自适应,范围从 40px 到 120px。
- const ChatInput = () => {
- const [inputHeight, setInputHeight] = useState(40);
- const [keyboardHeight, setKeyboardHeight] = useState(0);
- const inputRef = useRef(null);
- useEffect(() => {
- const show = Keyboard.addListener('keyboardDidShow', (e) => {
- setKeyboardHeight(e.endCoordinates.height);
- });
- const hide = Keyboard.addListener('keyboardDidHide', () => {
- setKeyboardHeight(0);
- });
- return () => {
- show.remove();
- hide.remove();
- };
- }, []);
- const handleSend = () => {
- sendMessage();
- Keyboard.dismiss();
- };
- return (
- <View style={{ paddingBottom: keyboardHeight, backgroundColor: '#fff' }}>
- <View style={{ flexDirection: 'row', alignItems: 'flex-end' }}>
- <TextInput
- ref={inputRef}
- placeholder='输入消息...'
- multiline
- style={{
- flex: 1,
- height: Math.max(40, Math.min(120, inputHeight)),
- borderWidth: 1,
- borderRadius: 20,
- paddingHorizontal: 16,
- }}
- onContentSizeChange={(e) =>
- setInputHeight(e.nativeEvent.contentSize.height)
- }
- />
- <Pressable onPress={handleSend}>
- <Text>发送</Text>
- </Pressable>
- </View>
- </View>
- );
- };
复制代码
四、搜索页自动聚焦与表单页跳转
搜索页的典型模式是页面加载后延迟 300ms 自动聚焦输入框,让键盘弹出;同时监听 keyboardDidShow,一旦键盘弹出就隐藏搜索历史。表单页则可以用 returnKeyType 和 onSubmitEditing 在多个输入框之间跳转,最后一个字段用 Keyboard.dismiss 收起键盘。ScrollView 需要设置 keyboardShouldPersistTaps='handled',并把 contentContainerStyle 的 paddingBottom 与 keyboardOffset 联动。
- useEffect(() => {
- const timer = setTimeout(() => {
- inputRef.current?.focus();
- }, 300);
- return () => clearTimeout(timer);
- }, []);
- useEffect(() => {
- const sub = Keyboard.addListener('keyboardDidShow', () => {
- setShowHistory(false);
- });
- return () => sub.remove();
- }, []);
复制代码- const focusNext = (currentField) => {
- const fieldOrder = ['name', 'phone', 'email', 'address'];
- const currentIndex = fieldOrder.indexOf(currentField);
- if (currentIndex < fieldOrder.length - 1) {
- const nextField = fieldOrder[currentIndex + 1];
- inputs.current[nextField]?.focus();
- } else {
- Keyboard.dismiss();
- }
- };
复制代码
五、KeyboardAvoidingView 与底部工具栏
KeyboardAvoidingView 可以和 Keyboard API 搭配使用。KeyboardAvoidingView 负责自动偏移,Keyboard 提供额外状态控制。原文示例中,鸿蒙上的 behavior 同样使用 padding,keyboardVerticalOffset 对 harmony 设为 0。底部工具栏则处理键盘、表情面板、语音面板之间的切换:切换到表情或语音时先 Keyboard.dismiss,避免两个面板同时出现;点击同一个按钮时,根据 keyboardVisible 决定收起键盘还是重新聚焦输入框。
- <KeyboardAvoidingView
- behavior={Platform.OS === 'harmony' ? 'padding' : 'padding'}
- style={{ flex: 1 }}
- keyboardVerticalOffset={Platform.select({
- ios: 88,
- android: 0,
- harmony: 0,
- })}
- >
- ...
- </KeyboardAvoidingView>
复制代码- const switchTool = (mode) => {
- if (mode === toolMode) {
- if (keyboardVisible) {
- Keyboard.dismiss();
- } else {
- inputRef.current?.focus();
- }
- } else {
- setToolMode(mode);
- Keyboard.dismiss();
- }
- };
复制代码
六、鸿蒙上的四个坑
坑 1:will 系列事件不支持。鸿蒙跟 Android 一样,keyboardWillShow、keyboardWillHide、keyboardWillChangeFrame 不触发,只有 keyboardDidShow、keyboardDidHide、keyboardDidChangeFrame 可用。
- Keyboard.addListener('keyboardDidShow', handler);
- Keyboard.addListener('keyboardDidHide', handler);
- Keyboard.addListener('keyboardDidChangeFrame', handler);
- Keyboard.addListener('keyboardWillShow', handler);
- Keyboard.addListener('keyboardWillHide', handler);
- Keyboard.addListener('keyboardWillChangeFrame', handler);
复制代码
坑 2:键盘高度获取时机。鸿蒙上 keyboardDidShow 事件中的 endCoordinates.height 可能不准确,跟实际键盘高度有偏差。原文建议做兜底:如果高度小于 100 或大于 1000,就用 400 作为默认键盘高度。
- Keyboard.addListener('keyboardDidShow', (e) => {
- let height = e.endCoordinates.height;
- if (height < 100 || height > 1000) {
- height = 400;
- }
- setKeyboardHeight(height);
- });
复制代码
坑 3:scheduleLayoutAnimation 不生效。鸿蒙上该方法可能不生效,因为底层依赖 iOS 的键盘通知机制。原文建议直接用状态控制偏移量。
- Keyboard.addListener('keyboardDidShow', (e) => {
- setKeyboardHeight(e.endCoordinates.height);
- setShowToolbar(true);
- });
复制代码
坑 4:isVisible 延迟。鸿蒙上 Keyboard.isVisible() 在 keyboardDidShow 回调执行后可能仍返回 false,因为底层状态还没更新。不要在鸿蒙上依赖 isVisible 做实时判断,建议用状态变量跟踪键盘状态。
- Keyboard.addListener('keyboardDidShow', () => {
- const visible = Keyboard.isVisible();
- setKeyboardOpen(true);
- });
复制代码
七、方法速查与适配建议
鸿蒙上用 keyboardDidShow 和 keyboardDidHide,别依赖 will 系列;监听事件要在 useEffect 中注册并清理,防止内存泄漏;用一个状态变量跟踪键盘状态,比 isVisible 更可靠;键盘高度做兜底,鸿蒙上获取到的高度可能异常;配合 KeyboardAvoidingView 使用,大多数场景下够用;scheduleLayoutAnimation 在鸿蒙不生效时,手动控制偏移量。原文还给出了键盘事件统计思路,可以用 useRef 记录 showCount、hideCount、lastShowTime、totalVisibleDuration,用于埋点分析用户输入习惯。
本文基于 React Native 0.84 + RNOH 0.84.1 编写,鸿蒙设备为 HarmonyOS 6.0。不同版本之间可能存在差异,以实际测试结果为准。原文中的代码示例均已在鸿蒙设备上测试通过,可直接参考。 |