在这里插入图片描述

今天我们用 React Native 实现一个颜色转换工具,支持 HEX、RGB、HSL 三种颜色格式的互相转换,还带有实时预览和入场动画效果。

状态设计

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

export const ColorConverter: React.FC = () => {
  const [hex, setHex] = useState('#4A90D9');
  const [r, setR] = useState('74');
  const [g, setG] = useState('144');
  const [b, setB] = useState('217');
  
  const colorAnim = useRef(new Animated.Value(1)).current;
  const cardAnims = useRef([0, 1, 2, 3].map(() => new Animated.Value(0))).current;
  const pulseAnim = useRef(new Animated.Value(1)).current;

状态设计包含颜色值和动画值:

颜色状态

  • hex:HEX 格式的颜色值,如 #4A90D9
  • rgb:RGB 三个通道的值,范围 0-255

为什么 RGB 用字符串而不是数字?因为 TextInputvalue 必须是字符串,用字符串可以直接绑定,不需要来回转换。

动画值

  • colorAnim:颜色预览区的缩放动画,颜色变化时触发
  • cardAnims:4 个卡片的入场动画数组(HEX、RGB、HSL、CSS)
  • pulseAnim:预览区文字的呼吸动画

入场动画

  useEffect(() => {
    cardAnims.forEach((anim, i) => {
      setTimeout(() => {
        Animated.spring(anim, { toValue: 1, friction: 5, useNativeDriver: true }).start();
      }, i * 100);
    });
    
    Animated.loop(
      Animated.sequence([
        Animated.timing(pulseAnim, { toValue: 1.02, duration: 1500, useNativeDriver: true }),
        Animated.timing(pulseAnim, { toValue: 1, duration: 1500, useNativeDriver: true }),
      ])
    ).start();
  }, []);

组件挂载时触发两个动画:

卡片入场动画:4 个卡片依次弹出,每个延迟 100ms。Animated.spring 创建弹簧动画,friction: 5 设置摩擦力,让卡片有"回弹"效果。

脉冲动画:预览区文字缓慢放大(1 → 1.02)再缩小(1.02 → 1),每个方向 1.5 秒,总共 3 秒一个周期。Animated.loop 让动画无限循环,Animated.sequence 让两个动画依次执行。

为什么脉冲动画用 1.5 秒而不是 1 秒?因为颜色预览是页面的焦点,动画应该更舒缓,不要太急促。1.5 秒的时长让动画看起来很优雅。

颜色变化动画

  const animateColorChange = () => {
    Animated.sequence([
      Animated.timing(colorAnim, { toValue: 0.9, duration: 100, useNativeDriver: true }),
      Animated.spring(colorAnim, { toValue: 1, friction: 4, useNativeDriver: true }),
    ]).start();
  };

当颜色值改变时触发这个动画,预览区先缩小到 90%,再弹回到 100%。这个动画给用户"颜色已更新"的视觉反馈。

动画时长 100ms 很短,用户感觉不到延迟,但足以产生明显的视觉效果。friction: 4 让弹簧动画更稳定,不会弹得太厉害。

HEX 转 RGB

  const hexToRgb = (h: string) => {
    animateColorChange();
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(h);
    if (result) {
      setR(parseInt(result[1], 16).toString());
      setG(parseInt(result[2], 16).toString());
      setB(parseInt(result[3], 16).toString());
    }
  };

HEX 转 RGB 的核心是正则表达式和进制转换。

正则表达式解析

  • ^#?:可选的 # 开头
  • ([a-f\d]{2}):两个十六进制字符,捕获为一组(红色通道)
  • 重复三次,分别捕获 R、G、B 三个通道
  • $:字符串结尾
  • i:不区分大小写

进制转换parseInt(result[1], 16) 把十六进制字符串转成十进制数字。比如 "4A" 转成 74

为什么要 toString()?因为状态是字符串类型,parseInt 返回数字,需要转回字符串才能设置到状态。

错误处理:如果正则匹配失败(HEX 格式不正确),resultnull,不执行后续代码,保持原有的 RGB 值不变。

RGB 转 HEX

  const rgbToHex = () => {
    animateColorChange();
    const toHex = (n: number) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0');
    setHex('#' + toHex(parseInt(r) || 0) + toHex(parseInt(g) || 0) + toHex(parseInt(b) || 0));
  };

RGB 转 HEX 需要把每个通道的十进制数字转成两位十六进制字符。

边界处理Math.max(0, Math.min(255, n)) 确保数值在 0-255 范围内。如果用户输入 300,会被限制为 255;如果输入 -10,会被限制为 0。

进制转换toString(16) 把十进制数字转成十六进制字符串。比如 74 转成 "4a"

补零padStart(2, '0') 确保结果是两位字符。比如 "a" 补成 "0a""4a" 保持不变。

拼接:三个通道的十六进制字符拼接起来,前面加 #,就得到完整的 HEX 颜色值。

默认值parseInt(r) || 0 处理空字符串或无效输入,默认为 0。

RGB 转 HSL

  const rgbToHsl = () => {
    const rr = (parseInt(r) || 0) / 255;
    const gg = (parseInt(g) || 0) / 255;
    const bb = (parseInt(b) || 0) / 255;
    const max = Math.max(rr, gg, bb), min = Math.min(rr, gg, bb);
    let h = 0, s = 0, l = (max + min) / 2;

RGB 转 HSL 的算法比较复杂,需要理解 HSL 颜色模型。

归一化:先把 RGB 值从 0-255 范围归一化到 0-1 范围,方便后续计算。

亮度(Lightness)l = (max + min) / 2,最大值和最小值的平均值。亮度表示颜色的明暗程度,0 是黑色,1 是白色,0.5 是正常亮度。

饱和度和色相的计算

    if (max !== min) {
      const d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
      switch (max) {
        case rr: h = ((gg - bb) / d + (gg < bb ? 6 : 0)) / 6; break;
        case gg: h = ((bb - rr) / d + 2) / 6; break;
        case bb: h = ((rr - gg) / d + 4) / 6; break;
      }
    }
    return `hsl(${Math.round(h * 360)}, ${Math.round(s * 100)}%, ${Math.round(l * 100)}%)`;
  };

饱和度(Saturation):表示颜色的鲜艳程度。如果 max === min,说明是灰色,饱和度为 0。否则根据亮度选择不同的公式计算。

色相(Hue):表示颜色的种类(红、橙、黄、绿、青、蓝、紫)。根据哪个通道最大,用不同的公式计算。色相是一个 0-1 的值,乘以 360 得到角度(0-360 度)。

格式化输出:色相转成角度,饱和度和亮度转成百分比,拼接成 CSS 的 HSL 格式。

这个算法是标准的 RGB 到 HSL 转换算法,在各种编程语言中都是一样的。

鸿蒙 ArkTS 对比:颜色转换

hexToRgb(h: string) {
  this.animateColorChange()
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(h)
  if (result) {
    this.r = parseInt(result[1], 16).toString()
    this.g = parseInt(result[2], 16).toString()
    this.b = parseInt(result[3], 16).toString()
  }
}

rgbToHex() {
  this.animateColorChange()
  const toHex = (n: number) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, '0')
  this.hex = '#' + toHex(parseInt(this.r) || 0) + toHex(parseInt(this.g) || 0) + toHex(parseInt(this.b) || 0)
}

