【HarmonyOS】RN_of_HarmonyOS实战项目_TextInput表情符号输入

【HarmonyOS】RN of HarmonyOS实战项目:TextInput表情符号输入完整实现


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

项目概述:本文详细介绍在HarmonyOS平台上使用React Native实现支持Emoji表情符号输入的TextInput组件,解决UTF-16编码导致的各种问题,提供完整的生产级解决方案。


欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

一、项目背景

在现代移动应用中,表情符号已成为用户交互不可或缺的一部分。但在HarmonyOS平台上实现正确的Emoji输入面临着诸多技术挑战:

  • 字符编码问题 :Emoji通常占用两个UTF-16代码单元(代理对),导致length计算错误
  • maxLength失效:原生属性基于UTF-16代码单元计数,而非视觉字符数
  • 光标位置偏移:在代理对边界处可能出现光标跳跃
  • 格式化截断:截断时可能破坏Emoji编码结构,导致显示为乱码

二、技术原理

2.1 UTF-16编码与Emoji代理对

JavaScript字符串基于UTF-16编码,每个"字符"实际上是UTF-16代码单元。普通字符占用1个单元,而Emoji等特殊字符需要2个单元(代理对):

复制代码
"👋" = "\uD83D\uDC4B" (2个UTF-16单元)

这导致:

javascript 复制代码
"👋".length === 2  // 而非 1

2.2 解决方案概述

核心思路:使用Array.from()或扩展运算符正确分割字符串,识别真正的Unicode字符。

流程图


用户输入Emoji
使用Array.from分割
获取真实字符数组
计算字符数量
数量 < 上限?
更新State Value
截断并提示
重新渲染TextInput

三、核心实现代码

3.1 Emoji输入组件实现

typescript 复制代码
/**
 * Emoji支持的TextInput组件
 * 解决UTF-16编码导致的length计算错误问题
 *
 * @platform HarmonyOS 2.0+
 * @react-native 0.72+
 */
import React, { useState, useCallback, useMemo } from 'react';
import {
  View,
  TextInput,
  Text,
  StyleSheet,
  TouchableOpacity,
  Keyboard,
  Platform,
} from 'react-native';

interface EmojiInputProps {
  maxLength?: number;
  placeholder?: string;
  onTextChange?: (text: string, isValid: boolean) => void;
  onSubmit?: (text: string) => void;
  autoFocus?: boolean;
}

const EmojiInput: React.FC<EmojiInputProps> = ({
  maxLength = 100,
  placeholder = '请输入表情或文字...',
  onTextChange,
  onSubmit,
  autoFocus = false,
}) => {
  const [text, setText] = useState<string>('');
  const [visualCount, setVisualCount] = useState<number>(0);

  /**
   * Unicode字符计数函数
   * 使用Array.from正确识别代理对
   */
  const getUnicodeLength = useCallback((str: string): number => {
    // Array.from能正确识别代理对,将每个完整字符作为单独元素
    return Array.from(str).length;
  }, []);

  /**
   * 截断字符串到指定视觉长度
   * 保持Emoji编码完整性
   */
  const truncateToVisualLength = useCallback((str: string, maxLen: number): string => {
    const chars = Array.from(str);
    return chars.slice(0, maxLen).join('');
  }, []);

  /**
   * 处理文本变化
   */
  const handleTextChange = useCallback((inputText: string) => {
    const chars = Array.from(inputText);
    const currentLength = chars.length;

    if (currentLength <= maxLength) {
      setText(inputText);
      setVisualCount(currentLength);
      onTextChange?.(inputText, true);
    } else {
      // 截断并提示
      const truncated = truncateToVisualLength(inputText, maxLength);
      setText(truncated);
      setVisualCount(maxLength);
      onTextChange?.(truncated, false);

      // 触觉反馈(需要配置权限)
      if (Platform.OS === 'harmony') {
        // HarmonyOS震动反馈
        console.warn('已达到最大字符限制');
      }
    }
  }, [maxLength, onTextChange, truncateToVisualLength]);

  /**
   * 计算UTF-16代码单元数量(用于调试显示)
   */
  const utf16Length = useMemo(() => text.length, [text]);

  /**
   * 清空输入
   */
  const handleClear = useCallback(() => {
    setText('');
    setVisualCount(0);
    onTextChange?.('', true);
    inputRef.current?.focus();
  }, [onTextChange]);

  const inputRef = React.useRef<TextInput>(null);

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Emoji输入演示</Text>

      <View style={styles.inputWrapper}>
        <TextInput
          ref={inputRef}
          style={styles.input}
          value={text}
          onChangeText={handleTextChange}
          placeholder={placeholder}
          placeholderTextColor="#999999"
          autoFocus={autoFocus}
          multiline={false}
          keyboardType="default"
          autoCorrect={false}
          // 关键:不设置maxLength属性,完全由JS逻辑控制
        />

        <TouchableOpacity
          style={styles.clearButton}
          onPress={handleClear}
          hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
        >
          <Text style={styles.clearButtonText}>×</Text>
        </TouchableOpacity>
      </View>

      {/* 字符计数显示 */}
      <View style={styles.counterContainer}>
        <Text style={[
          styles.counterText,
          visualCount >= maxLength * 0.9 && styles.counterWarning,
        ]}>
          {visualCount} / {maxLength}
        </Text>
      </View>

      {/* 调试信息:显示UTF-16长度与Unicode长度的差异 */}
      <View style={styles.debugInfo}>
        <Text style={styles.debugText}>
          UTF-16长度: {utf16Length}
        </Text>
        <Text style={styles.debugText}>
          Unicode长度: {visualCount}
        </Text>
      </View>

      <TouchableOpacity
        style={styles.submitButton}
        onPress={() => {
          Keyboard.dismiss();
          onSubmit?.(text);
        }}
      >
        <Text style={styles.submitButtonText}>提交</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    backgroundColor: '#F1F3F5',
    justifyContent: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 30,
    textAlign: 'center',
    color: '#333',
  },
  inputWrapper: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#FFFFFF',
    borderRadius: 12,
    paddingHorizontal: 16,
    paddingVertical: 8,
    marginBottom: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  input: {
    flex: 1,
    fontSize: 18,
    color: '#333333',
    paddingVertical: 8,
  },
  clearButton: {
    width: 28,
    height: 28,
    borderRadius: 14,
    backgroundColor: '#E0E0E0',
    justifyContent: 'center',
    alignItems: 'center',
    marginLeft: 10,
  },
  clearButtonText: {
    fontSize: 18,
    color: '#495057',
    fontWeight: 'bold',
  },
  counterContainer: {
    alignItems: 'flex-end',
    marginBottom: 10,
  },
  counterText: {
    fontSize: 14,
    color: '#999999',
  },
  counterWarning: {
    color: '#FF3B30',
    fontWeight: 'bold',
  },
  submitButton: {
    backgroundColor: '#007AFF',
    paddingVertical: 14,
    borderRadius: 10,
    alignItems: 'center',
  },
  submitButtonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '600',
  },
  debugInfo: {
    marginTop: 40,
    padding: 15,
    backgroundColor: '#E8E8E8',
    borderRadius: 8,
  },
  debugText: {
    fontSize: 12,
    color: '#666',
    fontFamily: 'monospace',
    marginBottom: 4,
  },
});

