在这里插入图片描述

在日常开发中,我们经常需要对文本进行编码和解码。Base64 是最常用的编码方式之一,它可以把任意二进制数据转换成可打印的 ASCII 字符。今天我们用 React Native 实现一个文本加密工具,支持 Base64 编码和解码,还带有流畅的切换动画。

Base64 编码原理

Base64 不是加密算法,而是一种编码方式。它的核心思想是把 3 个字节(24 位)的数据分成 4 组,每组 6 位,然后用 64 个可打印字符来表示这 6 位数据。

为什么要用 Base64?因为有些场景只能传输文本数据,比如 JSON、XML、URL 参数等。如果要传输二进制数据(图片、文件等),就需要先编码成文本。

Base64 使用的 64 个字符是:A-Z、a-z、0-9、+、/,再加上填充字符 =。

状态设计

import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Animated } from 'react-native';

export const TextEncrypt: React.FC = () => {
  const [input, setInput] = useState('');
  const [output, setOutput] = useState('');
  const [mode, setMode] = useState<'encode' | 'decode'>('encode');
  
  const scaleAnim = useRef(new Animated.Value(1)).current;
  const outputAnim = useRef(new Animated.Value(0)).current;
  const tabAnim = useRef(new Animated.Value(0)).current;

状态变量的设计很简洁:

输入和输出 inputoutput:分别存储用户输入的文本和处理后的结果。初始都是空字符串。

模式 mode:用 TypeScript 的联合类型限定只能是 'encode''decode',避免拼写错误。默认是编码模式。

动画值

  • scaleAnim:按钮按下时的缩放动画,初始值 1 表示正常大小
  • outputAnim:结果区域的弹出动画,初始值 0 表示隐藏状态
  • tabAnim:标签页切换时的滑动动画,0 表示左边,1 表示右边

为什么用三个独立的动画值?因为它们控制不同的元素,需要独立触发。如果用一个动画值控制多个元素,就无法实现不同的动画效果。

标签页切换动画

  useEffect(() => {
    Animated.timing(tabAnim, {
      toValue: mode === 'encode' ? 0 : 1,
      duration: 200,
      useNativeDriver: false,
    }).start();
  }, [mode]);

这个 useEffect 监听 mode 的变化,当用户切换编码/解码模式时,自动触发标签指示器的滑动动画。

动画目标值:编码模式时滑到左边(0),解码模式时滑到右边(1)。

动画时长:200 毫秒,这是一个比较舒适的速度,不会太快也不会太慢。

useNativeDriver:这里设置为 false,因为我们要动画化 left 属性(位置),而原生驱动只支持 transformopacity。虽然性能稍差,但对于这种简单动画影响不大。

为什么用 useEffect 而不是在按钮点击时触发?因为 mode 可能在其他地方被修改,用 useEffect 可以确保动画总是和状态同步。

Base64 编码实现

  const base64Encode = (str: string) => {
    try {
      const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
      const bytes = new TextEncoder().encode(str);
      let result = '';

Base64 编码的第一步是把字符串转换成字节数组。TextEncoder 是浏览器和 Node.js 都支持的标准 API,它把字符串按 UTF-8 编码转换成字节。

字符表 chars:这 64 个字符就是 Base64 的"字母表"。索引 0-63 分别对应这 64 个字符。

为什么用 try-catch:编码过程可能失败,比如输入包含无法编码的字符。捕获异常后返回友好的错误提示,避免程序崩溃。

编码核心逻辑

      for (let i = 0; i < bytes.length; i += 3) {
        const b1 = bytes[i], b2 = bytes[i + 1] || 0, b3 = bytes[i + 2] || 0;
        result += chars[b1 >> 2] + chars[((b1 & 3) << 4) | (b2 >> 4)] +
          (i + 1 < bytes.length ? chars[((b2 & 15) << 2) | (b3 >> 6)] : '=') +
          (i + 2 < bytes.length ? chars[b3 & 63] : '=');
      }
      return result;
    } catch { return '编码失败'; }
  };

这是 Base64 编码的核心算法,每次处理 3 个字节(24 位),转换成 4 个 Base64 字符。

循环步长i += 3 表示每次跳 3 个字节。如果字节数不是 3 的倍数,最后一组会不足 3 个字节。

读取字节b1b2b3 分别是当前组的三个字节。如果不足 3 个,用 || 0 补零。

位运算拆分

  • 第 1 个字符:取 b1 的高 6 位 → b1 >> 2
  • 第 2 个字符:取 b1 的低 2 位和 b2 的高 4 位 → ((b1 & 3) << 4) | (b2 >> 4)
  • 第 3 个字符:取 b2 的低 4 位和 b3 的高 2 位 → ((b2 & 15) << 2) | (b3 >> 6)
  • 第 4 个字符:取 b3 的低 6 位 → b3 & 63

填充处理:如果原始数据不足 3 个字节,用 = 填充。i + 1 < bytes.length 判断是否有第 2 个字节,没有就用 = 代替第 3 个字符。同理,第 4 个字符也可能是 =

这个算法看起来复杂,但本质就是把 24 位数据重新分组成 4 个 6 位数据。位运算是最高效的实现方式。

Base64 解码实现

  const base64Decode = (str: string) => {
    try {
      const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
      str = str.replace(/=+$/, '');
      const bytes: number[] = [];

解码是编码的逆过程,把 Base64 字符串还原成字节数组,再转换成原始字符串。

去除填充replace(/=+$/, '') 删除末尾的 = 字符。填充只是为了凑齐 4 的倍数,解码时不需要。

字节数组:用 number[] 存储解码后的字节,最后再转换成字符串。

解码核心逻辑

      for (let i = 0; i < str.length; i += 4) {
        const b1 = chars.indexOf(str[i]), b2 = chars.indexOf(str[i + 1]);
        const b3 = chars.indexOf(str[i + 2]), b4 = chars.indexOf(str[i + 3]);
        bytes.push((b1 << 2) | (b2 >> 4));
        if (b3 >= 0) bytes.push(((b2 & 15) << 4) | (b3 >> 2));
        if (b4 >= 0) bytes.push(((b3 & 3) << 6) | b4);
      }
      return new TextDecoder().decode(new Uint8Array(bytes));
    } catch { return '解码失败'; }
  };

解码算法每次处理 4 个 Base64 字符,还原成 3 个字节。

查找索引chars.indexOf(str[i]) 把字符转换回 0-63 的索引值。如果字符不在字符表中,返回 -1。

位运算合并

  • 第 1 个字节:取 b1 的全部 6 位和 b2 的高 2 位 → (b1 << 2) | (b2 >> 4)
  • 第 2 个字节:取 b2 的低 4 位和 b3 的高 4 位 → ((b2 & 15) << 4) | (b3 >> 2)
  • 第 3 个字节:取 b3 的低 2 位和 b4 的全部 6 位 → ((b3 & 3) << 6) | b4

边界检查if (b3 >= 0) 判断第 3 个字符是否存在(不是填充)。如果是 -1,说明原始数据只有 1 个字节,不需要生成第 2、3 个字节。

转换字符串TextDecoder 把字节数组按 UTF-8 解码成字符串。Uint8Array 是 JavaScript 的类型化数组,专门用于处理二进制数据。

这个算法和编码算法是对称的,理解了编码,解码就很容易理解了。

鸿蒙 ArkTS 对比:编解码实现

import util from '@ohos.util'

base64Encode(str: string): string {
  try {
    const textEncoder = new util.TextEncoder()
    const bytes = textEncoder.encodeInto(str)
    const base64 = new util.Base64Helper()
    return base64.encodeToStringSync(bytes)
  } catch {
    return '编码失败'
  }
}

base64Decode(str: string): string {
  try {
    const base64 = new util.Base64Helper()
    const bytes = base64.decodeSync(str)
    const textDecoder = util.TextDecoder.create('utf-8')
    return textDecoder.decodeWithStream(bytes)
  } catch {
    return '解码失败'
  }
}

鸿蒙系统提供了 util.Base64Helper 工具类,可以直接调用,不需要手动实现位运算。这是系统级 API,性能更好,代码也更简洁。

但我们的手动实现也有优势:跨平台兼容性好,不依赖特定平台的 API。而且理解了算法原理,遇到问题时更容易调试。

处理函数

  const process = () => {
    Animated.sequence([
      Animated.timing(scaleAnim, { toValue: 0.95, duration: 100, useNativeDriver: true }),
      Animated.spring(scaleAnim, { toValue: 1, friction: 3, useNativeDriver: true }),
    ]).start();
    
    outputAnim.setValue(0);
    Animated.spring(outputAnim, { toValue: 1, friction: 5, useNativeDriver: true }).start();
    
    setOutput(mode === 'encode' ? base64Encode(input) : base64Decode(input));
  };

点击编码/解码按钮时触发这个函数,它做了三件事:

按钮动画:先缩小到 95%(100ms),再弹回原大小。Animated.spring 创建弹簧动画,friction: 3 设置摩擦力,值越小弹性越大。这个动画给用户"按下按钮"的触感反馈。

结果动画:重置 outputAnim 为 0(隐藏状态),然后弹出到 1(显示状态)。friction: 5 让弹出效果更明显。

执行编解码:根据当前模式调用对应的函数,把结果存入 output。三元运算符让代码更简洁。

为什么先触发动画再设置结果?因为动画是异步的,start() 不会阻塞代码执行。如果先设置结果,界面会立即更新,动画效果就不明显了。

标签页插值

  const tabPosition = tabAnim.interpolate({ inputRange: [0, 1], outputRange: ['0%', '50%'] });

interpolate 把动画值映射成实际的位置:

  • 动画值 0 → 位置 0%(左边)
  • 动画值 1 → 位置 50%(右边)

为什么用百分比而不是像素?因为标签页的宽度是动态的,用百分比可以自适应不同屏幕尺寸。50% 正好是两个标签页的分界线。

界面渲染:头部

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerIcon}>🔐</Text>
        <Text style={styles.headerTitle}>文本加密</Text>
        <Text style={styles.headerSubtitle}>Base64 编码/解码</Text>
      </View>