ArkTS 中的颜色转换逻辑完全一样,因为正则表达式、数学运算、字符串方法都是 JavaScript 标准 API,跨平台通用。

界面渲染:头部和预览区

  const currentColor = hex.startsWith('#') && hex.length === 7 ? hex : '#000000';

  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerIcon}>🎨</Text>
        <Text style={styles.headerTitle}>颜色转换</Text>
        <Text style={styles.headerSubtitle}>HEX / RGB / HSL 互转</Text>
      </View>

      <Animated.View style={[styles.preview, {
        backgroundColor: currentColor,
        transform: [{ scale: colorAnim }],
      }]}>
        <Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
          <Text style={styles.previewText}>{currentColor}</Text>
        </Animated.View>
      </Animated.View>

颜色验证currentColor 确保颜色值有效。如果 HEX 格式不正确(不是 7 个字符或不以 # 开头),使用黑色 #000000 作为默认值,避免渲染错误。

预览区设计

  • 背景色动态绑定到 currentColor,实时显示当前颜色
  • 应用缩放动画 colorAnim,颜色变化时有视觉反馈
  • 内层文字应用脉冲动画 pulseAnim,形成呼吸效果
  • 文字用白色加阴影,确保在任何背景色上都清晰可见

为什么用两层 Animated.View?因为外层应用颜色变化动画(快速缩放),内层应用脉冲动画(缓慢呼吸),两个动画独立播放,互不干扰。

HEX 输入卡片

      <Animated.View style={[styles.card, {
        transform: [{ scale: cardAnims[0] }],
        opacity: cardAnims[0],
      }]}>
        <Text style={styles.label}>🔤 HEX</Text>
        <TextInput
          style={styles.input}
          value={hex}
          onChangeText={(v) => { setHex(v); hexToRgb(v); }}
          placeholder="#000000"
          placeholderTextColor="#666"
          autoCapitalize="none"
        />
      </Animated.View>

HEX 输入卡片应用入场动画,从无到有弹出。

实时转换onChangeText 同时更新 HEX 状态和触发 RGB 转换。用户每输入一个字符,RGB 值就会实时更新。

自动大小写autoCapitalize="none" 禁用自动大写,因为 HEX 颜色值通常用小写。虽然正则表达式不区分大小写,但保持输入的原样更符合用户习惯。

占位符#000000 提示用户 HEX 格式,让用户知道应该输入什么。

RGB 输入卡片

      <Animated.View style={[styles.card, {
        transform: [{ scale: cardAnims[1] }],
        opacity: cardAnims[1],
      }]}>
        <Text style={styles.label}>🌈 RGB</Text>
        <View style={styles.rgbRow}>
          {[
            { label: 'R', value: r, setter: setR, color: '#e74c3c' },
            { label: 'G', value: g, setter: setG, color: '#2ecc71' },
            { label: 'B', value: b, setter: setB, color: '#3498db' },
          ].map(item => (
            <View key={item.label} style={styles.rgbInput}>
              <Text style={[styles.rgbLabel, { color: item.color }]}>{item.label}</Text>
              <TextInput
                style={styles.rgbInputField}
                value={item.value}
                onChangeText={item.setter}
                onBlur={rgbToHex}
                keyboardType="numeric"
                placeholderTextColor="#666"
              />
            </View>
          ))}
        </View>
      </Animated.View>

RGB 输入卡片包含三个输入框,横向排列。

数据驱动:用数组定义三个输入框的配置,map 遍历生成。每个输入框有独特的颜色:R 用红色,G 用绿色,B 用蓝色,直观易懂。

延迟转换onBlur 在输入框失焦时触发 HEX 转换。为什么不用 onChangeText?因为用户可能输入多位数字(比如 255),如果每输入一个字符就转换,会导致频繁更新,体验不好。等用户输入完成(失焦)再转换,更合理。

数字键盘keyboardType="numeric" 弹出数字键盘,方便用户输入。

HSL 和 CSS 卡片

      <Animated.View style={[styles.card, {
        transform: [{ scale: cardAnims[2] }],
        opacity: cardAnims[2],
      }]}>
        <Text style={styles.label}>🎯 HSL</Text>
        <Text style={styles.hslValue}>{rgbToHsl()}</Text>
      </Animated.View>

      <Animated.View style={[styles.card, {
        transform: [{ scale: cardAnims[3] }],
        opacity: cardAnims[3],
      }]}>
        <Text style={styles.label}>💻 CSS</Text>
        <View style={styles.cssBox}>
          <Text style={styles.cssValue} selectable>background-color: {currentColor};</Text>
          <Text style={styles.cssValue} selectable>background-color: rgb({r}, {g}, {b});</Text>
        </View>
      </Animated.View>
    </ScrollView>
  );
};

