从产品需求到代码实现:React Native for OpenHarmony Checkbox 组件全解析
项目开源地址:https://atomgit.com/nutpi/rn_for_openharmony_element
上周产品经理拿着原型图来找我,说要做一个商品筛选页面,用户可以勾选多个品牌、多个价格区间。我看了一眼,这不就是一堆复选框嘛。但仔细一想,复选框这东西还真有点讲究。
今天就把我封装 Checkbox 组件的过程记录下来,从需求分析到最终代码,一步步来。
产品给的原型图里有这么几种复选框的用法:
第一种是最普通的,一个方框加一段文字,点一下打勾,再点一下取消。品牌筛选、标签选择都是这种。
第二种是全选功能。上面有个"全选"复选框,下面是一堆子选项。全选框有三种状态:全没选的时候是空的,全选了是打勾,选了一部分是个横杠。这个横杠状态叫"半选"或者"不确定状态"。
第三种是禁用状态。有些选项因为库存不足或者其他原因不能选,要灰显出来。
第四种是不同颜色。成功的用绿色,警告的用橙色,危险的用红色。
需求清楚了,开始设计接口。
interface CheckboxProps {
checked: boolean;
onPress: (checked: boolean) => void;
label?: string;
color?: ColorType;
size?: SizeType;
disabled?: boolean;
indeterminate?: boolean;
style?: ViewStyle;
}
这个接口定义了复选框的所有配置项,下面逐个解释:
-
checked 和 onPress 是核心属性。checked 表示当前是否选中,onPress 是点击时的回调。这是受控组件的写法,状态由外部管理。为什么用受控模式?因为复选框很少单独使用,通常是一组复选框配合使用,需要在外部统一管理状态。
-
onPress 的参数是新的 checked 值,不是事件对象。调用方直接拿到新值,不用自己取反,写起来就是
onPress={setChecked}这么简单。 -
indeterminate 是半选状态。这个词有点长,但它是业界通用的叫法,HTML 的 checkbox 也用这个词。半选状态只是视觉上的,逻辑上还是 checked 或者 unchecked。
-
label 是复选框旁边的文字。大多数情况下复选框都要配文字说明,把这个功能内置进来,省得每次都要自己写 Text 组件。
const sizeMap: Record<SizeType, { box: number; icon: number; fontSize: number }> = {
sm: { box: 16, icon: 10, fontSize: 12 },
md: { box: 20, icon: 12, fontSize: 14 },
lg: { box: 24, icon: 14, fontSize: 16 },
};
尺寸配置定义了三种规格下的具体数值:
-
box 是方框的边长。sm 是 16px,适合紧凑列表;md 是 20px,默认尺寸;lg 是 24px,适合需要强调的场景。
-
icon 是勾或横杠的字号,大概是 box 的 60%。这个比例是试出来的,太大会撑满方框,太小又看不清。
-
fontSize 是旁边文字的大小,和 box 要匹配。小方框配小字,大方框配大字,视觉上才协调。
<View
style={[
styles.box,
{
width: sizeMap[size].box,
height: sizeMap[size].box,
borderRadius: UITheme.borderRadius.sm,
backgroundColor: isActive ? colorValue : 'transparent',
borderColor: isActive ? colorValue : UITheme.colors.gray[400],
opacity: disabled ? 0.5 : 1,
},
]}
>
方框的样式逻辑是整个组件的核心:
-
isActive 是
checked || indeterminate,只要选中或者半选,方框就是激活状态。 -
激活状态下背景色和边框色都是主题色,形成一个实心的彩色方框。未激活状态下背景透明,边框是灰色,形成一个空心的灰色方框。这个设计让两种状态的视觉差异很明显。
-
borderRadius 用的是 sm(4px),让方框有一点圆角但不会太圆。复选框传统上是方的,圆角太大会像单选框。
-
disabled 状态下 opacity 变成 0.5,整个复选框变淡,这是通用的禁用状态表示方式。
{checked && <Text style={[styles.icon, { fontSize: sizeMap[size].icon }]}>✓</Text>}
{indeterminate && !checked && <Text style={[styles.icon, { fontSize: sizeMap[size].icon }]}>−</Text>}
勾和横杠的显示逻辑:
-
选中状态显示 ✓,半选状态显示 −。用 Text 组件渲染 Unicode 字符,不用引入图标库。
-
第二行的条件是
indeterminate && !checked。如果同时传了 checked 和 indeterminate,checked 优先。这是因为 indeterminate 只是一个视觉状态,逻辑上 checked 才是真正的值。 -
icon 的样式是白色粗体:
{ color: UITheme.colors.white, fontWeight: '700' },在彩色背景上很清晰。
<TouchableOpacity
style={[styles.container, style]}
onPress={() => !disabled && onPress(!checked)}
activeOpacity={0.7}
disabled={disabled}
>
点击事件的处理有几个要点:
-
整个复选框(方框 + 文字)都可以点击,不只是方框。这样点击区域更大,更容易操作,这是我踩过的坑,一开始只让方框可点,用户反馈说不好点。
-
onPress 里先检查 disabled,禁用状态下不触发回调。然后调用外部传入的 onPress,参数是 checked 的取反值。
-
即使是半选状态,点击后也是变成 checked。因为半选只是视觉状态,点击的逻辑还是在 checked 和 unchecked 之间切换。
{label && (
<Text
style={[
styles.label,
{ fontSize: sizeMap[size].fontSize, opacity: disabled ? 0.5 : 1 },
]}
>
{label}
</Text>
)}
标签的渲染逻辑:
-
有 label 才渲染,没有就不渲染,用
{label && ...}短路求值实现。 -
label 的基础样式是
{ marginLeft: 8px, color: gray[700] },让文字和方框之间有间距,颜色比纯黑淡一点。 -
disabled 状态下 label 也要变淡,和方框保持一致的视觉效果。
完整代码:
import React from 'react';
import { TouchableOpacity, View, Text, StyleSheet, ViewStyle } from 'react-native';
import { UITheme, ColorType, SizeType } from './theme';
interface CheckboxProps {
checked: boolean;
onPress: (checked: boolean) => void;
label?: string;
color?: ColorType;
size?: SizeType;
disabled?: boolean;
indeterminate?: boolean;
style?: ViewStyle;
}
export const Checkbox: React.FC<CheckboxProps> = ({
checked,
onPress,
label,
color = 'primary',
size = 'md',
disabled = false,
indeterminate = false,
style,
}) => {
const colorValue = UITheme.colors[color];
const sizeMap: Record<SizeType, { box: number; icon: number; fontSize: number }> = {
sm: { box: 16, icon: 10, fontSize: 12 },
md: { box: 20, icon: 12, fontSize: 14 },
lg: { box: 24, icon: 14, fontSize: 16 },
};
const isActive = checked || indeterminate;
return (
<TouchableOpacity
style={[styles.container, style]}
onPress={() => !disabled && onPress(!checked)}
activeOpacity={0.7}
disabled={disabled}
>
<View
style={[
styles.box,
{
width: sizeMap[size].box,
height: sizeMap[size].box,
borderRadius: UITheme.borderRadius.sm,
backgroundColor: isActive ? colorValue : 'transparent',
borderColor: isActive ? colorValue : UITheme.colors.gray[400],
opacity: disabled ? 0.5 : 1,
},
]}
>
{checked && <Text style={[styles.icon, { fontSize: sizeMap[size].icon }]}>✓</Text>}
{indeterminate && !checked && <Text style={[styles.icon, { fontSize: sizeMap[size].icon }]}>−</Text>}
</View>
{label && (
<Text
style={[
styles.label,
{ fontSize: sizeMap[size].fontSize, opacity: disabled ? 0.5 : 1 },
]}
>
{label}
</Text>
)}
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: { flexDirection: 'row', alignItems: 'center' },
box: { borderWidth: 2, alignItems: 'center', justifyContent: 'center' },
icon: { color: UITheme.colors.white, fontWeight: '700' },
label: { marginLeft: UITheme.spacing.sm, color: UITheme.colors.gray[700] },
});
下面是几个实际使用场景。
最简单的用法,单个复选框:
const [agreed, setAgreed] = useState(false);
<Checkbox
checked={agreed}
onPress={setAgreed}
label="我已阅读并同意用户协议"
/>
一行代码搞定。checked 绑定状态,onPress 直接传 setState,label 写上文字。
多选场景,比如选择兴趣标签:
const [selected, setSelected] = useState<string[]>([]);
const options = ['音乐', '电影', '读书', '运动', '旅行', '美食'];
const toggleOption = (option: string) => {
setSelected(prev =>
prev.includes(option)
? prev.filter(item => item !== option)
: [...prev, option]
);
};
<View>
{options.map(option => (
<Checkbox
key={option}
checked={selected.includes(option)}
onPress={() => toggleOption(option)}
label={option}
style={{ marginBottom: 8 }}
/>
))}
</View>
这个例子的关键点:
- 用数组存储选中的选项,而不是为每个选项单独建一个 state。
- toggleOption 函数处理选中和取消选中的逻辑:已选中的就从数组里删掉,没选中的就加进去。
- 每个 Checkbox 的 checked 通过
selected.includes(option)计算得出。
全选功能,这个稍微复杂一点:
const [items, setItems] = useState([
{ id: 1, name: '苹果', checked: false },
{ id: 2, name: '香蕉', checked: true },
{ id: 3, name: '橙子', checked: false },
]);
const allChecked = items.every(item => item.checked);
const someChecked = items.some(item => item.checked);
const toggleAll = () => {
const newValue = !allChecked;
setItems(items.map(item => ({ ...item, checked: newValue })));
};
const toggleItem = (id: number) => {
setItems(items.map(item =>
item.id === id ? { ...item, checked: !item.checked } : item
));
};
<View>
<Checkbox
checked={allChecked}
indeterminate={someChecked && !allChecked}
onPress={toggleAll}
label="全选"
/>
<View style={{ marginLeft: 24, marginTop: 8 }}>
{items.map(item => (
<Checkbox
key={item.id}
checked={item.checked}
onPress={() => toggleItem(item.id)}
label={item.name}
style={{ marginTop: 8 }}
/>
))}
</View>
</View>
全选框的状态由子选项决定:
- 全部选中:checked = true, indeterminate = false
- 部分选中:checked = false, indeterminate = true
- 全部未选:checked = false, indeterminate = false
点击全选框时,如果当前不是全选状态就全选,如果已经全选就全取消。子选项缩进 24px,视觉上形成层级关系。
表单验证场景:
const [agreed, setAgreed] = useState(false);
const [error, setError] = useState('');
const handleSubmit = () => {
if (!agreed) {
setError('请先同意用户协议');
return;
}
setError('');
// 提交表单
};
<View>
<Checkbox
checked={agreed}
onPress={(v) => {
setAgreed(v);
if (v) setError('');
}}
label="我已阅读并同意用户协议"
color={error ? 'danger' : 'primary'}
/>
{error && <Text style={{ color: 'red', marginTop: 4 }}>{error}</Text>}
<Button title="提交" onPress={handleSubmit} style={{ marginTop: 16 }} />
</View>
这个例子展示了错误状态的处理:
- 没勾选就提交时显示错误提示,复选框变成红色。
- 勾选后错误消失,颜色恢复正常。
- 通过 color 属性动态切换颜色,不需要额外的样式处理。
最后说说我踩过的坑:
第一个坑是点击区域太小。 一开始我只让方框可以点击,用户反馈说不好点。后来改成整个区域(方框 + 文字)都可以点击,体验好多了。
第二个坑是半选状态的逻辑。 一开始我把 indeterminate 当成第三种状态,和 checked、unchecked 并列。后来发现这样逻辑很乱,改成 indeterminate 只是视觉状态,逻辑上还是二元的,就清晰多了。
第三个坑是动画。 我试过给勾加个缩放动画,从 0 放大到 1。效果是挺好看的,但在列表里快速点击多个复选框时会卡。后来去掉了动画,流畅度更重要。
再补充几个进阶用法。
复选框组的封装:
如果项目里多选场景很多,可以封装一个 CheckboxGroup 组件:
interface CheckboxGroupProps {
options: { label: string; value: string; disabled?: boolean }[];
value: string[];
onChange: (value: string[]) => void;
}
const CheckboxGroup: React.FC<CheckboxGroupProps> = ({ options, value, onChange }) => {
const toggle = (optionValue: string) => {
onChange(
value.includes(optionValue)
? value.filter(v => v !== optionValue)
: [...value, optionValue]
);
};
return (
<View>
{options.map(option => (
<Checkbox
key={option.value}
checked={value.includes(option.value)}
onPress={() => toggle(option.value)}
label={option.label}
disabled={option.disabled}
style={{ marginBottom: 8 }}
/>
))}
</View>
);
};
使用起来就简单多了:
const [selected, setSelected] = useState(['music']);
<CheckboxGroup
options={[
{ label: '音乐', value: 'music' },
{ label: '电影', value: 'movie' },
{ label: '读书', value: 'reading', disabled: true },
]}
value={selected}
onChange={setSelected}
/>
这样封装的好处是:
- 统一管理选中状态,不用在每个 Checkbox 上写 checked 和 onPress
- options 配置化,数据和 UI 分离,方便从接口获取选项
- 支持禁用单个选项,通过 disabled 字段控制
和表单库配合使用:
如果项目用了 Formik 或 React Hook Form 这类表单库,Checkbox 可以这样接入:
// 配合 React Hook Form
import { Controller, useForm } from 'react-hook-form';
const { control, handleSubmit } = useForm({
defaultValues: { agreed: false }
});
<Controller
control={control}
name="agreed"
rules={{ required: '请同意用户协议' }}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<View>
<Checkbox
checked={value}
onPress={onChange}
label="我已阅读并同意用户协议"
color={error ? 'danger' : 'primary'}
/>
{error && <Text style={{ color: 'red' }}>{error.message}</Text>}
</View>
)}
/>
关键点是:
- Controller 包裹 Checkbox,让表单库管理状态
- field.onChange 直接传给 onPress,因为我们的 onPress 参数就是新值
- fieldState.error 用来显示验证错误和改变颜色
无障碍支持:
为了让视障用户也能使用复选框,需要添加无障碍属性:
<TouchableOpacity
accessible={true}
accessibilityRole="checkbox"
accessibilityState={{ checked, disabled }}
accessibilityLabel={label}
// ... 其他属性
>
这几个属性的作用:
- accessibilityRole=“checkbox” 告诉屏幕阅读器这是一个复选框
- accessibilityState 传递当前状态,屏幕阅读器会朗读"已选中"或"未选中"
- accessibilityLabel 是朗读的内容,通常就是 label 的文字
在 OpenHarmony 上这些属性同样有效,能让应用对所有用户都友好。
性能优化:
在长列表里使用复选框时,要注意性能:
// 用 useCallback 缓存 toggle 函数
const toggleItem = useCallback((id: number) => {
setItems(prev => prev.map(item =>
item.id === id ? { ...item, checked: !item.checked } : item
));
}, []);
// 用 React.memo 包裹列表项
const CheckboxItem = React.memo(({ item, onToggle }) => (
<Checkbox
checked={item.checked}
onPress={() => onToggle(item.id)}
label={item.name}
/>
));
// 在 FlatList 里使用
<FlatList
data={items}
renderItem={({ item }) => <CheckboxItem item={item} onToggle={toggleItem} />}
keyExtractor={item => item.id.toString()}
/>
优化要点:
- useCallback 缓存回调函数,避免每次渲染都创建新函数
- React.memo 包裹列表项组件,props 不变就不重新渲染
- FlatList 代替 ScrollView + map,自动回收屏幕外的组件
这些优化在选项很多(比如几十上百个)的时候特别重要。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)