头部区域包含三个元素:

图标 🔐:锁的 emoji 表示"加密"的概念,字号 50 很醒目。

标题:28 号粗体白色文字,是页面的主标题。

副标题:14 号灰色文字,说明工具的功能。副标题比标题小,颜色也更淡,形成视觉层次。

ScrollView 包裹整个页面,因为输入和输出的文本可能很长,需要滚动查看。

标签页切换

      <View style={styles.tabContainer}>
        <Animated.View style={[styles.tabIndicator, { left: tabPosition }]} />
        <TouchableOpacity style={styles.tab} onPress={() => setMode('encode')} activeOpacity={0.7}>
          <Text style={[styles.tabText, mode === 'encode' && styles.tabTextActive]}>🔒 编码</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.tab} onPress={() => setMode('decode')} activeOpacity={0.7}>
          <Text style={[styles.tabText, mode === 'decode' && styles.tabTextActive]}>🔓 解码</Text>
        </TouchableOpacity>
      </View>

标签页的实现很巧妙:

容器:深色背景,圆角 12,内边距 4,形成一个"轨道"。

指示器:蓝色背景的 Animated.View,绝对定位,宽度 50%。left 属性绑定动画值,实现滑动效果。

标签按钮:两个按钮平分宽度(flex: 1),zIndex: 1 让它们显示在指示器上方。点击时切换 mode 状态。