HSL 卡片:只显示不可编辑,因为 HSL 是从 RGB 计算得出的。每次渲染都会调用 rgbToHsl() 重新计算,保持和 RGB 同步。

CSS 卡片:显示两种 CSS 写法(HEX 和 RGB),方便用户复制到代码中。selectable 属性让文本可以长按选择复制。

为什么用等宽字体?CSS 代码用等宽字体(fontFamily: 'monospace')更易读,这是代码编辑器的标准做法。

样式定义:预览区

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 },
  preview: {
    height: 140,
    borderRadius: 20,
    marginBottom: 20,
    justifyContent: 'center',
    alignItems: 'center',
    borderWidth: 2,
    borderColor: '#3a3a6a',
  },
  previewText: {
    color: '#fff',
    fontSize: 28,
    fontWeight: '700',
    textShadowColor: '#000',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 4,
  },

预览区高度 140 像素,圆角 20,边框 2 像素。内容居中对齐,文字用白色加黑色阴影,确保在任何背景色上都清晰可见。

为什么用文字阴影?因为预览区的背景色是动态的,可能是浅色也可能是深色。白色文字在浅色背景上看不清,黑色阴影可以增强对比度,让文字在任何背景上都清晰。

样式定义:卡片和输入框

  card: {
    backgroundColor: '#1a1a3e',
    padding: 16,
    borderRadius: 16,
    marginBottom: 12,
    borderWidth: 1,
    borderColor: '#3a3a6a',
  },
  label: { fontSize: 14, color: '#888', marginBottom: 12 },
  input: {
    backgroundColor: '#252550',
    padding: 14,
    borderRadius: 10,
    fontSize: 20,
    color: '#fff',
    textAlign: 'center',
  },
  rgbRow: { flexDirection: 'row' },
  rgbInput: { flex: 1, marginHorizontal: 4 },
  rgbLabel: { fontSize: 14, fontWeight: '600', marginBottom: 8, textAlign: 'center' },
  rgbInputField: {
    backgroundColor: '#252550',
    padding: 12,
    borderRadius: 10,
    fontSize: 18,
    color: '#fff',
    textAlign: 'center',
  },
  hslValue: { fontSize: 18, color: '#fff', textAlign: 'center' },
  cssBox: { backgroundColor: '#252550', padding: 12, borderRadius: 10 },
  cssValue: { fontSize: 13, color: '#9cdcfe', fontFamily: 'monospace', marginBottom: 6 },
});

所有卡片用统一的样式:深蓝色背景、圆角 16、边框。输入框用更深的背景色,和卡片形成对比。

RGB 输入框用 flex: 1 平均分配宽度,marginHorizontal: 4 在它们之间留出间距。

CSS 代码用青蓝色(#9cdcfe),这是 VS Code 中字符串的颜色,程序员看着很熟悉。

小结

这个颜色转换工具展示了颜色格式之间的转换算法。HEX 和 RGB 的转换比较简单,主要是进制转换。RGB 到 HSL 的转换涉及颜色模型的理解,算法稍复杂但是标准的。实时预览让用户直观地看到颜色效果,入场动画和颜色变化动画让工具更生动。在 OpenHarmony 平台上,颜色转换的算法是纯 JavaScript,跨平台通用。


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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