【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:ActionSheet 动作面板(底部弹起的模态面板)
React Native与HarmonyOS集成开发摘要 本文介绍了在React Native中开发鸿蒙应用的流程,重点展示了动作面板组件的实现方案。开发步骤包括:1) 配置环境并安装华为SDK;2) 通过@hms-react-native/hms-plugin集成鸿蒙服务;3) 使用华为特有组件如HMSActionSheet。文中提供了完整的自定义ActionSheet组件代码,包含图标渲染、动
在React Native中开发针对华为鸿蒙操作系统(HarmonyOS)的应用时,通常需要使用华为的HarmonyOS SDK和开发工具。华为提供了专门的API和组件库,用以支持在HarmonyOS设备上开发应用。对于使用React Native开发的应用,如果你想集成华为特有的功能或者使用特定的HarmonyOS组件,你可以通过以下步骤进行:
- 设置开发环境
确保你的开发环境已经安装了React Native CLI、Node.js以及华为的HarmonyOS SDK。
- 配置项目
-
安装华为移动服务SDK:在你的React Native项目中,可以通过npm或yarn安装华为移动服务SDK。
npm install @hms-react-native/hms-plugin -
配置OpenHarmony项目:在你的
OpenHarmony/app/build.gradle文件中添加华为的依赖和配置。dependencies { implementation 'com.huawei.hms:base:5.1.0.300' // 其他必要的依赖 }
- 使用HarmonyOS特定组件
-
动作面板(ActionSheet):华为提供了一个名为
HMSActionSheet的组件,用于实现类似iOS的ActionSheet功能。-
首先,确保在你的项目中已经正确集成了华为移动服务SDK。
-
然后,你可以通过以下方式使用
HMSActionSheet:import { HMSActionSheet } from '@hms-react-native/hms-plugin'; const showActionSheet = () => { HMSActionSheet.show({ options: ['选项1', '选项2', '取消'], cancelButtonIndex: 2, // 取消按钮的索引,从0开始计数 title: '请选择', // 可选标题 }, (buttonIndex) => { console.log('你选择了: ', buttonIndex); // 输出选择的索引 }); };
-
- 测试和调试
- 在实际设备或使用华为提供的模拟器进行测试,确保所有功能正常工作。
- 使用华为的开发者工具和日志系统来调试应用。
- 发布应用
- 在应用准备好后,使用华为的HarmonyOS AppGallery Connect进行应用的发布和分发。
通过上述步骤,你可以在React Native应用中集成并使用HarmonyOS特有的功能,如动作面板等。确保遵循华为的官方文档和指南,以获取最新的API信息和最佳实践。同时,注意检查React Native社区和华为开发者论坛,以获取更多关于React Native与HarmonyOS集成的最新信息和帮助。
真实项目组件案列场景:
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, TouchableOpacity, Modal, Animated, Easing } from 'react-native';
// Simple Icon Component using Unicode symbols
interface IconProps {
name: string;
size?: number;
color?: string;
style?: object;
}
const Icon: React.FC<IconProps> = ({
name,
size = 24,
color = '#333333',
style
}) => {
const getIconSymbol = () => {
switch (name) {
case 'share': return '📤';
case 'delete': return '🗑️';
case 'edit': return '✏️';
case 'copy': return '📋';
case 'download': return '📥';
case 'favorite': return '❤️';
case 'more': return '⋯';
case 'camera': return '📷';
case 'gallery': return '🖼️';
case 'document': return '📄';
case 'link': return '🔗';
case 'qr': return '🔲';
case 'settings': return '⚙️';
case 'info': return 'ℹ️';
case 'close': return '✕';
default: return '⋯';
}
};
return (
<View style={[{ width: size, height: size, justifyContent: 'center', alignItems: 'center' }, style]}>
<Text style={{ fontSize: size * 0.8, color, includeFontPadding: false, textAlign: 'center' }}>
{getIconSymbol()}
</Text>
</View>
);
};
// Action Sheet Component
interface ActionSheetProps {
visible: boolean;
onClose: () => void;
actions: {
title: string;
icon?: string;
color?: string;
disabled?: boolean;
destructive?: boolean;
}[];
title?: string;
message?: string;
cancelText?: string;
}
const ActionSheet: React.FC<ActionSheetProps> = ({
visible,
onClose,
actions,
title,
message,
cancelText = '取消'
}) => {
const [slideAnim] = useState(new Animated.Value(Dimensions.get('window').height));
React.useEffect(() => {
if (visible) {
Animated.timing(slideAnim, {
toValue: 0,
duration: 300,
easing: Easing.out(Easing.ease),
useNativeDriver: true
}).start();
} else {
Animated.timing(slideAnim, {
toValue: Dimensions.get('window').height,
duration: 250,
useNativeDriver: true
}).start();
}
}, [visible]);
const handleActionPress = (index: number) => {
if (actions[index].disabled) return;
onClose();
// In a real app, you would call the action's onPress handler here
};
return (
<Modal
visible={visible}
transparent
animationType="none"
onRequestClose={onClose}
>
<TouchableOpacity
style={styles.overlay}
onPress={onClose}
activeOpacity={1}
>
<Animated.View
style={[
styles.actionSheetContainer,
{ transform: [{ translateY: slideAnim }] }
]}
>
{title && (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
{message && <Text style={styles.message}>{message}</Text>}
</View>
)}
<View style={styles.actionsContainer}>
{actions.map((action, index) => (
<TouchableOpacity
key={index}
style={[
styles.actionItem,
action.destructive && styles.destructiveAction,
action.disabled && styles.disabledAction
]}
onPress={() => handleActionPress(index)}
disabled={action.disabled}
activeOpacity={action.disabled ? 1 : 0.7}
>
{action.icon && (
<Icon
name={action.icon}
size={24}
color={
action.disabled
? '#cccccc'
: action.destructive
? '#ff4d4f'
: action.color || '#1890ff'
}
style={styles.actionIcon}
/>
)}
<Text
style={[
styles.actionText,
action.destructive && styles.destructiveText,
action.disabled && styles.disabledText,
{ color: action.color || (action.destructive ? '#ff4d4f' : '#262626') }
]}
>
{action.title}
</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.separator} />
<TouchableOpacity
style={styles.cancelButton}
onPress={onClose}
>
<Text style={styles.cancelText}>{cancelText}</Text>
</TouchableOpacity>
</Animated.View>
</TouchableOpacity>
</Modal>
);
};
// Main App Component
const ActionSheetComponentApp = () => {
const [basicVisible, setBasicVisible] = useState(false);
const [withTitleVisible, setWithTitleVisible] = useState(false);
const [destructiveVisible, setDestructiveVisible] = useState(false);
const [iconVisible, setIconVisible] = useState(false);
const basicActions = [
{ title: '选项一' },
{ title: '选项二' },
{ title: '选项三' },
{ title: '选项四' }
];
const titledActions = [
{ title: '分享到朋友圈', icon: 'share' },
{ title: '发送给朋友', icon: 'send' },
{ title: '复制链接', icon: 'copy' },
{ title: '二维码', icon: 'qr' }
];
const destructiveActions = [
{ title: '编辑', icon: 'edit' },
{ title: '收藏', icon: 'favorite' },
{ title: '下载', icon: 'download' },
{ title: '删除', icon: 'delete', destructive: true }
];
const iconActions = [
{ title: '拍照', icon: 'camera', color: '#1890ff' },
{ title: '从相册选择', icon: 'gallery', color: '#52c41a' },
{ title: '新建文档', icon: 'document', color: '#722ed1' },
{ title: '链接分享', icon: 'link', color: '#fa8c16' },
{ title: '更多', icon: 'more', color: '#13c2c2' }
];
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.headerTitle}>动作面板组件</Text>
<Text style={styles.headerSubtitle}>美观实用的操作菜单控件</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>基础用法</Text>
<View style={styles.actionGroupsContainer}>
<TouchableOpacity
style={styles.actionButton}
onPress={() => setBasicVisible(true)}
>
<Text style={styles.actionButtonText}>显示基础动作面板</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => setWithTitleVisible(true)}
>
<Text style={styles.actionButtonText}>带标题的动作面板</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => setDestructiveVisible(true)}
>
<Text style={styles.actionButtonText}>带危险操作</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => setIconVisible(true)}
>
<Text style={styles.actionButtonText}>带图标的动作面板</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>应用场景</Text>
<View style={styles.scenariosContainer}>
<View style={styles.scenarioCard}>
<Icon name="share" size={32} color="#1890ff" style={styles.scenarioIcon} />
<Text style={styles.scenarioTitle}>分享操作</Text>
<Text style={styles.scenarioDesc}>社交分享菜单</Text>
</View>
<View style={styles.scenarioCard}>
<Icon name="edit" size={32} color="#52c41a" style={styles.scenarioIcon} />
<Text style={styles.scenarioTitle}>编辑操作</Text>
<Text style={styles.scenarioDesc}>内容编辑选项</Text>
</View>
<View style={styles.scenarioCard}>
<Icon name="delete" size={32} color="#ff4d4f" style={styles.scenarioIcon} />
<Text style={styles.scenarioTitle}>删除操作</Text>
<Text style={styles.scenarioDesc}>危险操作确认</Text>
</View>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>功能演示</Text>
<View style={styles.demosContainer}>
<View style={styles.demoItem}>
<Icon name="more" size={24} color="#1890ff" style={styles.demoIcon} />
<View>
<Text style={styles.demoTitle}>底部弹出</Text>
<Text style={styles.demoDesc}>从屏幕底部滑出的操作面板</Text>
</View>
</View>
<View style={styles.demoItem}>
<Icon name="share" size={24} color="#52c41a" style={styles.demoIcon} />
<View>
<Text style={styles.demoTitle}>图标支持</Text>
<Text style={styles.demoDesc}>支持自定义图标和颜色</Text>
</View>
</View>
<View style={styles.demoItem}>
<Icon name="delete" size={24} color="#ff4d4f" style={styles.demoIcon} />
<View>
<Text style={styles.demoTitle}>危险操作</Text>
<Text style={styles.demoDesc}>特殊标识危险操作项</Text>
</View>
</View>
</View>
</View>
<View style={styles.usageSection}>
<Text style={styles.sectionTitle}>使用方法</Text>
<View style={styles.codeBlock}>
<Text style={styles.codeText}>{'<ActionSheet'}</Text>
<Text style={styles.codeText}> visible={'{isVisible}'}</Text>
<Text style={styles.codeText}> onClose={'{setVisible}'}</Text>
<Text style={styles.codeText}> actions={'{actions}'}</Text>
<Text style={styles.codeText}>{'/>'}</Text>
</View>
<Text style={styles.description}>
ActionSheet组件提供了完整的动作面板功能,包括底部弹出、图标支持、危险操作标识等。
通过visible控制显示状态,onClose处理关闭事件,actions定义操作项。
</Text>
</View>
<View style={styles.featuresSection}>
<Text style={styles.sectionTitle}>功能特性</Text>
<View style={styles.featuresList}>
<View style={styles.featureItem}>
<Icon name="more" size={20} color="#1890ff" style={styles.featureIcon} />
<Text style={styles.featureText}>底部弹出</Text>
</View>
<View style={styles.featureItem}>
<Icon name="share" size={20} color="#52c41a" style={styles.featureIcon} />
<Text style={styles.featureText}>图标支持</Text>
</View>
<View style={styles.featureItem}>
<Icon name="delete" size={20} color="#ff4d4f" style={styles.featureIcon} />
<Text style={styles.featureText}>危险操作</Text>
</View>
<View style={styles.featureItem}>
<Icon name="settings" size={20} color="#722ed1" style={styles.featureIcon} />
<Text style={styles.featureText}>状态控制</Text>
</View>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>© 2023 动作面板组件 | 现代化UI组件库</Text>
</View>
{/* Action Sheets */}
<ActionSheet
visible={basicVisible}
onClose={() => setBasicVisible(false)}
actions={basicActions}
/>
<ActionSheet
visible={withTitleVisible}
onClose={() => setWithTitleVisible(false)}
actions={titledActions}
title="分享到"
message="选择分享方式"
/>
<ActionSheet
visible={destructiveVisible}
onClose={() => setDestructiveVisible(false)}
actions={destructiveActions}
title="操作选项"
/>
<ActionSheet
visible={iconVisible}
onClose={() => setIconVisible(false)}
actions={iconActions}
title="选择操作"
cancelText="取消操作"
/>
</ScrollView>
);
};
const { width, height } = Dimensions.get('window');
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
backgroundColor: '#ffffff',
paddingVertical: 30,
paddingHorizontal: 20,
marginBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#e8e8e8',
},
headerTitle: {
fontSize: 28,
fontWeight: '700',
color: '#262626',
textAlign: 'center',
marginBottom: 5,
},
headerSubtitle: {
fontSize: 16,
color: '#8c8c8c',
textAlign: 'center',
},
section: {
marginBottom: 25,
},
sectionTitle: {
fontSize: 20,
fontWeight: '700',
color: '#262626',
paddingHorizontal: 20,
paddingBottom: 15,
},
actionGroupsContainer: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 12,
padding: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
marginBottom: 10,
},
actionButton: {
backgroundColor: '#f0f5ff',
borderRadius: 8,
paddingVertical: 15,
paddingHorizontal: 20,
marginBottom: 15,
borderWidth: 1,
borderColor: '#d9e6ff',
},
actionButtonLast: {
marginBottom: 0,
},
actionButtonText: {
fontSize: 16,
color: '#1890ff',
fontWeight: '500',
textAlign: 'center',
},
scenariosContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingHorizontal: 15,
},
scenarioCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
width: (width - 60) / 3,
alignItems: 'center',
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
scenarioIcon: {
marginBottom: 15,
},
scenarioTitle: {
fontSize: 16,
fontWeight: '600',
color: '#262626',
marginBottom: 5,
},
scenarioDesc: {
fontSize: 14,
color: '#8c8c8c',
textAlign: 'center',
},
demosContainer: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 15,
padding: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
demoItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
},
demoItemLast: {
marginBottom: 0,
},
demoIcon: {
marginRight: 15,
},
demoTitle: {
fontSize: 16,
fontWeight: '600',
color: '#262626',
marginBottom: 3,
},
demoDesc: {
fontSize: 14,
color: '#8c8c8c',
},
usageSection: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 15,
padding: 20,
marginBottom: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
codeBlock: {
backgroundColor: '#2d3748',
borderRadius: 8,
padding: 15,
marginBottom: 15,
},
codeText: {
fontFamily: 'monospace',
color: '#e2e8f0',
fontSize: 14,
lineHeight: 22,
},
description: {
fontSize: 15,
color: '#595959',
lineHeight: 22,
},
featuresSection: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 15,
padding: 20,
marginBottom: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
featuresList: {
paddingLeft: 10,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 15,
},
featureIcon: {
marginRight: 15,
},
featureText: {
fontSize: 16,
color: '#262626',
},
footer: {
paddingVertical: 20,
alignItems: 'center',
},
footerText: {
color: '#bfbfbf',
fontSize: 14,
},
// Action Sheet Styles
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
actionSheetContainer: {
backgroundColor: '#ffffff',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: height * 0.7,
paddingTop: 15,
},
header: {
paddingHorizontal: 20,
paddingBottom: 15,
alignItems: 'center',
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#262626',
marginBottom: 5,
},
message: {
fontSize: 14,
color: '#8c8c8c',
},
actionsContainer: {
paddingHorizontal: 10,
},
actionItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 15,
paddingHorizontal: 20,
},
destructiveAction: {
backgroundColor: '#fff1f0',
},
disabledAction: {
opacity: 0.5,
},
actionIcon: {
marginRight: 15,
},
actionText: {
fontSize: 18,
color: '#262626',
fontWeight: '400',
},
destructiveText: {
color: '#ff4d4f',
fontWeight: '500',
},
disabledText: {
color: '#cccccc',
},
separator: {
height: 10,
backgroundColor: '#f5f5f5',
},
cancelButton: {
paddingVertical: 18,
alignItems: 'center',
},
cancelText: {
fontSize: 18,
color: '#1890ff',
fontWeight: '500',
},
});
export default ActionSheetComponentApp;
从鸿蒙ArkUI开发角度分析,这段React Native ActionSheet组件的实现逻辑体现了鸿蒙模态交互的核心设计理念。ActionSheet作为底部弹出的操作菜单,在鸿蒙中对应着CustomDialogController的实现模式,通过@State装饰器管理可见性状态,当visible属性变更时触发动画效果。
Icon组件通过Unicode符号映射实现图标显示,这与鸿蒙的Symbol组件设计思路一致。在鸿蒙中,图标资源通过ResourceManager统一管理,支持多分辨率适配和主题切换。getIconSymbol方法中的switch-case逻辑对应鸿蒙的图标资源映射机制。
动画系统采用Animated API实现平移动画,slideAnim通过useState初始化为屏幕高度,在useEffect中根据visible状态触发不同的动画序列。这与鸿蒙的animateTo方法对应,支持duration、easing等动画参数配置。useNativeDriver设置为true对应鸿蒙的动画硬件加速优化。

组件结构采用Modal作为容器,实现透明背景和独立渲染层级。TouchableOpacity作为交互元素,通过activeOpacity控制点击反馈效果,这与鸿蒙的Button组件点击态设计相似。overlay区域点击关闭的功能对应鸿蒙的onClick事件处理。
actions数组的渲染采用map方法遍历,每个actionItem支持icon、title、color、disabled、destructive等属性配置。状态控制逻辑通过条件样式实现,disabled状态时降低透明度并阻止点击事件,destructive状态时显示警告色。
布局系统采用Flexbox实现垂直排列,header区域条件渲染标题和消息,actionsContainer承载操作列表,separator作为视觉分隔,cancelButton提供取消操作。这种结构与鸿蒙的Column容器布局完全对应。
事件处理机制通过handleActionPress函数实现,当action被点击时首先检查disabled状态,然后调用onClose关闭菜单并触发对应的业务逻辑。这种设计模式在鸿蒙应用中广泛用于分享、设置、删除确认等场景。
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

更多推荐




所有评论(0)