文字样式:默认灰色,选中时白色加粗。mode === 'encode' && styles.tabTextActive 是条件样式的常用写法。

图标:🔒 表示编码(锁上),🔓 表示解码(解锁),直观易懂。

这种标签页设计在很多 App 中都能看到,用户很熟悉,不需要学习成本。

鸿蒙 ArkTS 对比:标签页

Tabs({ index: this.mode === 'encode' ? 0 : 1 }) {
  TabContent() {
    // 编码界面
  }.tabBar('🔒 编码')
  
  TabContent() {
    // 解码界面
  }.tabBar('🔓 解码')
}
.barMode(BarMode.Fixed)
.barBackgroundColor('#1a1a3e')
.onChange((index: number) => {
  this.mode = index === 0 ? 'encode' : 'decode'
})

鸿蒙提供了 Tabs 组件,自带滑动指示器和切换动画,不需要手动实现。但灵活性不如自定义实现,比如指示器的样式、动画效果等都受限于组件的设计。

输入区域

      <View style={styles.inputCard}>
        <Text style={styles.label}>{mode === 'encode' ? '📝 原文' : '🔤 Base64'}</Text>
        <TextInput
          style={styles.input}
          value={input}
          onChangeText={setInput}
          multiline
          placeholder="输入文本..."
          placeholderTextColor="#666"
        />
      </View>