export default EmojiInput;

3.2 字符计数工具Hook

typescript 复制代码
/**
 * Unicode字符计数Hook
 * 解决Emoji等代理对字符的计数问题
 */
import { useCallback, useMemo } from 'react';

interface UseUnicodeCountResult {
  unicodeLength: number;      // Unicode字符数
  utf16Length: number;        // UTF-16代码单元数
  difference: number;          // 差值
  hasSurrogatePairs: boolean;   // 是否包含代理对
}

export const useUnicodeCount = (text: string): UseUnicodeCountResult => {
  const result = useMemo(() => {
    const unicodeLength = Array.from(text).length;
    const utf16Length = text.length;

    return {
      unicodeLength,
      utf16Length,
      difference: utf16Length - unicodeLength,
      hasSurrogatePairs: utf16Length > unicodeLength,
    };
  }, [text]);

  return result;
};

// 使用示例
function UnicodeCountDemo() {
  const { hasSurrogatePairs, difference } = useUnicodeCount("Hello 👋 World");

  return (
    <Text>
      {hasSurrogatePairs ? "包含" : "不包含"}代理对,差值: {difference}
    </Text>
  );
}

3.3 智能截断工具函数

typescript 复制代码
/**
 * 字符串工具类 - 处理Unicode相关的字符串操作
 */
class UnicodeStringUtils {
  /**
   * 安全截取字符串到指定Unicode字符数
   * @param str 原始字符串
   * @param maxChars 最大Unicode字符数
   * @returns 截取后的字符串
   */
  static safeTruncate(str: string, maxChars: number): string {
    const chars = Array.from(str);

    if (chars.length <= maxChars) {
      return str;
    }

    return chars.slice(0, maxChars).join('');
  }

  /**
   * 获取字符串的Unicode字符数组
   * @param str 原始字符串
   * @returns Unicode字符数组
   */
  static toCharArray(str: string): string[] {
    return Array.from(str);
  }

  /**
   * 检查字符串是否包含代理对
   * @param str 要检查的字符串
   * @returns 是否包含代理对
   */
  static hasSurrogatePairs(str: string): boolean {
    return str.length !== Array.from(str).length;
  }

  /**
   * 获取字符串中的所有Emoji
   * @param str 原始字符串
   @returns Emoji数组
   */
  static extractEmojis(str: string): string[] {
    const emojiRegex = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE0E6}]/u;
    const emojis: string[] = [];
    let match;

    while ((match = emojiRegex.exec(str)) !== null) {
      emojis.push(match[0]);
    }

    return emojis;
  }

  /**
   * 统计字符串中的Emoji数量
   * @param str 原始字符串
   * @returns Emoji数量
   */
  static countEmojis(str: string): number {
    return this.extractEmojis(str).length;
  }
}

四、HarmonyOS平台特定优化

4.1 平台检测与适配

typescript 复制代码
import { Platform } from 'react-native';

