商城App标签选择组件开发,如何React Native鸿蒙跨平台开发`react-native-tags`是一个流行的React Native库,用于实现标签选择功能
本文介绍了在React Native中实现商城App标签选择组件的多种方法。主要包括使用原生组件(TouchableOpacity和FlatList)的基本实现,以及推荐使用react-native-tags等第三方库来简化开发。文章提供了详细的代码示例,包括标签选择、样式定制和交互逻辑处理。此外还展示了一个完整的真实案例,演示了如何实现带图标的多选标签功能,包含全选/清空操作和状态管理。这些方案
在React Native中开发一个商城App的标签选择组件,你可以使用多种方法。这里将介绍几种常用的方法来实现标签选择功能,包括使用原生组件和第三方库。
方法1:使用原生组件
- 使用
<TouchableOpacity>和<View>
这是最基本的实现方式,适合简单的标签选择。
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
const TagSelector = ({ tags, onSelect }) => (
<View style={styles.container}>
{tags.map((tag, index) => (
<TouchableOpacity
key={index}
style={[styles.tag, { backgroundColor: tag.selected ? '007AFF' : 'E0E0E0' }]}
onPress={() => onSelect(tag)}>
<Text style={styles.tagText}>{tag.name}</Text>
</TouchableOpacity>
))}
</View>
);
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
flexWrap: 'wrap',
padding: 10,
},
tag: {
margin: 5,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 15,
},
tagText: {
color: '000',
}
});
export default TagSelector;
- 使用
<FlatList>优化性能
如果你有大量的标签,使用<FlatList>可以提高性能。
import React from 'react';
import { FlatList, TouchableOpacity, Text, StyleSheet } from 'react-native';
const TagSelector = ({ tags, onSelect }) => (
<FlatList
data={tags}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.tag, { backgroundColor: item.selected ? '007AFF' : 'E0E0E0' }]}
onPress={() => onSelect(item)}>
<Text style={styles.tagText}>{item.name}</Text>
</TouchableOpacity>
)}
contentContainerStyle={styles.container}
horizontal={true} // 如果需要水平滚动显示标签
/>
);
const styles = StyleSheet.create({
container: { paddingHorizontal: 10 },
tag: { marginRight: 10, padding: 10, borderRadius: 15 },
tagText: { color: '000' }
});
方法2:使用第三方库
使用react-native-tags库
react-native-tags是一个流行的React Native库,用于实现标签选择功能。首先,你需要安装这个库:
npm install react-native-tags --save
或者使用yarn:
yarn add react-native-tags
然后,你可以这样使用它:
import React from 'react';
import { Tags } from 'react-native-tags';
import { View } from 'react-native';
const TagSelector = ({ tags, onSelect }) => (
<View>
<Tags
initialTags={tags}
onChange={onSelect}
textStyle={{ fontSize: 16 }}
containerStyle={{ flexWrap: 'wrap' }}
onTagPress={(index) => console.log(`Tag pressed was ${index}`)}
/>
</View>
);
方法3:自定义样式和交互逻辑更复杂的组件可以使用react-native-tag-selector等库。这些库通常提供了更多定制化的选项和更复杂的交互逻辑。例如:使用react-native-tag-selector:首先安装:bashnpm install react-native-tag-selector --save然后使用:```javascriptimport React from ‘react’;import { TagSelector } from ‘react-native-tag-selector’;const App = () => { const handleSelect = (selectedTags) => { console.log(selectedTags); }; return ( <TagSelector tags={[‘Tag1’, ‘Tag2’, ‘Tag3’]} onChange={handleSelect} initialSelected
---
## 真实案例项目代码演示:
```js
// app.tsx
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Image } from 'react-native';
const App = () => {
const [selectedTags, setSelectedTags] = useState<string[]>([]);
// 标签数据
const tags = [
{ id: '1', name: '手机数码', icon: '📱' },
{ id: '2', name: '家用电器', icon: '📺' },
{ id: '3', name: '服装鞋帽', icon: '👕' },
{ id: '4', name: '美妆护肤', icon: '💄' },
{ id: '5', name: '家居用品', icon: '🏠' },
{ id: '6', name: '运动户外', icon: '⚽' },
{ id: '7', name: '图书音像', icon: '📚' },
{ id: '8', name: '食品饮料', icon: '🍎' },
{ id: '9', name: '母婴用品', icon: '👶' },
{ id: '10', name: '汽车配件', icon: '🚗' },
{ id: '11', name: '珠宝首饰', icon: '💍' },
{ id: '12', name: '办公用品', icon: '📎' }
];
// Base64图标
const icons = {
check: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWxpbmUgcG9pbnRzPSIyMCA2IDkgMTcgNCAxMiI+PC9wb2x5bGluZT48L3N2Zz4=',
uncheck: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNEN0Q4REEiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCI+PC9jaXJjbGU+PC9zdmc+'
};
const toggleTag = (tagId: string) => {
setSelectedTags(prev => {
if (prev.includes(tagId)) {
return prev.filter(id => id !== tagId);
} else {
return [...prev, tagId];
}
});
};
const selectAll = () => {
setSelectedTags(tags.map(tag => tag.id));
};
const clearAll = () => {
setSelectedTags([]);
};
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>商品标签选择</Text>
<Text style={styles.subtitle}>请选择您感兴趣的商品类别</Text>
</View>
<View style={styles.selectionControls}>
<TouchableOpacity style={styles.controlButton} onPress={selectAll}>
<Text style={styles.controlButtonText}>全选</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.controlButton} onPress={clearAll}>
<Text style={styles.controlButtonText}>清空</Text>
</TouchableOpacity>
</View>
<View style={styles.tagContainer}>
{tags.map(tag => {
const isSelected = selectedTags.includes(tag.id);
return (
<TouchableOpacity
key={tag.id}
style={[
styles.tag,
isSelected && styles.selectedTag
]}
onPress={() => toggleTag(tag.id)}
>
<View style={styles.tagContent}>
<Text style={styles.tagIcon}>{tag.icon}</Text>
<Text style={[
styles.tagName,
isSelected && styles.selectedTagName
]}>
{tag.name}
</Text>
</View>
<View style={styles.tagIndicator}>
{isSelected ? (
<Image source={{ uri: icons.check }} style={styles.indicatorIcon} />
) : (
<Image source={{ uri: icons.uncheck }} style={styles.indicatorIcon} />
)}
</View>
</TouchableOpacity>
);
})}
</View>
<View style={styles.resultContainer}>
<Text style={styles.resultTitle}>已选择 ({selectedTags.length})</Text>
{selectedTags.length > 0 ? (
<View style={styles.selectedTagsContainer}>
{selectedTags.map(tagId => {
const tag = tags.find(t => t.id === tagId);
return tag ? (
<View key={tagId} style={styles.selectedTagItem}>
<Text style={styles.selectedTagText}>{tag.icon} {tag.name}</Text>
</View>
) : null;
})}
</View>
) : (
<Text style={styles.noSelectionText}>暂未选择任何标签</Text>
)}
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8f9fa',
padding: 20
},
header: {
alignItems: 'center',
marginBottom: 25,
paddingTop: 20
},
title: {
fontSize: 26,
fontWeight: 'bold',
color: '#2c3e50'
},
subtitle: {
fontSize: 15,
color: '#7f8c8d',
marginTop: 6
},
selectionControls: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 20
},
controlButton: {
backgroundColor: '#4285F4',
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: 25
},
controlButtonText: {
color: '#fff',
fontWeight: '600',
fontSize: 15
},
tagContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between'
},
tag: {
width: '48%',
backgroundColor: '#fff',
borderRadius: 14,
padding: 16,
marginBottom: 15,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4
},
selectedTag: {
backgroundColor: '#e8f0fe',
borderColor: '#4285F4',
borderWidth: 1
},
tagContent: {
flexDirection: 'row',
alignItems: 'center'
},
tagIcon: {
fontSize: 20,
marginRight: 10
},
tagName: {
fontSize: 16,
color: '#34495e',
fontWeight: '500'
},
selectedTagName: {
color: '#4285F4',
fontWeight: '600'
},
tagIndicator: {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center'
},
indicatorIcon: {
width: 20,
height: 20
},
resultContainer: {
backgroundColor: '#fff',
borderRadius: 16,
padding: 20,
marginTop: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4
},
resultTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 15
},
selectedTagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap'
},
selectedTagItem: {
backgroundColor: '#4285F4',
borderRadius: 20,
paddingHorizontal: 15,
paddingVertical: 8,
margin: 5
},
selectedTagText: {
color: '#fff',
fontSize: 14
},
noSelectionText: {
color: '#95a5a6',
fontStyle: 'italic',
textAlign: 'center',
paddingVertical: 15
}
});
export default App;
这段React Native代码实现了一个商品标签选择功能,其核心原理基于React的状态管理和事件处理机制。toggleTag函数作为标签切换的核心逻辑,通过函数式更新确保状态变更的准确性和可预测性。该函数使用prev参数获取先前的selectedTags状态,然后根据当前标签是否已选中来决定是添加还是移除标签ID,这种实现方式避免了闭包陷阱,确保在并发更新场景下的状态一致性。
从鸿蒙系统适配的角度来看,该代码充分利用了React Native的跨平台特性,在鸿蒙设备上能够获得原生级的性能表现。鸿蒙系统的分布式架构强调组件间的低耦合和高内聚,而React的单向数据流和状态提升概念与这一理念高度契合。selectedTags状态作为单一数据源,通过props向下传递给各个子组件,确保了数据的一致性,这在鸿蒙系统的多设备协同场景中尤为重要。
标签选择功能的实现考虑了鸿蒙系统的触摸交互特性。TouchableOpacity组件提供了内置的触摸反馈效果,这种交互设计符合鸿蒙系统的UI规范。在鸿蒙设备上,触摸事件的处理经过了系统层面的优化,能够提供流畅的响应体验。toggleTag函数中的数组操作使用了不可变数据模式,通过扩展运算符和filter方法创建新数组,而不是直接修改原数组,这种做法在鸿蒙系统的状态管理中能够更好地配合其响应式更新机制。
全选和清空功能的实现体现了对用户操作效率的考量。selectAll函数通过map方法一次性获取所有标签ID,clearAll函数通过设置空数组实现状态重置,这两种操作在鸿蒙系统的批量数据处理场景中能够提供良好的性能表现。鸿蒙系统强调高效的任务处理能力,这种设计能够减少不必要的状态更新次数,提升应用响应速度。
UI渲染逻辑通过map方法遍历标签数组,动态生成标签组件。每个标签组件根据isSelected状态应用不同的样式,这种条件渲染模式在鸿蒙系统的动态UI构建中非常常见。标签组件内部采用了水平布局,左侧显示图标和名称,右侧显示选中状态指示器,这种设计在鸿蒙系统的不同屏幕尺寸设备上都能保持良好的视觉效果。
结果展示区域实时显示已选择的标签数量和具体标签内容,通过find方法从原始标签数组中获取选中标签的详细信息。这种数据处理方式在鸿蒙系统的数据同步机制中具有优势,能够确保展示数据与选择状态的一致性。当没有标签被选中时,显示友好的提示信息,体现了良好的用户体验设计。
鸿蒙系统的内存管理机制强调对象的复用和及时释放,代码中通过条件渲染和短路求值等方式避免了不必要的组件创建和销毁。标签组件的key属性使用标签ID,确保了React的diff算法能够正确识别组件身份,在鸿蒙设备的高性能渲染需求下能够提供稳定的性能表现。
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

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

