React Native for OpenHarmony 实战:颜色转换实现

今天我们用 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 格式的颜色值,如#4A90D9r、g、b:RGB 三个通道的值,范围 0-255
为什么 RGB 用字符串而不是数字?因为 TextInput 的 value 必须是字符串,用字符串可以直接绑定,不需要来回转换。
动画值:
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 格式不正确),result 为 null,不执行后续代码,保持原有的 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
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)