【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:NumberKeyboard 数字键盘(虚拟数字键盘)
React Native数字键盘实现指南:提供了两种实现方式,一种是通过TextInput的keyboardType="numeric"快速实现基础数字输入,另一种是创建自定义数字键盘组件。详细介绍了自定义键盘的实现步骤,包括创建键盘UI、处理点击事件以及在应用中集成使用。文章包含完整代码示例和样式定义,并特别提醒在Android鸿蒙系统上的兼容性注意事项。还附带了一个真实案例
在React Native中实现一个数字键盘(Number Keyboard),你可以使用<TextInput>组件的keyboardType属性设置为numeric来快速实现一个基础的数字键盘,但这通常只适用于输入数字的场景。如果你想创建一个自定义的数字键盘,比如一个完整的键盘面板,你可以通过以下步骤来实现:
- 使用
<TextInput>的基本数字键盘
这是最简单的方法,适用于需要用户输入数字但不希望自定义键盘的情况。
<TextInput
keyboardType="numeric"
placeholder="输入数字"
/>
- 创建自定义数字键盘
如果你需要更复杂的布局或样式,你可以创建一个自定义的数字键盘。
步骤1: 创建键盘组件
首先,创建一个自定义的数字键盘组件。例如,你可以创建一个名为NumberKeyboard.js的文件:
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
const NumberKeyboard = ({ onPress }) => {
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'C', 0];
return (
<View style={styles.container}>
{numbers.map(number => (
<TouchableOpacity key={number} onPress={() => onPress(number)} style={styles.button}>
<Text style={styles.buttonText}>{number}</Text>
</TouchableOpacity>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
},
button: {
borderWidth: 1,
borderColor: '000',
padding: 20,
margin: 5,
width: 60,
height: 60,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
fontSize: 20,
},
});
export default NumberKeyboard;
步骤2: 在你的应用中使用这个键盘组件
然后,在你的主组件中引入并使用这个NumberKeyboard组件:
import React, { useState } from 'react';
import { View, TextInput } from 'react-native';
import NumberKeyboard from './NumberKeyboard'; // 确保路径正确
const App = () => {
const [inputValue, setInputValue] = useState('');
const handlePress = (number) => {
if (number === 'C') { // 如果按下的是清除按钮,则清空输入值
setInputValue('');
} else { // 否则,追加数字到输入值中
setInputValue(prevValue => prevValue + number);
}
};
return (
<View>
<TextInput value={inputValue} editable={false} /> {/* 使TextInput不可编辑 */}
<NumberKeyboard onPress={handlePress} />
</View>
);
};
export default App;
在这个例子中,我们创建了一个不可编辑的TextInput用于显示输入值,并通过自定义的NumberKeyboard组件来输入数据。当用户点击数字或清除按钮时,会调用handlePress函数来更新输入值。这样你就得到了一个自定义的数字键盘。
注意事项:
- 确保你的React Native环境已经配置好,特别是在使用Android鸿蒙系统时,考虑到可能的兼容性问题,确保你使用的库和React Native版本都支持鸿蒙系统。你可以查看React Native官方文档或社区关于鸿蒙系统的支持情况。
- 在开发过程中,测试你的应用在Android设备上的表现,特别是鸿蒙系统的设备上,以确保一切功能正常。你可以使用真机调试或模拟器进行测试。
真实案例演示效果:
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, TouchableOpacity, PanResponder, Animated } 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 'volume-low': return '🔈';
case 'volume-high': return '🔊';
case 'brightness-low': return '🔅';
case 'brightness-high': return '🔆';
case 'temperature-low': return '🧊';
case 'temperature-high': return '🔥';
case 'min': return '➖';
case 'max': return '➕';
case 'slider': 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>
);
};
// Slider Component
interface SliderProps {
value: number;
onValueChange: (value: number) => void;
min?: number;
max?: number;
step?: number;
disabled?: boolean;
showLabels?: boolean;
showValue?: boolean;
minIcon?: string;
maxIcon?: string;
color?: string;
trackHeight?: number;
}
const Slider: React.FC<SliderProps> = ({
value,
onValueChange,
min = 0,
max = 100,
step = 1,
disabled = false,
showLabels = true,
showValue = true,
minIcon,
maxIcon,
color = '#1890ff',
trackHeight = 6
}) => {
const [trackWidth, setTrackWidth] = useState(0);
const panResponder = React.useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
if (disabled) return;
},
onPanResponderMove: (_, gestureState) => {
if (disabled) return;
const newValue = Math.min(max, Math.max(min,
min + ((gestureState.dx / trackWidth) * (max - min))
));
// Apply step rounding
const steppedValue = Math.round(newValue / step) * step;
onValueChange(steppedValue);
},
onPanResponderRelease: () => {
if (disabled) return;
},
})
).current;
const getPercentage = () => {
return ((value - min) / (max - min)) * 100;
};
return (
<View style={styles.sliderContainer}>
{showLabels && (
<View style={styles.labelsContainer}>
{minIcon ? (
<Icon name={minIcon} size={20} color="#999999" />
) : (
<Text style={styles.minLabel}>{min}</Text>
)}
<View style={{ flex: 1 }} />
{maxIcon ? (
<Icon name={maxIcon} size={20} color="#999999" />
) : (
<Text style={styles.maxLabel}>{max}</Text>
)}
</View>
)}
<View
style={styles.trackContainer}
onLayout={(event) => {
setTrackWidth(event.nativeEvent.layout.width);
}}
>
<View
style={[
styles.trackBackground,
{
height: trackHeight,
backgroundColor: disabled ? '#f0f0f0' : '#e8e8e8'
}
]}
/>
<Animated.View
style={[
styles.trackProgress,
{
height: trackHeight,
backgroundColor: disabled ? '#cccccc' : color,
width: `${getPercentage()}%`
}
]}
/>
<Animated.View
{...panResponder.panHandlers}
style={[
styles.thumb,
{
backgroundColor: disabled ? '#cccccc' : '#ffffff',
borderColor: disabled ? '#cccccc' : color,
left: `${getPercentage()}%`,
transform: [{ translateX: -12 }]
}
]}
/>
</View>
{showValue && (
<View style={styles.valueContainer}>
<Text style={[styles.valueText, { color: disabled ? '#999999' : color }]}>
{value}
</Text>
</View>
)}
</View>
);
};
// Main App Component
const SliderComponentApp = () => {
const [volume, setVolume] = useState(50);
const [brightness, setBrightness] = useState(75);
const [temperature, setTemperature] = useState(22);
const [priceRange, setPriceRange] = useState(1500);
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.sliderGroupsContainer}>
<View style={styles.sliderGroup}>
<Text style={styles.sliderLabel}>音量调节</Text>
<Slider
value={volume}
onValueChange={setVolume}
min={0}
max={100}
minIcon="volume-low"
maxIcon="volume-high"
color="#1890ff"
showValue
/>
<View style={styles.valueDisplay}>
<Text style={styles.valueDisplayText}>当前音量: {volume}%</Text>
</View>
</View>
<View style={styles.sliderGroup}>
<Text style={styles.sliderLabel}>亮度调节</Text>
<Slider
value={brightness}
onValueChange={setBrightness}
min={0}
max={100}
minIcon="brightness-low"
maxIcon="brightness-high"
color="#faad14"
showValue
/>
<View style={styles.valueDisplay}>
<Text style={styles.valueDisplayText}>当前亮度: {brightness}%</Text>
</View>
</View>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>温度调节</Text>
<View style={styles.sliderGroupsContainer}>
<View style={styles.sliderGroup}>
<Text style={styles.sliderLabel}>空调温度</Text>
<Slider
value={temperature}
onValueChange={setTemperature}
min={16}
max={30}
step={0.5}
minIcon="temperature-low"
maxIcon="temperature-high"
color="#ff4d4f"
showValue
/>
<View style={styles.valueDisplay}>
<Text style={styles.valueDisplayText}>当前温度: {temperature}°C</Text>
</View>
</View>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>价格区间</Text>
<View style={styles.sliderGroupsContainer}>
<View style={styles.sliderGroup}>
<Text style={styles.sliderLabel}>预算范围</Text>
<Slider
value={priceRange}
onValueChange={setPriceRange}
min={0}
max={5000}
step={100}
color="#52c41a"
showValue
/>
<View style={styles.valueDisplay}>
<Text style={styles.valueDisplayText}>预算上限: ¥{priceRange}</Text>
</View>
</View>
</View>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>功能演示</Text>
<View style={styles.demosContainer}>
<View style={styles.demoItem}>
<Icon name="slider" 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="volume-high" 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="temperature-high" size={24} color="#fa8c16" 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}>{'<Slider'}</Text>
<Text style={styles.codeText}> value={'{sliderValue}'}</Text>
<Text style={styles.codeText}> onValueChange={'{setSliderValue}'}</Text>
<Text style={styles.codeText}> min={'{0}'} max={'{100}'} step={'{1}'}{'\n'}/></Text>
</View>
<Text style={styles.description}>
Slider组件提供了完整的滑块功能,包括连续滑动、步长控制、范围设定等。
通过value控制当前值,onValueChange处理值变化,支持自定义样式和图标。
</Text>
</View>
<View style={styles.featuresSection}>
<Text style={styles.sectionTitle}>功能特性</Text>
<View style={styles.featuresList}>
<View style={styles.featureItem}>
<Icon name="slider" size={20} color="#1890ff" style={styles.featureIcon} />
<Text style={styles.featureText}>连续滑动</Text>
</View>
<View style={styles.featureItem}>
<Icon name="volume-high" size={20} color="#52c41a" style={styles.featureIcon} />
<Text style={styles.featureText}>步长控制</Text>
</View>
<View style={styles.featureItem}>
<Icon name="temperature-high" size={20} color="#fa8c16" style={styles.featureIcon} />
<Text style={styles.featureText}>范围设定</Text>
</View>
<View style={styles.featureItem}>
<Icon name="brightness-high" 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>
</ScrollView>
);
};
const { width } = Dimensions.get('window');
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f6f8fa',
},
header: {
backgroundColor: '#ffffff',
paddingVertical: 30,
paddingHorizontal: 20,
marginBottom: 10,
borderBottomWidth: 1,
borderBottomColor: '#e1e4e8',
},
headerTitle: {
fontSize: 28,
fontWeight: '700',
color: '#24292e',
textAlign: 'center',
marginBottom: 5,
},
headerSubtitle: {
fontSize: 16,
color: '#586069',
textAlign: 'center',
},
section: {
marginBottom: 25,
},
sectionTitle: {
fontSize: 20,
fontWeight: '700',
color: '#24292e',
paddingHorizontal: 20,
paddingBottom: 15,
},
sliderGroupsContainer: {
backgroundColor: '#ffffff',
marginHorizontal: 15,
borderRadius: 12,
padding: 20,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
marginBottom: 10,
},
sliderGroup: {
marginBottom: 25,
},
sliderGroupLast: {
marginBottom: 0,
},
sliderLabel: {
fontSize: 16,
fontWeight: '500',
color: '#24292e',
marginBottom: 15,
},
valueDisplay: {
marginTop: 15,
padding: 12,
backgroundColor: '#f1f8ff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#c8e1ff',
},
valueDisplayText: {
fontSize: 16,
color: '#1890ff',
fontWeight: '500',
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: '#24292e',
marginBottom: 3,
},
demoDesc: {
fontSize: 14,
color: '#586069',
},
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: '#24292e',
borderRadius: 8,
padding: 15,
marginBottom: 15,
},
codeText: {
fontFamily: 'monospace',
color: '#e1e4e8',
fontSize: 14,
lineHeight: 22,
},
description: {
fontSize: 15,
color: '#24292e',
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: '#24292e',
},
footer: {
paddingVertical: 20,
alignItems: 'center',
},
footerText: {
color: '#6a737d',
fontSize: 14,
},
// Slider Styles
sliderContainer: {
marginBottom: 10,
},
labelsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 10,
},
minLabel: {
fontSize: 14,
color: '#999999',
},
maxLabel: {
fontSize: 14,
color: '#999999',
},
trackContainer: {
height: 30,
justifyContent: 'center',
},
trackBackground: {
position: 'absolute',
left: 0,
right: 0,
borderRadius: 3,
},
trackProgress: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
borderRadius: 3,
},
thumb: {
position: 'absolute',
width: 24,
height: 24,
borderRadius: 12,
borderWidth: 2,
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
},
valueContainer: {
alignItems: 'center',
marginTop: 10,
},
valueText: {
fontSize: 16,
fontWeight: '600',
},
});
export default SliderComponentApp;
这段 React Native 滑块组件在鸿蒙系统上的技术实现涉及与鸿蒙 ACE 引擎的深度集成。
从 Icon 组件的 Unicode 符号映射机制来看,在鸿蒙的方舟编译器中,字符串常量经过深度优化,Unicode 符号的直接渲染比传统图标资源具有更好的内存管理效率。鸿蒙的渲染管线中,Text 组件针对 Emoji 和特殊符号进行了专门处理,这确保了在不同分辨率鸿蒙设备上图标显示的清晰度和一致性。
在 PanResponder 手势响应系统的实现中,代码通过 onPanResponderMove 事件监听用户拖拽操作。当用户在鸿蒙设备上进行拖拽时,gestureState.dx 参数捕获水平位移,结合 trackWidth 计算出新的数值。这种手势处理机制在鸿蒙的多点触控框架中需要与系统的触摸事件分发机制进行协调。

数值计算算法采用了边界检查和步进舍入的双重保障。Math.min(max, Math.max(min, …)) 确保结果始终在有效范围内,而 Math.round(newValue / step) * step 实现了步进值的精确控制。
在鸿蒙的弹性布局系统中,滑块组件的尺寸自适应需要特别关注。trackHeight 参数不仅影响轨道高度,还需要与鸿蒙的屏幕密度缩放机制进行匹配,确保在不同设备上获得一致的视觉效果。
getPercentage 函数通过简单的数学运算将当前值转换为百分比,这种算法在鸿蒙的分布式UI框架中能够保持计算的一致性。
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

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

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

更多推荐




所有评论(0)