输入区域包含标签和输入框:

动态标签:根据模式显示不同的文字。编码模式下是"原文",解码模式下是"Base64"。这样用户一眼就知道应该输入什么。

多行输入框multiline 属性让输入框支持换行,适合输入长文本。minHeight: 120 确保输入框有足够的高度。

占位符:灰色的提示文字,输入内容后自动消失。placeholderTextColor 自定义占位符颜色,默认的颜色在深色背景上不够明显。

受控组件valueonChangeText 绑定状态,这是 React 的标准模式。输入框的内容完全由状态控制,方便做验证、格式化等操作。

处理按钮

      <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
        <TouchableOpacity style={styles.btn} onPress={process} activeOpacity={0.8}>
          <Text style={styles.btnIcon}>{mode === 'encode' ? '🔒' : '🔓'}</Text>
          <Text style={styles.btnText}>{mode === 'encode' ? '编码' : '解码'}</Text>
        </TouchableOpacity>
      </Animated.View>

按钮用 Animated.View 包裹,应用缩放动画。点击时会触发 process 函数中的动画序列。

动态内容:图标和文字都根据模式动态变化。编码时显示 🔒 和"编码",解码时显示 🔓 和"解码"。

横向布局flexDirection: 'row' 让图标和文字水平排列,alignItems: 'center' 垂直居中。

视觉反馈activeOpacity={0.8} 让按钮按下时稍微变暗,配合缩放动画,给用户明确的触摸反馈。

输出区域

      {output ? (
        <Animated.View style={[styles.outputCard, {
          transform: [{ scale: outputAnim }, { perspective: 1000 }],
          opacity: outputAnim,
        }]}>
          <Text style={styles.label}>{mode === 'encode' ? '🔤 Base64' : '📝 原文'}</Text>
          <View style={styles.outputBox}>
            <Text style={styles.output} selectable>{output}</Text>
          </View>
        </Animated.View>
      ) : null}
    </ScrollView>
  );
};

输出区域只在有结果时才显示,用条件渲染实现。

弹出动画:同时应用缩放和透明度动画。scale 从 0 到 1,opacity 也从 0 到 1,形成"弹出"效果。

透视效果perspective: 1000 添加 3D 透视,让缩放动画更有立体感。这是一个细节优化,让动画看起来更自然。

动态标签:和输入区域相反,编码模式下显示"Base64",解码模式下显示"原文"。

可选择文本selectable 属性让用户可以长按选择复制结果,这是一个很重要的功能。如果不能复制,用户就得手动输入,体验很差。

等宽字体fontFamily: 'monospace' 让 Base64 字符串更易读,每个字符占据相同宽度。

