React Native for OpenHarmony 实战:封装一个高复用的 Button 按钮组件
项目开源地址:https://atomgit.com/nutpi/rn_for_openharmony_element
在跨平台开发中,按钮是使用频率最高的交互组件。本文将从零开始,手把手带你封装一个支持多种样式、多种状态的 Button 组件,代码可直接用于生产环境。
一、定义组件的类型接口
首先明确 Button 需要支持哪些能力。我们通过 TypeScript 接口来定义:
interface ButtonProps {
title: string;
onPress?: () => void;
variant?: 'solid' | 'outline' | 'ghost';
color?: ColorType;
size?: SizeType;
disabled?: boolean;
loading?: boolean;
icon?: string;
fullWidth?: boolean;
style?: ViewStyle;
}
这里有几个关键的设计决策需要说明:
关于 variant 属性
variant 定义了三种视觉风格,每种风格有其特定的使用场景:
-
solid是实心填充样式,视觉权重最高。当页面上有多个按钮时,solid 样式的按钮会第一时间吸引用户注意力,因此它适合用于主要操作,比如"提交"、“确认”、"立即购买"这类需要引导用户点击的按钮。 -
outline是描边样式,只有边框没有填充背景。它的视觉权重比 solid 低一级,适合用于次要操作,比如"取消"、“返回”、“稍后再说”。当主要按钮和次要按钮并排出现时,用户能很直观地分辨出哪个是主操作。 -
ghost是幽灵按钮,既没有背景也没有边框,只有文字。它的视觉权重最低,适合用于工具栏、列表项操作、或者需要弱化视觉干扰的场景。比如文章底部的"点赞"、“收藏”、"分享"按钮,用 ghost 样式就不会喧宾夺主。
关于 color 属性
color 使用语义化命名而非具体色值。这样做有三个好处:
第一,代码可读性更好。当你看到 color="danger" 时,立刻就知道这是一个危险操作按钮,不需要去查 #EF4444 是什么颜色。
第二,便于全局换肤。如果哪天设计师说要把主色调从紫色换成蓝色,你只需要改 theme 文件里的一个值,所有用到 primary 的地方都会自动更新。
第三,减少沟通成本。设计师说"这个按钮用成功色",你直接写 color="success",不用再问"成功色的色值是多少"。
关于 size 属性
size 提供 sm/md/lg 三档尺寸。移动端屏幕寸土寸金,按钮尺寸的选择需要权衡两个因素:太大会浪费空间,太小又不好点击(OpenHarmony 的人机交互指南建议可点击区域至少 44x44 vp)。
三档尺寸的典型应用场景:sm 用于紧凑型界面,比如表格里的操作按钮;md 是默认尺寸,适合大多数场景;lg 用于需要强调的场景,比如登录页的提交按钮。
关于 loading 和 disabled
这两个是独立的状态属性。loading 表示正在进行异步操作,按钮会显示加载指示器;disabled 表示按钮不可用,通常是因为前置条件不满足(比如表单校验不通过)。
两者可以同时存在:一个 disabled 的按钮也可能处于 loading 状态(虽然这种情况比较少见)。把它们设计成独立属性而不是一个 status 枚举,是为了保持 API 的灵活性。
二、建立统一的主题配置
在写组件逻辑之前,先把主题配置文件准备好。这是保证整个 UI 库风格统一的基础:
export const UITheme = {
colors: {
primary: '#6366F1',
secondary: '#8B5CF6',
success: '#10B981',
warning: '#F59E0B',
danger: '#EF4444',
info: '#3B82F6',
white: '#FFFFFF',
gray: {
50: '#F9FAFB',
100: '#F3F4F6',
200: '#E5E7EB',
300: '#D1D5DB',
400: '#9CA3AF',
500: '#6B7280',
600: '#4B5563',
700: '#374151',
800: '#1F2937',
900: '#111827',
},
},
spacing: {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 24,
},
borderRadius: {
sm: 4,
md: 8,
lg: 12,
full: 9999,
},
fontSize: {
xs: 10,
sm: 12,
md: 14,
lg: 16,
xl: 18,
},
};
export type ColorType = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info';
export type SizeType = 'sm' | 'md' | 'lg';
颜色体系的设计思路
primary 选用 #6366F1,这是一个偏紫的蓝色,视觉上比较现代,也是近几年流行的主色调。secondary 用 #8B5CF6,是比 primary 更紫一点的颜色,用于辅助强调。
success/warning/danger 分别对应绿/黄/红,这是全世界通用的颜色语义:绿色代表成功、安全、可以继续;黄色代表警告、需要注意;红色代表危险、错误、需要停止。遵循这个约定,用户不需要学习就能理解按钮的含义。
info 用蓝色,表示中性的信息提示,既不是好消息也不是坏消息,只是需要用户知道。
灰色梯度的作用
gray 定义了从 50 到 900 共 10 个灰度值。50 最浅接近白色,900 最深接近黑色。这个梯度在 UI 开发中非常有用:背景色通常用 50-100,边框用 200-300,次要文字用 400-500,主要文字用 700-800。
类型导出的意义
ColorType 和 SizeType 的导出是为了让 TypeScript 能够进行类型检查。当你写 color="primar" 少打一个 y 时,IDE 会立刻标红提示错误,而不是等到运行时才发现按钮颜色不对。这种编译期检查能大大减少低级错误。
三、实现尺寸计算逻辑
不同尺寸的按钮,高度、内边距、字号都不一样。我们用一个对象来集中管理这些数值:
const sizeStyles: Record<SizeType, {
height: number;
paddingHorizontal: number;
fontSize: number
}> = {
sm: { height: 32, paddingHorizontal: 12, fontSize: 12 },
md: { height: 40, paddingHorizontal: 16, fontSize: 14 },
lg: { height: 48, paddingHorizontal: 24, fontSize: 16 },
};
为什么用 Record 而不是普通对象
Record<SizeType, ...> 是 TypeScript 的工具类型,它确保对象必须包含 SizeType 中定义的所有 key。如果你漏写了 lg 的配置,TypeScript 会报错:Property ‘lg’ is missing。这比运行时报 undefined 错误要好得多。
数值选择的依据
高度从 32 到 48,每档增加 8px。32px 是移动端按钮的最小建议高度,再小就不好点击了;48px 是比较大的按钮,适合需要强调的场景。
内边距从 12 到 24,控制按钮的宽度。按钮的宽度 = 文字宽度 + 左右内边距,内边距越大按钮越宽。sm 按钮用 12px 内边距,看起来比较紧凑;lg 按钮用 24px 内边距,看起来更大气。
字号从 12 到 16,保持可读性。12px 是移动端正文的最小建议字号,再小就看不清了;16px 是比较舒适的阅读字号。字号和按钮高度要匹配,小按钮配小字,大按钮配大字,比例才协调。
四、实现按钮容器样式
按钮的容器样式需要根据 variant 参数动态生成。这是整个组件最核心的逻辑:
const getButtonStyle = (): ViewStyle => {
const colorValue = UITheme.colors[color];
const base: ViewStyle = {
height: sizeStyles[size].height,
paddingHorizontal: sizeStyles[size].paddingHorizontal,
borderRadius: UITheme.borderRadius.md,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
opacity: disabled ? 0.5 : 1,
};
if (fullWidth) base.width = '100%';
switch (variant) {
case 'solid':
return { ...base, backgroundColor: colorValue };
case 'outline':
return {
...base,
backgroundColor: 'transparent',
borderWidth: 1.5,
borderColor: colorValue
};
case 'ghost':
return { ...base, backgroundColor: 'transparent' };
default:
return base;
}
};
基础样式的设计
base 对象包含所有变体共有的样式。flexDirection: 'row' 让图标和文字水平排列;alignItems: 'center' 和 justifyContent: 'center' 让内容垂直和水平居中。
borderRadius: UITheme.borderRadius.md 使用主题配置的圆角值(8px),保证和其他组件的圆角一致。如果每个组件都写死自己的圆角值,后期想统一调整就很麻烦。
disabled 状态的处理
disabled 状态直接把 opacity 设为 0.5,这是一个简单但有效的方案。它的好处是对任何颜色的按钮都有效:红色按钮变成半透明的红色,蓝色按钮变成半透明的蓝色,视觉上都能明显看出是禁用状态。
另一种常见方案是把按钮变成灰色,但这需要为每种颜色单独处理,代码会复杂很多。
fullWidth 的实现
if (fullWidth) base.width = '100%' 这行代码让按钮占满父容器宽度。注意 ‘100%’ 是字符串不是数字,React Native 支持百分比字符串作为宽度值。
fullWidth 按钮常用于表单底部的提交按钮,或者弹窗里的确认按钮,让按钮更醒目、更容易点击。
variant 的样式差异
solid 样式设置 backgroundColor: colorValue,用主题色填充整个按钮。
outline 样式设置 backgroundColor: 'transparent' 让背景透明,然后用 borderWidth: 1.5 和 borderColor: colorValue 画一个主题色的边框。边框宽度用 1.5px 而不是 1px,是因为 1px 在高清屏上太细了,看起来像是渲染问题。
ghost 样式只设置 backgroundColor: 'transparent',既没有填充也没有边框,只靠文字颜色来表达按钮的存在。
五、实现文字样式
文字颜色需要根据按钮变体来变化,确保在任何背景上都清晰可读:
const getTextStyle = (): TextStyle => {
const colorValue = UITheme.colors[color];
const base: TextStyle = {
fontSize: sizeStyles[size].fontSize,
fontWeight: '600',
};
switch (variant) {
case 'solid':
return { ...base, color: UITheme.colors.white };
case 'outline':
case 'ghost':
return { ...base, color: colorValue };
default:
return base;
}
};
fontWeight 的选择
fontWeight 设为 ‘600’ 是半粗体(semibold),介于普通字重(400/regular)和粗体(700/bold)之间。按钮文字用半粗体是业界通行的做法,因为:
- 比普通字重更醒目,能吸引用户注意
- 比粗体更轻盈,不会显得笨重
- 在小字号下仍然清晰可读
颜色对比度的考量
solid 按钮用白色文字,因为主题色(primary/success/danger 等)都是中等明度的颜色,白色文字在这些背景上有足够的对比度。
outline 和 ghost 按钮用主题色文字,因为它们的背景是透明的(通常是白色或浅灰色页面背景),主题色文字在浅色背景上同样有足够的对比度。
这种设计确保了无论哪种变体,文字都清晰可读,符合 WCAG 无障碍标准。
六、组装完整的渲染逻辑
最后把所有部分组装成完整的组件:
export const Button: React.FC<ButtonProps> = ({
title,
onPress,
variant = 'solid',
color = 'primary',
size = 'md',
disabled = false,
loading = false,
icon,
fullWidth = false,
style,
}) => {
const colorValue = UITheme.colors[color];
return (
<TouchableOpacity
style={[getButtonStyle(), style]}
onPress={onPress}
disabled={disabled || loading}
activeOpacity={0.7}
>
{loading ? (
<ActivityIndicator
size="small"
color={variant === 'solid' ? UITheme.colors.white : colorValue}
/>
) : (
<>
{icon && <Text style={{ marginRight: 6 }}>{icon}</Text>}
<Text style={getTextStyle()}>{title}</Text>
</>
)}
</TouchableOpacity>
);
};
参数默认值的设置
所有可选参数都设置了默认值:variant 默认 ‘solid’,color 默认 ‘primary’,size 默认 ‘md’。这意味着最简单的用法只需要传 title 和 onPress:
<Button title="点击我" onPress={handleClick} />
这个按钮会是一个中等大小、主色调、实心填充的按钮,符合大多数场景的需求。
style 的合并方式
style={[getButtonStyle(), style]} 把计算出的样式和外部传入的 style 合并成数组。React Native 会按顺序应用样式,后面的会覆盖前面的。
这意味着外部传入的 style 优先级更高,可以覆盖组件内部的样式。比如你想给某个按钮加个 marginTop,直接传 style={{ marginTop: 10 }} 就行,不需要修改组件代码。
disabled 的双重判断
disabled={disabled || loading} 这行代码确保 loading 状态下按钮也不可点击。这是防止重复提交的关键:用户点击按钮后,按钮进入 loading 状态,此时再点击不会触发 onPress。
activeOpacity 的作用
activeOpacity 控制按下时的透明度。TouchableOpacity 的默认值是 0.2,按下去按钮会变得很透明,视觉上不太舒服。设为 0.7 后,按下时只是稍微变暗一点,反馈明显但不突兀。
loading 指示器的颜色适配
ActivityIndicator 的颜色需要根据按钮变体来设置:solid 按钮背景是深色的,用白色指示器;outline 和 ghost 按钮背景是透明的,用主题色指示器。这样无论哪种变体,loading 指示器都清晰可见。
图标的实现方式
用 emoji 作为图标是最简单的方案,不需要引入任何图标库。marginRight: 6 让图标和文字之间有 6px 的间距,视觉上不会太挤。
如果需要更专业的图标,可以把 icon 属性的类型从 string 改成 ReactNode,然后传入 react-native-vector-icons 或其他图标库的组件。
七、完整的使用示例
组件封装好了,来看看各种场景下怎么使用:
import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import { Button } from './components/ui/Button';
const ButtonDemo = () => {
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
setLoading(true);
await submitForm();
setLoading(false);
};
return (
<View style={styles.container}>
{/* 主要操作和次要操作并排 */}
<View style={styles.row}>
<Button
title="确认"
color="primary"
onPress={handleConfirm}
/>
<Button
title="取消"
variant="outline"
onPress={handleCancel}
style={styles.ml}
/>
</View>
{/* 危险操作用红色警示 */}
<Button
title="删除账户"
icon="🗑️"
color="danger"
onPress={handleDelete}
/>
{/* 异步操作显示 loading */}
<Button
title={loading ? "提交中..." : "提交"}
loading={loading}
fullWidth
onPress={handleSubmit}
/>
{/* 表单未填完时禁用提交按钮 */}
<Button
title="下一步"
disabled={!formValid}
fullWidth
onPress={handleNext}
/>
{/* 工具栏用小号幽灵按钮 */}
<View style={styles.toolbar}>
<Button title="编辑" variant="ghost" size="sm" onPress={handleEdit} />
<Button title="复制" variant="ghost" size="sm" onPress={handleCopy} />
<Button title="分享" variant="ghost" size="sm" onPress={handleShare} />
</View>
</View>
);
};
const styles = StyleSheet.create({
container: { padding: 16, gap: 16 },
row: { flexDirection: 'row' },
ml: { marginLeft: 8 },
toolbar: { flexDirection: 'row', justifyContent: 'space-around' },
});
每种场景选择不同的 variant、color、size 组合,能让界面的层次感更清晰。用户一眼就能分辨哪些是主要操作、哪些是次要操作、哪些是危险操作。
八、进阶优化方向
这个 Button 组件已经能满足大部分需求。如果想进一步完善,可以考虑以下方向:
增加按压动画:用 Animated API 实现按下时轻微缩放的效果,提升交互体验。
支持图标位置:增加 iconPosition 属性,支持图标在文字左边或右边。
支持全圆角:增加 rounded 属性,设为 true 时按钮变成胶囊形状。
支持渐变背景:用 react-native-linear-gradient 实现渐变色按钮。
支持涟漪效果:在 OpenHarmony 上可以通过自定义动画实现涟漪效果,增强用户交互体验。
这些优化都可以在现有代码基础上增量添加,不需要重构核心逻辑。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)