Expo React Native 鍵盤工具欄成功實現方案

核心問題

在 React Native 中為 TextInput 添加自訂鍵盤工具欄,支援文字插入到游標位置,且按鈕點擊時不關閉鍵盤。

關鍵技術要點

1. 文字插入到 Cursor (游標)位置的核心實現

const insertTextAtCursor = (insertString) => {
  const { start, end } = selection;
  const beforeText = text.substring(0, start);
  const afterText = text.substring(end);
  const newText = beforeText + insertString + afterText;
  const newCursorPosition = start + insertString.length;

  // 更新文字
  setText(newText);

  // 更新選擇位置
  const newSelection = { start: newCursorPosition, end: newCursorPosition };
  setSelection(newSelection);

  // 立即重新聚焦,防止鍵盤關閉
  if (textInputRef.current) {
    textInputRef.current.focus();

    // 延遲設置光標位置
    setTimeout(() => {
      if (textInputRef.current) {
        textInputRef.current.setNativeProps({
          selection: newSelection
        });
        // 再次確保焦點
        textInputRef.current.focus();
      }
    }, 50);
  }
};

2. 狀態管理

const [selection, setSelection] = useState({ start: 0, end: 0 });
const textInputRef = useRef(null);

// 處理選擇變化
const handleSelectionChange = (event) => {
  setSelection(event.nativeEvent.selection);
};

3. TextInput 配置

<TextInput
  ref={textInputRef}
  onSelectionChange={handleSelectionChange}
  selection={selection}
  // 其他屬性...
/>

4. 防止鍵盤關閉的關鍵設定

ScrollView 配置

<ScrollView
  horizontal
  keyboardShouldPersistTaps="always"  // 關鍵:允許點擊時保持鍵盤
  keyboardDismissMode="none"          // 關鍵:禁止滑動關閉鍵盤
>

按鈕事件處理

<TouchableOpacity
  onPress={() => {
    insertTextAtCursor(button.text);
  }}
  onPressIn={() => {
    // 按下時立即聚焦,防止失焦
    if (textInputRef.current) {
      textInputRef.current.focus();
    }
  }}
  delayPressIn={0}     // 減少延遲
  delayPressOut={0}    // 減少延遲
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}  // 增大點擊區域
>

5. 鍵盤監聽和工具欄定位

// 監聽鍵盤事件
useEffect(() => {
  const keyboardDidShow = Keyboard.addListener('keyboardDidShow', (event) => {
    setKeyboardHeight(event.endCoordinates.height);
    setKeyboardVisible(true);
  });

  const keyboardDidHide = Keyboard.addListener('keyboardDidHide', () => {
    setKeyboardVisible(false);
    setKeyboardHeight(0);
  });

  return () => {
    keyboardDidShow?.remove();
    keyboardDidHide?.remove();
  };
}, []);

// 工具欄定位
<View 
  style={[
    styles.toolbar,
    { 
      bottom: Platform.OS === 'ios' ? keyboardHeight - 34 : keyboardHeight,
      marginBottom: 35  // 確保不被遮擋
    }
  ]}
  pointerEvents="box-none"  // 允許子元素接收觸摸事件
>

失敗的方案

❌ react-native-keyboard-controller

  • 問題:與 Expo 相容性問題,出現 "doesn't seem to be linked" 錯誤
  • 原因:需要原生鏈接,Expo 環境難以配置

❌ InputAccessoryView

  • 問題:官方文檔明確指出不支援 multiline TextInput
  • 限制:只適用於單行輸入

❌ 簡單的 onPress 處理

  • 問題:點擊按鈕會導致 TextInput 失焦,鍵盤關閉
  • 原因:沒有正確處理焦點管理和事件傳播

成功關鍵因素

  1. 雙重焦點保證:onPressIn 和 onPress 都確保 TextInput 保持焦點
  2. 延遲設置光標:使用 setTimeout 確保 setNativeProps 生效
  3. 正確的 ScrollView 配置:keyboardShouldPersistTaps 和 keyboardDismissMode
  4. 精確的字串操作:使用 substring 而非正規表達式進行文字插入
  5. 適當的工具欄定位:考慮平台差異和安全區域

測試驗證

✅ 點擊按鈕時鍵盤保持顯示
✅ 文字正確插入到游標位置
✅ 工具欄不被鍵盤遮擋
✅ 支援多行文字編輯
✅ 平台相容性 (iOS/Android)

總結

成功的關鍵在於理解 React Native 的焦點管理機制,正確配置 ScrollView 的鍵盤行為,以及使用適當的延遲策略來確保原生屬性設置生效。避免使用第三方函式庫,採用原生 API 組合實現更加穩定可靠。