最后运行效果图如下显示:
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
在React Native中开发一个运动App的步数计数器组件,你可以使用多种方法来实现这一功能。以下是几个步骤和示例代码,帮助你开始开发:
- 安装必要的库
首先,你需要安装一些库来帮助你处理设备的步数数据。最常用的是react-native-sensors库,它可以让你访问设备的传感器数据,包括步数传感器。
npm install react-native-sensors
- 配置权限
在你的Harmony/app/src/main/HarmonyManifest.xml和Harmony/项目名/Info.plist中添加必要的权限。
Harmony:
<uses-permission Harmony:name="Harmony.permission.ACTIVITY_RECOGNITION" />
Harmony:
对于Harmony,你可能需要使用CoreMotion框架,并且确保你的App有使用传感器数据的权限。在Info.plist中添加以下键值对:
<key>NSMotionUsageDescription</key>
<string>Your usage description</string>
- 使用
react-native-sensors获取步数数据
在你的React Native组件中,你可以使用react-native-sensors来订阅步数数据。
import React, { useEffect, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import Sensors from 'react-native-sensors';
const StepCounter = () => {
const [steps, setSteps] = useState(0);
const [error, setError] = useState(null);
const [isSubscribed, setIsSubscribed] = useState(false);
useEffect(() => {
const subscribe = () => {
Sensors.startStepCounter((err, step) => {
if (err) {
setError(err);
} else {
setSteps(step);
}
});
setIsSubscribed(true);
};
const unsubscribe = () => {
Sensors.stopStepCounter();
setIsSubscribed(false);
};
subscribe(); // 启动步数计数器监听
return () => unsubscribe(); // 清理操作,停止监听
}, []); // 空依赖数组确保只运行一次
if (error) {
return <Text>Error: {error.message}</Text>;
}
return (
<View style={styles.container}>
<Text>Steps: {steps}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
export default StepCounter;
- 测试你的应用
现在你可以在模拟器或真机上测试你的应用了。确保你的设备支持步数传感器的使用,并且你已经正确配置了所有必要的权限。
- 优化和扩展功能(可选)
你可以根据需要添加更多功能,如步数历史记录、步数目标设置、实时更新UI等。例如,你可以使用react-native-step-by-step库来获取更详细的步行数据。此外,你还可以使用redux或react-query来管理状态,以便在应用的不同部分之间共享步数数据。
npm install react-native-step-by-step redux react-redux react-query @reduxjs/toolkit --save
通过以上步骤,你应该能够成功地在React Native中开发一个运动App的步数计数器组件。
// app.tsx
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Image, Alert } from 'react-native';
const App = () => {
const [steps, setSteps] = useState(0);
const [isCounting, setIsCounting] = useState(false);
const [startTime, setStartTime] = useState<number | null>(null);
const [elapsedTime, setElapsedTime] = useState(0);
const [calories, setCalories] = useState(0);
const [distance, setDistance] = useState(0);
// Base64 icons
const icons = {
play: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cG9seWdvbiBwb2ludHM9IjUgMyAyMCAxMiA1IDIxIDUgMyI+PC9wb2x5Z29uPjwvc3ZnPg==',
pause: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cmVjdCB4PSI2IiB5PSI0IiB3aWR0aD0iNCIgaGVpZ2h0PSIxNiIgcng9IjEiIHJ5PSIxIj48L3JlY3Q+PHJlY3QgeD0iMTQiIHk9IjQiIHdpZHRoPSI0IiBoZWlnaHQ9IjE2IiByeD0iMSIgcnk9IjEiPjwvcmVjdD48L3N2Zz4=',
reset: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM5OTkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMyAxMWE5IDkgMCAwIDEgOS05YzIuNjEgMCA0LjkxIDEuMDQgNi43IDEuODIiPjwvcGF0aD48cGF0aCBkPSJNMjEgMTVhOSA5IDAgMCAxLTkgOWMtMi42MSAwLTQuOTEtMS4wNC02LjctMS44MiI+PC9wYXRoPjxwYXRoIGQ9Ik04IDEyaDgiPjwvcGF0aD48cGF0aCBkPSJNMTIgOGw0IDQgLTQgNCI+PC9wYXRoPjwvc3ZnPg==',
step: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTMgNGE0IDQgMCAxIDEtOCAwIDQgNCAwIDAgMSA4IDB6Ij48L3BhdGg+PHBhdGggZD0iTTE1IDEyYTQgNCAwIDEgMS04IDAgNCA0IDAgMCAxIDggMHoiPjwvcGF0aD48cGF0aCBkPSJNMTcgMjBhNCA0IDAgMSAxLTggMCA0IDQgMCAwIDEgOCAweiI+PC9wYXRoPjxsaW5lIHgxPSIxIiB5MT0iMSIgeDI9IjIzIiB5Mj0iMjMiPjwvbGluZT48L3N2Zz4=',
calorie: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCI+PC9jaXJjbGU+PHBhdGggZD0iTTEyIDhhNCA0IDAgMCAxIDAgOGE0IDQgMCAwIDEgMC04eiI+PC9wYXRoPjxwYXRoIGQ9Ik0xMiA0YTYuOTMgNi45MyAwIDAgMCAwIDE2YTYuOTMgNi45MyAwIDAgMCAwLTE2eiI+PC9wYXRoPjwvc3ZnPg==',
distance: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Mjg1RjQiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMTIgMjF2LTIiPjwvcGF0aD48cGF0aCBkPSJNMTIgMTV2LTIiPjwvcGF0aD48cGF0aCBkPSJNMTIgOVY3Ij48L3BhdGg+PHBhdGggZD0iTTEyIDVWMiI+PC9wYXRoPjxwYXRoIGQ9Ik01IDEyaC0yIj48L3BhdGg+PHBhdGggZD0iTTE5IDEyaDIiPjwvcGF0aD48cGF0aCBkPSJNMTUgMTJhMyAzIDAgMSAxLTYgMCAzIDMgMCAwIDEgNiAweiI+PC9wYXRoPjwvc3ZnPg=='
};
// 计算时间格式化
const formatTime = (seconds: number) => {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 开始/暂停计步
const toggleCounting = () => {
if (isCounting) {
setIsCounting(false);
} else {
setIsCounting(true);
if (startTime === null) {
setStartTime(Date.now());
}
}
};
// 重置计数器
const resetCounter = () => {
Alert.alert(
'确认重置',
'确定要重置所有数据吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: () => {
setSteps(0);
setIsCounting(false);
setStartTime(null);
setElapsedTime(0);
setCalories(0);
setDistance(0);
}
}
]
);
};
// 模拟增加步数
const addSteps = () => {
if (isCounting) {
const newSteps = steps + 1;
setSteps(newSteps);
// 每步约消耗0.04卡路里,行走距离约0.7米
setCalories(parseFloat((newSteps * 0.04).toFixed(2)));
setDistance(parseFloat((newSteps * 0.7 / 1000).toFixed(2))); // 转换为公里
}
};
// 时间计算效果
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (isCounting && startTime) {
interval = setInterval(() => {
setElapsedTime(Math.floor((Date.now() - startTime) / 1000));
}, 1000);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isCounting, startTime]);
return (
<ScrollView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>步数计数器</Text>
<Text style={styles.subtitle}>记录您的每日运动</Text>
</View>
{/* 步数主显示区 */}
<View style={styles.mainCounter}>
<TouchableOpacity onPress={addSteps} style={styles.stepButton}>
<Image source={{ uri: icons.step }} style={styles.stepIcon} />
<Text style={styles.stepCount}>{steps.toLocaleString()}</Text>
<Text style={styles.stepLabel}>步数</Text>
</TouchableOpacity>
</View>
{/* 控制按钮 */}
<View style={styles.controls}>
<TouchableOpacity
style={[styles.controlButton, styles.startButton]}
onPress={toggleCounting}
>
<Image
source={{ uri: isCounting ? icons.pause : icons.play }}
style={styles.controlIcon}
/>
<Text style={styles.controlText}>{isCounting ? '暂停' : '开始'}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.controlButton, styles.resetButton]}
onPress={resetCounter}
>
<Image source={{ uri: icons.reset }} style={styles.controlIcon} />
<Text style={styles.controlText}>重置</Text>
</TouchableOpacity>
</View>
{/* 运动统计信息 */}
<View style={styles.statsContainer}>
<Text style={styles.statsTitle}>运动统计</Text>
<View style={styles.statsGrid}>
<View style={styles.statCard}>
<View style={styles.statHeader}>
<Image source={{ uri: icons.distance }} style={styles.statIcon} />
<Text style={styles.statLabel}>距离</Text>
</View>
<Text style={styles.statValue}>{distance.toFixed(2)} km</Text>
</View>
<View style={styles.statCard}>
<View style={styles.statHeader}>
<Image source={{ uri: icons.calorie }} style={styles.statIcon} />
<Text style={styles.statLabel}>消耗</Text>
</View>
<Text style={styles.statValue}>{calories.toFixed(1)} kcal</Text>
</View>
<View style={styles.statCard}>
<View style={styles.statHeader}>
<Image source={{ uri: icons.step }} style={styles.statIcon} />
<Text style={styles.statLabel}>时长</Text>
</View>
<Text style={styles.statValue}>{formatTime(elapsedTime)}</Text>
</View>
</View>
</View>
{/* 进度目标 */}
<View style={styles.goalContainer}>
<View style={styles.goalHeader}>
<Text style={styles.goalTitle}>今日目标</Text>
<Text style={styles.goalValue}>10,000 步</Text>
</View>
<View style={styles.progressBar}>
<View
style={[
styles.progressFill,
{ width: `${Math.min(100, (steps / 10000) * 100)}%` }
]}
/>
</View>
<Text style={styles.progressText}>
已完成 {Math.min(100, Math.round((steps / 10000) * 100))}%
</Text>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f0f8ff',
padding: 20
},
header: {
alignItems: 'center',
marginBottom: 30,
paddingTop: 20
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#2c3e50'
},
subtitle: {
fontSize: 16,
color: '#7f8c8d',
marginTop: 6
},
mainCounter: {
alignItems: 'center',
marginBottom: 40
},
stepButton: {
width: 220,
height: 220,
borderRadius: 110,
backgroundColor: '#4285F4',
alignItems: 'center',
justifyContent: 'center',
elevation: 8,
shadowColor: '#4285F4',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8
},
stepIcon: {
width: 40,
height: 40,
marginBottom: 15
},
stepCount: {
fontSize: 48,
fontWeight: 'bold',
color: '#fff'
},
stepLabel: {
fontSize: 18,
color: '#e8f0fe'
},
controls: {
flexDirection: 'row',
justifyContent: 'center',
marginBottom: 30
},
controlButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 14,
paddingHorizontal: 30,
borderRadius: 30,
marginHorizontal: 10,
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4
},
startButton: {
backgroundColor: '#4285F4'
},
resetButton: {
backgroundColor: '#fff'
},
controlIcon: {
width: 24,
height: 24,
marginRight: 10
},
controlText: {
fontSize: 18,
fontWeight: '600'
},
statsContainer: {
backgroundColor: '#fff',
borderRadius: 20,
padding: 20,
marginBottom: 25,
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4
},
statsTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#2c3e50',
marginBottom: 20,
textAlign: 'center'
},
statsGrid: {
flexDirection: 'row',
justifyContent: 'space-between'
},
statCard: {
backgroundColor: '#f8f9fa',
borderRadius: 16,
padding: 16,
width: '31%',
alignItems: 'center'
},
statHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10
},
statIcon: {
width: 20,
height: 20,
marginRight: 8
},
statLabel: {
fontSize: 14,
color: '#7f8c8d'
},
statValue: {
fontSize: 18,
fontWeight: 'bold',
color: '#4285F4'
},
goalContainer: {
backgroundColor: '#fff',
borderRadius: 20,
padding: 20,
elevation: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4
},
goalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 15
},
goalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#2c3e50'
},
goalValue: {
fontSize: 16,
color: '#7f8c8d'
},
progressBar: {
height: 12,
backgroundColor: '#ecf0f1',
borderRadius: 6,
marginBottom: 10,
overflow: 'hidden'
},
progressFill: {
height: '100%',
backgroundColor: '#4285F4',
borderRadius: 6
},
progressText: {
fontSize: 14,
color: '#7f8c8d',
textAlign: 'center'
}
});
export default App;
这段React Native步数计数器代码实现了一个完整的运动数据追踪功能,其核心原理基于React的状态管理和副作用处理机制。formatTime函数通过数学运算将秒数转换为标准的时分秒格式,采用padStart方法确保时间显示的格式统一性,这种时间处理方式在鸿蒙系统的多设备时间同步场景中具有重要意义。
toggleCounting函数作为计数器控制的核心,通过isCounting状态管理开始和暂停功能。当开始计数时,函数检查startTime是否为空来决定是否设置起始时间,这种设计避免了重复点击开始按钮时的时间重置问题。在鸿蒙系统的分布式任务调度中,这种状态管理模式能够确保计数任务在不同设备间的连续性。
resetCounter函数通过Alert组件提供用户确认机制,防止误操作导致数据丢失。确认后的重置操作将所有相关状态恢复初始值,包括步数、计数状态、起始时间、经过时间、消耗卡路里和行走距离。这种全面的状态重置机制在鸿蒙系统的应用生命周期管理中非常重要,能够确保应用在各种使用场景下的状态一致性。
addSteps函数模拟步数增加逻辑,通过简单的数学计算实时更新卡路里消耗和行走距离。函数中使用toFixed方法控制小数位数,确保数据显示的规范性。在鸿蒙系统的传感器数据处理中,这种数据计算方式能够与实际的运动传感器数据良好对接。
useEffect钩子实现了时间计算的核心逻辑,通过setInterval创建定时器来更新经过时间。定时器的创建和清理逻辑确保了组件卸载时不会出现内存泄漏问题。在鸿蒙系统的后台任务管理中,这种定时器处理方式能够与系统的省电机制良好配合,避免不必要的资源消耗。

UI布局采用ScrollView作为根容器,确保内容在不同屏幕尺寸设备上的可滚动性。主计数区域通过TouchableOpacity组件实现步数增加功能,用户点击即可增加步数,这种直观的交互设计符合鸿蒙系统的用户界面规范。控制按钮区域包含开始/暂停和重置按钮,通过条件渲染显示不同的图标和文本,提供清晰的操作反馈。
运动统计信息区域通过网格布局展示距离、消耗和时长三个核心数据,每个数据项包含图标、标签和数值,这种信息架构在鸿蒙系统的健康数据展示中非常常见。进度目标区域通过进度条直观显示用户完成度,宽度计算基于当前步数与目标步数的比例,这种可视化反馈能够激励用户达成运动目标。
从鸿蒙系统适配角度来看,该代码充分利用了React Native的跨平台特性,在鸿蒙设备上能够获得原生级的性能表现。鸿蒙系统的分布式数据管理能力能够与React的状态管理机制良好结合,确保运动数据在手机、手表等不同设备间的同步。组件的生命周期管理与鸿蒙系统的应用管理机制保持一致,能够在应用前后台切换时正确处理定时器和状态更新。
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

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

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

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐




所有评论(0)