样式定义:容器和头部

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0f0f23', padding: 20 },
  header: { alignItems: 'center', marginBottom: 24 },
  headerIcon: { fontSize: 50, marginBottom: 8 },
  headerTitle: { fontSize: 28, fontWeight: '700', color: '#fff' },
  headerSubtitle: { fontSize: 14, color: '#888', marginTop: 4 },

容器用深蓝色背景 #0f0f23,这是一个很流行的深色主题配色,比纯黑色更柔和。

头部元素全部居中对齐,图标、标题、副标题之间有适当的间距,形成清晰的视觉层次。

样式定义:标签页

  tabContainer: {
    flexDirection: 'row',
    backgroundColor: '#1a1a3e',
    borderRadius: 12,
    padding: 4,
    marginBottom: 20,
    position: 'relative',
  },
  tabIndicator: {
    position: 'absolute',
    width: '50%',
    height: '100%',
    backgroundColor: '#4A90D9',
    borderRadius: 10,
    top: 4,
  },
  tab: { flex: 1, paddingVertical: 12, alignItems: 'center', zIndex: 1 },
  tabText: { color: '#888', fontSize: 16 },
  tabTextActive: { color: '#fff', fontWeight: '600' },

标签容器用 position: 'relative',让指示器可以绝对定位。内边距 4 在指示器和边缘之间留出空隙。

指示器的圆角 10 比容器的圆角 12 小一点,这样四个角不会超出容器。top: 4 对应容器的内边距。

标签按钮用 zIndex: 1 确保显示在指示器上方,否则文字会被蓝色背景遮挡。

样式定义:输入输出和按钮

  inputCard: {
    backgroundColor: '#1a1a3e',
    borderRadius: 20,
    padding: 20,
    marginBottom: 20,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  label: { color: '#888', fontSize: 14, marginBottom: 12 },
  input: {
    backgroundColor: '#252550',
    padding: 16,
    borderRadius: 12,
    minHeight: 120,
    color: '#fff',
    fontSize: 16,
  },
  btn: {
    backgroundColor: '#4A90D9',
    borderRadius: 16,
    paddingVertical: 18,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 20,
    shadowColor: '#4A90D9',
    shadowOffset: { width: 0, height: 8 },
    shadowOpacity: 0.4,
    shadowRadius: 15,
  },
  btnIcon: { fontSize: 20, marginRight: 10 },
  btnText: { color: '#fff', fontSize: 18, fontWeight: '700' },
  outputCard: {
    backgroundColor: '#1a1a3e',
    borderRadius: 20,
    padding: 20,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  outputBox: {
    backgroundColor: '#252550',
    padding: 16,
    borderRadius: 12,
    minHeight: 100,
  },
  output: { color: '#9cdcfe', fontFamily: 'monospace', fontSize: 14 },
});

输入和输出卡片用相同的样式,保持视觉一致性。深色背景 #1a1a3e 和边框 #3a3a6a 形成层次感。

输入框和输出框用更深的背景色 #252550,和卡片背景形成对比,让用户知道这是可交互的区域。

按钮用蓝色背景和蓝色阴影,是页面的视觉焦点。阴影向下偏移 8 像素,模拟"悬浮"效果。

输出文本用青蓝色 #9cdcfe,这是 VS Code 中字符串的颜色,程序员看着很亲切。

小结

这个文本加密工具展示了 Base64 编解码的完整实现。虽然鸿蒙系统提供了现成的 API,但手动实现让我们理解了编码的原理,也提高了代码的跨平台兼容性。

标签页的滑动动画、按钮的缩放动画、结果的弹出动画,这些细节让工具用起来更流畅。动画不是装饰,而是给用户反馈,让他们知道操作已经生效。

在实际项目中,Base64 常用于图片上传(转成 Data URL)、API 认证(Basic Auth)、数据传输等场景。理解了原理,遇到相关问题时就能快速定位和解决。


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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