
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
一、核心知识点
滑块组件不仅限于音量调节,还可以实现进度条和评分系统。在鸿蒙端,@react-native-ohos/slider 提供了完整的滑块功能支持,让开发者可以轻松实现各种进度控制和评分场景。
1.1 进度条与滑块的区别
typescript
// 滑块:用户可以自由拖动控制值
<Slider
value={userControlledValue}
onValueChange={setUserControlledValue}
/>
// 进度条:只读,用户不能控制,用于显示进度
<Slider
value={progressValue}
disabled={true} // 禁用用户交互
minimumTrackTintColor="#409EFF"
maximumTrackTintColor="#E5E6EB"
/>
关键区别:
- 滑块:用户可以交互,控制值由用户决定
- 进度条:只读显示,值由程序自动更新
- 评分组件:通常是整数步进,有固定的级别
1.2 评分系统的核心特性
- 整数步进 :使用
step={1}实现整数评分(1-5星) - 半星支持 :使用
step={0.5}支持半星评分 - 视觉反馈:根据评分改变星星颜色和样式
- 动画效果:评分变化时的平滑过渡动画
- 双向绑定:支持用户评分和程序设置评分
- 持久化存储:评分结果自动保存到本地
- 统计功能:记录平均评分和评分次数
二、实战核心代码深度解析
2.1 下载进度条深度解析
typescript
const DownloadProgressBar = () => {
const [progress, setProgress] = useState(0);
const [downloading, setDownloading] = useState(false);
const startDownload = () => {
setDownloading(true);
setProgress(0);
// 模拟下载过程
let currentProgress = 0;
const interval = setInterval(() => {
currentProgress += Math.random() * 10;
if (currentProgress >= 100) {
currentProgress = 100;
clearInterval(interval);
setDownloading(false);
}
setProgress(currentProgress);
}, 200);
};
const getProgressColor = () => {
if (progress < 30) return '#F44336'; // 红色:刚开始
if (progress < 70) return '#FF9800'; // 橙色:进行中
return '#4CAF50'; // 绿色:即将完成
};
return (
<View style={styles.container}>
<Text style={styles.title}>文件下载</Text>
<View style={styles.infoContainer}>
<Text style={styles.infoText}>进度: {Math.round(progress)}%</Text>
<Text style={styles.infoText}>
{downloading ? '下载中...' : '下载完成'}
</Text>
</View>
<View style={styles.progressContainer}>
<Slider
value={progress}
disabled={true}
minimumTrackTintColor={getProgressColor()}
maximumTrackTintColor="#E5E6EB"
thumbTintColor={getProgressColor()}
/>
</View>
<TouchableOpacity
style={[styles.button, downloading && styles.buttonDisabled]}
onPress={startDownload}
disabled={downloading}
>
<Text style={styles.buttonText}>
{downloading ? '下载中...' : '开始下载'}
</Text>
</TouchableOpacity>
</View>
);
};
技术深度解析:
-
进度更新的时机控制:
typescript// 使用 setInterval 模拟下载过程 const interval = setInterval(() => { currentProgress += Math.random() * 10; // 每次增加随机进度 if (currentProgress >= 100) { clearInterval(interval); // 完成时清除定时器 setDownloading(false); } setProgress(currentProgress); }, 200);- 间隔时间选择:200ms 提供流畅的视觉效果,但不会过于频繁更新
- 进度增量随机:模拟真实网络环境,避免线性的不真实感
- 清理定时器:完成后必须清理,避免内存泄漏
-
禁用状态的视觉设计:
typescriptdisabled={true} // 禁用用户交互- 用户体验:下载中用户不能干预进度
- 视觉提示 :禁用状态下滑块颜色会变灰(通过
thumbTintColor) - 语义正确:进度条的本质就是只读的显示组件
-
颜色状态机的实现:
typescriptconst getProgressColor = () => { if (progress < 30) return '#F44336'; // 红色 if (progress < 70) return '#FF9800'; // 橙色 return '#4CAF50'; // 绿色 };- 三阶段设计:刚开始、进行中、即将完成
- 颜色心理学:红色表示警告/等待,橙色表示进行中,绿色表示成功
- 动态更新:颜色随进度实时变化,提供直观反馈
-
下载状态的完整管理:
typescriptconst [downloading, setDownloading] = useState(false); const startDownload = () => { setDownloading(true); // 开始下载 // ... 下载逻辑 setDownloading(false); // 完成下载 };- 状态枚举:空闲、下载中、完成、失败
- 状态转换:只有从空闲可以转换到下载中
- 防止重复操作:下载中禁用开始按钮
2.2 视频播放进度条深度解析
typescript
const VideoProgressBar = () => {
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [isSeeking, setIsSeeking] = useState(false);
const seekPositionRef = useRef(0);
// 格式化时间显示
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 处理用户拖动进度条
const handleSeekStart = () => {
setIsSeeking(true);
seekPositionRef.current = currentTime;
};
const handleSeek = (value: number) => {
seekPositionRef.current = value;
};
const handleSeekEnd = () => {
setIsSeeking(false);
// 应用跳转
if (videoRef.current) {
videoRef.current.seek(seekPositionRef.current);
}
};
// 播放进度更新(仅在非拖拽状态下更新)
const handleProgress = (data: { currentTime: number }) => {
if (!isSeeking) {
setCurrentTime(data.currentTime);
}
};
// 拖拽时显示的预览时间
const previewTime = isSeeking ? seekPositionRef.current : currentTime;
return (
<View style={styles.container}>
{/* 时间显示 */}
<View style={styles.timeContainer}>
<Text style={styles.timeText}>{formatTime(previewTime)}</Text>
<Text style={styles.timeText}>{formatTime(duration)}</Text>
</View>
{/* 进度条 */}
<Slider
value={previewTime}
minimumValue={0}
maximumValue={duration}
onValueChange={handleSeek}
onSlidingStart={handleSeekStart}
onSlidingComplete={handleSeekEnd}
minimumTrackTintColor="#409EFF"
maximumTrackTintColor="#E5E6EB"
thumbTintColor="#409EFF"
step={0.1}
/>
{/* 播放控制 */}
<TouchableOpacity onPress={() => setIsPlaying(!isPlaying)}>
<Text style={styles.controlText}>
{isPlaying ? '⏸' : '▶'}
</Text>
</TouchableOpacity>
</View>
);
};
技术深度解析:
-
拖拽状态的精细控制:
typescriptconst [isSeeking, setIsSeeking] = useState(false); const seekPositionRef = useRef(0); const handleSeekStart = () => { setIsSeeking(true); }; const handleSeek = (value: number) => { seekPositionRef.current = value; }; const handleSeekEnd = () => { setIsSeeking(false); videoRef.current?.seek(seekPositionRef.current); };- 为什么使用 useRef :拖拽过程中频繁更新
seekPositionRef.current,但不触发重新渲染 - 三个阶段:开始拖动(onSlidingStart)、拖拽中(onValueChange)、结束拖动(onSlidingComplete)
- 状态隔离:拖拽时显示预览时间,不干扰实际的播放进度
- 为什么使用 useRef :拖拽过程中频繁更新
-
播放进度的更新策略:
typescriptconst handleProgress = (data: { currentTime: number }) => { if (!isSeeking) { setCurrentTime(data.currentTime); } };- 条件更新 :只在非拖拽状态下更新
currentTime - 避免冲突:拖拽时用户控制的预览时间优先
- 平滑过渡:拖拽结束后,预览时间自动平滑过渡到实际播放进度
- 条件更新 :只在非拖拽状态下更新
-
时间格式化的细节:
typescriptconst formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; };- padStart 的作用:确保总是显示两位数(如 05 而不是 5)
- 边界情况:处理超过 60 秒的时间
- 可扩展性 :可以添加小时显示(
%H:%M:%S)
-
步进值的精确控制:
typescriptstep={0.1}- 为什么是 0.1:支持精确到 0.1 秒的跳转
- 用户体验:更精细的控制,适合长视频
- 性能考虑:0.1 秒的精度不会造成性能问题
2.3 5星评分系统深度解析
typescript
const StarRating = () => {
const [rating, setRating] = useState(0);
const [hoverRating, setHoverRating] = useState(0);
const [userRatings, setUserRatings] = useState<number[]>([]);
// 保存评分
const saveRating = (value: number) => {
setRating(value);
setUserRatings([...userRatings, value]);
// 持久化存储
AsyncStorage.setItem('userRatings', JSON.stringify([...userRatings, value]));
// 发送评分事件
RatingEvents.emit('rating', { value, timestamp: Date.now() });
};
// 计算平均评分
const averageRating = userRatings.length > 0
? userRatings.reduce((sum, r) => sum + r, 0) / userRatings.length
: 0;
// 渲染星星
const renderStars = () => {
return [1, 2, 3, 4, 5].map((star) => {
const filled = star <= rating;
const halfFilled = star - 0.5 === rating;
return (
<TouchableOpacity
key={star}
onPress={() => saveRating(star)}
onLongPress={() => saveRating(star - 0.5)}
>
<Text style={styles.star}>
{filled ? '⭐' : halfFilled ? '✨' : '☆'}
</Text>
</TouchableOpacity>
);
});
};
return (
<View style={styles.container}>
<Text style={styles.title}>评分系统</Text>
{/* 用户评分 */}
<View style={styles.ratingContainer}>
{renderStars()}
<Text style={styles.ratingText}>{rating.toFixed(1)}</Text>
</View>
{/* 统计信息 */}
<View style={styles.statsContainer}>
<Text style={styles.statText}>平均评分: {averageRating.toFixed(1)}</Text>
<Text style={styles.statText}>评分次数: {userRatings.length}</Text>
</View>
{/* 滑块评分(精确控制) */}
<View style={styles.sliderContainer}>
<Text style={styles.sliderLabel}>精确评分: {rating.toFixed(1)}</Text>
<Slider
value={rating}
minimumValue={0}
maximumValue={5}
step={0.5}
onValueChange={setRating}
onSlidingComplete={saveRating}
minimumTrackTintColor="#FFD700"
maximumTrackTintColor="#E5E6EB"
thumbTintColor="#FFD700"
/>
</View>
</View>
);
};
技术深度解析:
-
评分的三种输入方式:
typescript// 方式1:点击星星(整数评分) onPress={() => saveRating(star)} // 方式2:长按星星(半星评分) onLongPress={() => saveRating(star - 0.5)} // 方式3:拖动滑块(精确评分) <Slider step={0.5} onValueChange={setRating} onSlidingComplete={saveRating} />- 满足不同用户习惯:有些用户喜欢点击,有些喜欢精确控制
- 半星支持 :通过
step={0.5}实现 - 一致性保证 :三种方式都更新同一个
rating状态
-
星星状态的三种表示:
typescriptconst filled = star <= rating; // 完全填充(实心星) const halfFilled = star - 0.5 === rating; // 半填充(半星) const empty = !filled && !halfFilled; // 空(空心星)- 实心星 ⭐:评分 >= 星星位置
- 半星 ✨:评分在 星星位置 - 0.5 和 星星位置之间
- 空心星 ☆:评分 < 星星位置 - 0.5
- 视觉层次:三种状态清晰区分
-
评分统计的实现:
typescriptconst averageRating = userRatings.length > 0 ? userRatings.reduce((sum, r) => sum + r, 0) / userRatings.length : 0;- reduce 的作用:计算所有评分的总和
- 边界处理 :
userRatings.length === 0时返回 0 - 精度控制 :
toFixed(1)保留一位小数
-
持久化存储的实现:
typescriptAsyncStorage.setItem('userRatings', JSON.stringify([...userRatings, value]));- JSON 序列化:数组转换为字符串存储
- 不使用第三方存储 :示例中移除了 AsyncStorage 依赖
- 实际应用:生产环境中可以使用 @react-native-async-storage/async-storage
2.4 双向数据绑定深度解析
typescript
const TwoWayBindingSlider = () => {
const [value, setValue] = useState(50);
const [inputValue, setInputValue] = useState('50');
// 滑块变化更新输入框
const handleSliderChange = (newValue: number) => {
setValue(newValue);
setInputValue(newValue.toString());
};
// 输入框变化更新滑块
const handleInputChange = (text: string) => {
setInputValue(text);
const numValue = parseFloat(text);
if (!isNaN(numValue) && numValue >= 0 && numValue <= 100) {
setValue(numValue);
}
};
// 输入框失去焦点时验证并格式化
const handleInputBlur = () => {
let numValue = parseFloat(inputValue);
if (isNaN(numValue)) numValue = 50;
numValue = Math.max(0, Math.min(100, numValue));
setValue(numValue);
setInputValue(numValue.toString());
};
return (
<View style={styles.container}>
<Text style={styles.title}>双向数据绑定</Text>
{/* 滑块控制 */}
<Slider
value={value}
minimumValue={0}
maximumValue={100}
step={1}
onValueChange={handleSliderChange}
/>
{/* 输入框控制 */}
<TextInput
style={styles.input}
value={inputValue}
onChangeText={handleInputChange}
onBlur={handleInputBlur}
keyboardType="numeric"
placeholder="请输入 0-100 的数值"
/>
<Text style={styles.valueText}>当前值: {value}</Text>
</View>
);
};
技术深度解析:
-
双向绑定的实现原理:
typescript// 滑块 → 输入框 const handleSliderChange = (newValue: number) => { setValue(newValue); setInputValue(newValue.toString()); }; // 输入框 → 滑块 const handleInputChange = (text: string) => { setInputValue(text); const numValue = parseFloat(text); if (!isNaN(numValue) && numValue >= 0 && numValue <= 100) { setValue(numValue); } };- 两个状态 :
value(数字)和inputValue(字符串) - 同步更新:一个变化时更新另一个
- 类型转换:数字 ↔ 字符串,需要验证和转换
- 两个状态 :
-
输入验证的边界处理:
typescriptconst handleInputBlur = () => { let numValue = parseFloat(inputValue); if (isNaN(numValue)) numValue = 50; // 无效值使用默认值 numValue = Math.max(0, Math.min(100, numValue)); // 限制在 0-100 范围 setValue(numValue); setInputValue(numValue.toString()); };- 为什么在 onBlur 验证:输入过程中允许临时无效值,失去焦点时才修正
- 三步验证:解析为数字 → 检查是否有效 → 限制范围
- 用户体验:避免输入过程中频繁报错
-
性能优化:避免无限循环:
typescript// ❌ 错误的做法:会导致无限循环 const handleSliderChange = (newValue: number) => { setValue(newValue); setInputValue(newValue.toString()); }; useEffect(() => { setInputValue(value.toString()); // 会导致滑块再变化 }, [value]); // ✅ 正确的做法:只在滑块变化时更新输入框 const handleSliderChange = (newValue: number) => { setValue(newValue); setInputValue(newValue.toString()); };- 避免循环依赖:不要在 useEffect 中更新另一个状态
- 单向更新:滑块变化时更新输入框,输入框变化时更新滑块
- 事件驱动:使用事件回调而不是响应式依赖
三、实战完整版:综合进度控制与评分系统
typescript
import React, { useState, useRef, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
SafeAreaView,
ScrollView,
StatusBar,
TextInput,
} from 'react-native';
import Slider from '@react-native-ohos/slider';
const ProgressRatingScreen = () => {
// 下载进度状态
const [downloadProgress, setDownloadProgress] = useState(0);
const [downloading, setDownloading] = useState(false);
// 视频播放状态
const [videoProgress, setVideoProgress] = useState(0);
const [videoDuration, setVideoDuration] = useState(100);
const [isPlaying, setIsPlaying] = useState(false);
const [isSeeking, setIsSeeking] = useState(false);
const seekPositionRef = useRef(0);
// 评分状态
const [rating, setRating] = useState(0);
const [userRatings, setUserRatings] = useState<number[]>([]);
// 双向绑定状态
const [sliderValue, setSliderValue] = useState(50);
const [inputValue, setInputValue] = useState('50');
// 加载保存的评分
useEffect(() => {
loadSavedRatings();
}, []);
const loadSavedRatings = async () => {
try {
console.log('加载评分功能已移除,不使用第三方存储');
} catch (error) {
console.error('加载评分失败:', error);
}
};
// 模拟下载
const startDownload = () => {
setDownloading(true);
setDownloadProgress(0);
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 8;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
setDownloading(false);
}
setDownloadProgress(progress);
}, 300);
};
// 视频控制
const togglePlay = () => {
setIsPlaying(!isPlaying);
};
const handleSeekStart = () => {
setIsSeeking(true);
seekPositionRef.current = videoProgress;
};
const handleSeek = (value: number) => {
seekPositionRef.current = value;
};
const handleSeekEnd = () => {
setIsSeeking(false);
setVideoProgress(seekPositionRef.current);
};
// 模拟视频播放
useEffect(() => {
if (isPlaying && !isSeeking) {
const interval = setInterval(() => {
setVideoProgress(prev => {
if (prev >= videoDuration) {
setIsPlaying(false);
return 0;
}
return prev + 0.5;
});
}, 100);
return () => clearInterval(interval);
}
}, [isPlaying, isSeeking, videoDuration]);
// 评分功能
const saveRating = async (value: number) => {
setRating(value);
setUserRatings([...userRatings, value]);
// 不使用第三方存储,仅作为示例
// 实际项目中可以使用 AsyncStorage 或其他存储方案
};
// 双向绑定
const handleSliderChange = (value: number) => {
setSliderValue(value);
setInputValue(value.toString());
};
const handleInputChange = (text: string) => {
setInputValue(text);
const numValue = parseFloat(text);
if (!isNaN(numValue) && numValue >= 0 && numValue <= 100) {
setSliderValue(numValue);
}
};
const handleInputBlur = () => {
let numValue = parseFloat(inputValue);
if (isNaN(numValue)) numValue = 50;
numValue = Math.max(0, Math.min(100, numValue));
setSliderValue(numValue);
setInputValue(numValue.toString());
};
// 格式化时间
const formatTime = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<View style={styles.header}>
<Text style={styles.headerTitle}>📊 进度控制与评分</Text>
<Text style={styles.headerSubtitle}>@react-native-ohos/slider</Text>
</View>
<ScrollView style={styles.content}>
{/* 下载进度卡片 */}
<View style={styles.card}>
<Text style={styles.cardTitle}>📥 下载进度</Text>
<View style={styles.infoRow}>
<Text style={styles.infoText}>
进度: {Math.round(downloadProgress)}%
</Text>
<Text style={styles.infoText}>
{downloading ? '下载中...' : downloadProgress === 100 ? '✅ 完成' : '等待下载'}
</Text>
</View>
<Slider
value={downloadProgress}
disabled={true}
minimumTrackTintColor={downloadProgress < 30 ? '#F44336' : downloadProgress < 70 ? '#FF9800' : '#4CAF50'}
maximumTrackTintColor="#E5E6EB"
thumbTintColor={downloadProgress < 30 ? '#F44336' : downloadProgress < 70 ? '#FF9800' : '#4CAF50'}
/>
<TouchableOpacity
style={[styles.button, downloading && styles.buttonDisabled]}
onPress={startDownload}
disabled={downloading}
>
<Text style={styles.buttonText}>
{downloading ? '下载中...' : '开始下载'}
</Text>
</TouchableOpacity>
</View>
{/* 视频播放卡片 */}
<View style={styles.card}>
<Text style={styles.cardTitle}>🎬 视频播放</Text>
<View style={styles.timeRow}>
<Text style={styles.timeText}>
{formatTime(isSeeking ? seekPositionRef.current : videoProgress)}
</Text>
<Text style={styles.timeText}>
{formatTime(videoDuration)}
</Text>
</View>
<Slider
value={isSeeking ? seekPositionRef.current : videoProgress}
minimumValue={0}
maximumValue={videoDuration}
onValueChange={handleSeek}
onSlidingStart={handleSeekStart}
onSlidingComplete={handleSeekEnd}
minimumTrackTintColor="#409EFF"
maximumTrackTintColor="#E5E6EB"
thumbTintColor="#409EFF"
step={0.1}
/>
<View style={styles.controlsRow}>
<TouchableOpacity
style={styles.controlButton}
onPress={togglePlay}
>
<Text style={styles.controlText}>
{isPlaying ? '⏸ 暂停' : '▶ 播放'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.controlButton}
onPress={() => setVideoProgress(0)}
>
<Text style={styles.controlText}>⏮ 重置</Text>
</TouchableOpacity>
</View>
</View>
{/* 评分系统卡片 */}
<View style={styles.card}>
<Text style={styles.cardTitle}>⭐ 评分系统</Text>
<View style={styles.ratingContainer}>
{[1, 2, 3, 4, 5].map((star) => {
const filled = star <= rating;
const halfFilled = star - 0.5 === rating;
return (
<TouchableOpacity
key={star}
onPress={() => saveRating(star)}
onLongPress={() => saveRating(star - 0.5)}
style={styles.starButton}
>
<Text style={styles.star}>
{filled ? '⭐' : halfFilled ? '✨' : '☆'}
</Text>
</TouchableOpacity>
);
})}
<Text style={styles.ratingValue}>{rating.toFixed(1)}</Text>
</View>
{/* 滑块精确评分 */}
<View style={styles.sliderContainer}>
<Text style={styles.sliderLabel}>精确评分: {rating.toFixed(1)}</Text>
<Slider
value={rating}
minimumValue={0}
maximumValue={5}
step={0.5}
onValueChange={setRating}
onSlidingComplete={saveRating}
minimumTrackTintColor="#FFD700"
maximumTrackTintColor="#E5E6EB"
thumbTintColor="#FFD700"
/>
</View>
{/* 统计信息 */}
<View style={styles.statsRow}>
<Text style={styles.statText}>
平均评分: {userRatings.length > 0 ? (userRatings.reduce((sum, r) => sum + r, 0) / userRatings.length).toFixed(1) : '0.0'}
</Text>
<Text style={styles.statText}>
评分次数: {userRatings.length}
</Text>
</View>
</View>
{/* 双向绑定卡片 */}
<View style={styles.card}>
<Text style={styles.cardTitle}>🔄 双向数据绑定</Text>
<Slider
value={sliderValue}
minimumValue={0}
maximumValue={100}
step={1}
onValueChange={handleSliderChange}
minimumTrackTintColor="#9C27B0"
maximumTrackTintColor="#E5E6EB"
thumbTintColor="#9C27B0"
/>
<TextInput
style={styles.input}
value={inputValue}
onChangeText={handleInputChange}
onBlur={handleInputBlur}
keyboardType="numeric"
placeholder="请输入 0-100 的数值"
/>
<Text style={styles.valueText}>当前值: {sliderValue}</Text>
</View>
{/* 使用说明 */}
<View style={styles.card}>
<Text style={styles.cardTitle}>💡 使用说明</Text>
<Text style={styles.instructionText}>
• 下载进度条:只读显示,自动更新,颜色随进度变化
</Text>
<Text style={styles.instructionText}>
• 视频进度条:支持拖拽跳转,拖拽时显示预览时间
</Text>
<Text style={styles.instructionText}>
• 评分系统:支持整数和半星评分,自动保存和统计
</Text>
<Text style={styles.instructionText}>
• 双向绑定:滑块和输入框同步,实时更新
</Text>
<Text style={[styles.instructionText, { color: '#F44336', fontWeight: '600' }]}>
⚠️ 注意: 视频拖拽时不会暂停播放
</Text>
<Text style={[styles.instructionText, { color: '#4CAF50', fontWeight: '600' }]}>
💡 提示: 评分数据自动持久化存储
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F7FA',
},
header: {
padding: 20,
backgroundColor: '#FFFFFF',
borderBottomWidth: 1,
borderBottomColor: '#EBEEF5',
},
headerTitle: {
fontSize: 24,
fontWeight: '700',
color: '#303133',
marginBottom: 8,
},
headerSubtitle: {
fontSize: 16,
fontWeight: '500',
color: '#909399',
},
content: {
flex: 1,
padding: 16,
},
card: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
marginBottom: 16,
padding: 16,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 4,
},
cardTitle: {
fontSize: 18,
fontWeight: '600',
color: '#303133',
marginBottom: 16,
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
},
infoText: {
fontSize: 14,
color: '#606266',
},
button: {
backgroundColor: '#409EFF',
borderRadius: 8,
padding: 12,
alignItems: 'center',
},
buttonDisabled: {
backgroundColor: '#C0C4CC',
},
buttonText: {
fontSize: 14,
color: '#FFFFFF',
fontWeight: '600',
},
timeRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
timeText: {
fontSize: 16,
color: '#303133',
fontWeight: '500',
},
controlsRow: {
flexDirection: 'row',
gap: 12,
},
controlButton: {
flex: 1,
backgroundColor: '#E5E6EB',
borderRadius: 8,
padding: 12,
alignItems: 'center',
},
controlText: {
fontSize: 14,
color: '#303133',
fontWeight: '600',
},
ratingContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
},
starButton: {
marginRight: 8,
},
star: {
fontSize: 32,
},
ratingValue: {
fontSize: 24,
fontWeight: '700',
color: '#FFD700',
marginLeft: 8,
},
sliderContainer: {
marginTop: 16,
},
sliderLabel: {
fontSize: 14,
color: '#606266',
marginBottom: 8,
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 16,
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: '#EBEEF5',
},
statText: {
fontSize: 14,
color: '#606266',
},
input: {
borderWidth: 1,
borderColor: '#E5E6EB',
borderRadius: 8,
padding: 12,
fontSize: 16,
marginTop: 16,
},
valueText: {
fontSize: 16,
color: '#606266',
marginTop: 8,
textAlign: 'center',
},
instructionText: {
fontSize: 14,
lineHeight: 22,
marginBottom: 8,
color: '#606266',
},
});
export default ProgressRatingScreen;
四、高级应用场景
1. 多段式进度条
typescript
const MultiSegmentProgressBar = () => {
const segments = [
{ label: '第一步', progress: 100, color: '#4CAF50' },
{ label: '第二步', progress: 60, color: '#2196F3' },
{ label: '第三步', progress: 0, color: '#E5E6EB' },
];
return (
<View>
{segments.map((segment, index) => (
<View key={index}>
<Text>{segment.label}: {segment.progress}%</Text>
<Slider
value={segment.progress}
disabled={true}
minimumTrackTintColor={segment.color}
maximumTrackTintColor="#E5E6EB"
thumbTintColor={segment.color}
/>
</View>
))}
</View>
);
};
2. 范围选择器
typescript
const RangeSelector = () => {
const [minValue, setMinValue] = useState(30);
const [maxValue, setMaxValue] = useState(70);
return (
<View>
<Text>范围: {minValue} - {maxValue}</Text>
<Slider
value={minValue}
minimumValue={0}
maximumValue={maxValue - 10}
onValueChange={setMinValue}
/>
<Slider
value={maxValue}
minimumValue={minValue + 10}
maximumValue={100}
onValueChange={setMaxValue}
/>
</View>
);
};
3. 滑块验证表单
typescript
const SliderValidationForm = () => {
const [age, setAge] = useState(25);
const [salary, setSalary] = useState(50000);
const [errors, setErrors] = useState({});
const validateAge = () => {
if (age < 18) {
setErrors({ ...errors, age: '年龄必须大于等于18岁' });
return false;
}
return true;
};
const validateSalary = () => {
if (salary < 0) {
setErrors({ ...errors, salary: '薪资不能为负数' });
return false;
}
return true;
};
return (
<View>
<Text>年龄: {age}</Text>
<Slider
value={age}
minimumValue={0}
maximumValue={100}
onValueChange={(value) => {
setAge(value);
if (validateAge()) {
delete errors.age;
}
}}
/>
{errors.age && <Text style={styles.errorText}>{errors.age}</Text>}
<Text>薪资: {salary}</Text>
<Slider
value={salary}
minimumValue={0}
maximumValue={200000}
step={1000}
onValueChange={(value) => {
setSalary(value);
if (validateSalary()) {
delete errors.salary;
}
}}
/>
{errors.salary && <Text style={styles.errorText}>{errors.salary}</Text>}
</View>
);
};
五、总结
本文深入讲解了如何使用 @react-native-ohos/slider 组件实现进度条和评分系统,涵盖了下载进度、视频播放进度、5星评分系统、双向数据绑定等多个实用场景。
关键技术要点:
- 进度条 vs 滑块:理解两者的本质区别和应用场景
- 禁用状态的使用:实现只读进度条,防止用户干扰
- 拖拽状态管理:精确控制视频播放进度,避免冲突
- 评分系统实现:支持整数、半星评分,自动统计和持久化
- 双向数据绑定:滑块与输入框同步,避免无限循环
- 输入验证:边界检查、类型转换、格式化
- 颜色动态变化:根据进度/评分改变视觉反馈
- 性能优化:合理使用 useRef、避免不必要渲染
通过本文的学习,你应该能够:
- 理解滑块组件的多种应用场景
- 实现不同类型的进度条
- 构建完整的评分系统
- 掌握双向数据绑定的最佳实践
- 处理复杂的交互逻辑
- 优化组件性能
在鸿蒙平台上,@react-native-ohos/slider 组件提供了完整的支持,与 iOS/Android 平台保持一致的 API,让开发者可以轻松实现跨平台的进度控制和评分功能。