/**
 * HarmonyOS平台工具类
 */
class HarmonyOSUtils {
  /**
   * 检测是否运行在HarmonyOS平台
   */
  static isHarmonyOS(): boolean {
    return Platform.OS === 'harmony';
  }

  /**
   * 获取平台特定的优化配置
   */
  static getOptimizedConfig() {
    return {
      textContentType: this.isHarmonyOS() ? undefined : 'none',
      autoComplete: this.isHarmonyOS() ? undefined : 'off',
      // HarmonyOS特有的优化配置
      selectionColor: this.isHarmonyOS() ? '#007AFF' : undefined,
    };
  }

  /**
   * 触觉反馈(需要权限)
   */
  static triggerHapticFeedback() {
    if (this.isHarmonyOS()) {
      // HarmonyOS震动反馈
      // 注意:需要在module.json5中申请ohos.permission.VIBRATE权限
      console.log('触发震动反馈');
    }
  }
}

4.2 配置文件(module.json5)

json5 复制代码
// harmony/entry/src/main/module.json5
{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": ["phone", "tablet"],
    "requestPermissions": [
      {
        "name": "ohos.permission.VIBRATE",
        "reason": "$string:vibrate_reason",
        "usedScene": {
          "abilities": [
            {
              "name": "EntryAbility",
              "skills": ["action.system.home"]
            }
          ]
        }
      }
    ]
  }
}

五、完整组件封装

typescript 复制代码
/**
 * 高级Emoji输入组件
 * 支持实时验证、字符计数、截断保护
 */
import React, { useState, useCallback } from 'react';
import { View, TextInput, Text, StyleSheet, TouchableOpacity } from 'react-native';

interface AdvancedEmojiInputProps {
  maxLength?: number;
  placeholder?: string;
  showDebugInfo?: boolean;
}

const AdvancedEmojiInput: React.FC<AdvancedEmojiInputProps> = ({
  maxLength = 50,
  placeholder,
  showDebugInfo = false,
}) => {
  const [text, setText] = useState('');

  const { unicodeLength, utf16Length, hasSurrogatePairs } = useUnicodeCount(text);

  const handleChange = useCallback((newText: string) => {
    const chars = Array.from(newText);

    if (chars.length <= maxLength) {
      setText(newText);
    } else {
      // 智能截断:保留有效字符
      setText(chars.slice(0, maxLength).join(''));
    }
  }, [maxLength]);

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        value={text}
        onChangeText={handleChange}
        placeholder={placeholder}
        maxLength={undefined}  // 不使用原生maxLength
        keyboardType="default"
        autoCorrect={false}
      />

      <View style={styles.infoBar}>
        <Text>字符数: {unicodeLength}/{maxLength}</Text>
        {showDebugInfo && (
          <>
            <Text> | UTF-16: {utf16Length}</Text>
            {hasSurrogatePairs && <Text style={styles.warning}>含Emoji</Text>}
          </>
        )}
      </View>
    </View>
  );
};

export default AdvancedEmojiInput;

六、性能优化与最佳实践

优化策略 实现方式 效果
防抖处理 使用useCallback包装onChangeText 减少不必要渲染
懒加载 按需加载验证逻辑 降低初始渲染时间
虚拟化 使用React.memo包装组件 避免父组件重渲染影响
缓存优化 缓存Array.from结果 避免重复计算

七、总结

本文详细介绍了在HarmonyOS平台上实现支持Emoji输入的TextInput组件的完整方案。核心要点:

  1. 问题根源:UTF-16编码导致Emoji占用2个代码单元
  2. 核心解决 :使用Array.from()正确分割字符
  3. 平台适配:针对HarmonyOS的特殊优化
  4. 生产级实现:包含验证、截断、调试等完整功能

参考资料

📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
lbb 小魔仙3 小时前
鸿蒙跨平台实战:React Native在OpenHarmony上的AccessibilityInfo无障碍检测
react native·华为·harmonyos
星空22237 小时前
【HarmonyOS】day43:RN_of_HarmonyOS实战项目_电话号码输入
华为·harmonyos
星空22239 小时前
【HarmonyOS】day44:RN_of_HarmonyOS实战项目_富文本编辑器
华为·harmonyos
松叶似针9 小时前
Flutter三方库适配OpenHarmony【doc_text】— Word 文档格式深度科普:从 OLE2 到 OOXML
flutter·harmonyos
木斯佳9 小时前
HarmonyOS实战(解决方案篇)—从实战案例了解应用并发设计
华为·harmonyos
空白诗9 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、粒子系统与流体模拟:动态粒子的视觉盛宴
flutter·harmonyos
空白诗9 小时前
Harmony Flutter 跨平台开发实战:鸿蒙与音乐律动艺术、混沌理论与奇异吸引子:从洛伦兹到音乐的动态艺术
flutter·harmonyos
lbb 小魔仙10 小时前
鸿蒙跨平台实战:React Native在OpenHarmony上的Font字体降级策略详解
react native·华为·harmonyos
hqk10 小时前
鸿蒙项目实战:手把手带你从零架构 WanAndroid 鸿蒙版
前端·架构·harmonyos