高级进阶 React Native 鸿蒙跨平台开发:slider 滑块组件 - 进度条与评分系统

欢迎加入开源鸿蒙跨平台社区: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>
  );
};

技术深度解析

  1. 进度更新的时机控制

    typescript 复制代码
    // 使用 setInterval 模拟下载过程
    const interval = setInterval(() => {
      currentProgress += Math.random() * 10; // 每次增加随机进度
      if (currentProgress >= 100) {
        clearInterval(interval); // 完成时清除定时器
        setDownloading(false);
      }
      setProgress(currentProgress);
    }, 200);
    • 间隔时间选择:200ms 提供流畅的视觉效果,但不会过于频繁更新
    • 进度增量随机:模拟真实网络环境,避免线性的不真实感
    • 清理定时器:完成后必须清理,避免内存泄漏
  2. 禁用状态的视觉设计

    typescript 复制代码
    disabled={true}  // 禁用用户交互
    • 用户体验:下载中用户不能干预进度
    • 视觉提示 :禁用状态下滑块颜色会变灰(通过 thumbTintColor
    • 语义正确:进度条的本质就是只读的显示组件
  3. 颜色状态机的实现

    typescript 复制代码
    const getProgressColor = () => {
      if (progress < 30) return '#F44336'; // 红色
      if (progress < 70) return '#FF9800'; // 橙色
      return '#4CAF50'; // 绿色
    };
    • 三阶段设计:刚开始、进行中、即将完成
    • 颜色心理学:红色表示警告/等待,橙色表示进行中,绿色表示成功
    • 动态更新:颜色随进度实时变化,提供直观反馈
  4. 下载状态的完整管理

    typescript 复制代码
    const [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>
  );
};

技术深度解析

  1. 拖拽状态的精细控制

    typescript 复制代码
    const [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)
    • 状态隔离:拖拽时显示预览时间,不干扰实际的播放进度
  2. 播放进度的更新策略

    typescript 复制代码
    const handleProgress = (data: { currentTime: number }) => {
      if (!isSeeking) {
        setCurrentTime(data.currentTime);
      }
    };
    • 条件更新 :只在非拖拽状态下更新 currentTime
    • 避免冲突:拖拽时用户控制的预览时间优先
    • 平滑过渡:拖拽结束后,预览时间自动平滑过渡到实际播放进度
  3. 时间格式化的细节

    typescript 复制代码
    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')}`;
    };
    • padStart 的作用:确保总是显示两位数(如 05 而不是 5)
    • 边界情况:处理超过 60 秒的时间
    • 可扩展性 :可以添加小时显示(%H:%M:%S
  4. 步进值的精确控制

    typescript 复制代码
    step={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>
  );
};

技术深度解析

  1. 评分的三种输入方式

    typescript 复制代码
    // 方式1:点击星星(整数评分)
    onPress={() => saveRating(star)}
    
    // 方式2:长按星星(半星评分)
    onLongPress={() => saveRating(star - 0.5)}
    
    // 方式3:拖动滑块(精确评分)
    <Slider
      step={0.5}
      onValueChange={setRating}
      onSlidingComplete={saveRating}
    />
    • 满足不同用户习惯:有些用户喜欢点击,有些喜欢精确控制
    • 半星支持 :通过 step={0.5} 实现
    • 一致性保证 :三种方式都更新同一个 rating 状态
  2. 星星状态的三种表示

    typescript 复制代码
    const filled = star <= rating;           // 完全填充(实心星)
    const halfFilled = star - 0.5 === rating; // 半填充(半星)
    const empty = !filled && !halfFilled;     // 空(空心星)
    • 实心星 ⭐:评分 >= 星星位置
    • 半星 ✨:评分在 星星位置 - 0.5 和 星星位置之间
    • 空心星 ☆:评分 < 星星位置 - 0.5
    • 视觉层次:三种状态清晰区分
  3. 评分统计的实现

    typescript 复制代码
    const averageRating = userRatings.length > 0
      ? userRatings.reduce((sum, r) => sum + r, 0) / userRatings.length
      : 0;
    • reduce 的作用:计算所有评分的总和
    • 边界处理userRatings.length === 0 时返回 0
    • 精度控制toFixed(1) 保留一位小数
  4. 持久化存储的实现

    typescript 复制代码
    AsyncStorage.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>
  );
};

技术深度解析

  1. 双向绑定的实现原理

    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(字符串)
    • 同步更新:一个变化时更新另一个
    • 类型转换:数字 ↔ 字符串,需要验证和转换
  2. 输入验证的边界处理

    typescript 复制代码
    const 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 验证:输入过程中允许临时无效值,失去焦点时才修正
    • 三步验证:解析为数字 → 检查是否有效 → 限制范围
    • 用户体验:避免输入过程中频繁报错
  3. 性能优化:避免无限循环

    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星评分系统、双向数据绑定等多个实用场景。

关键技术要点:

  1. 进度条 vs 滑块:理解两者的本质区别和应用场景
  2. 禁用状态的使用:实现只读进度条,防止用户干扰
  3. 拖拽状态管理:精确控制视频播放进度,避免冲突
  4. 评分系统实现:支持整数、半星评分,自动统计和持久化
  5. 双向数据绑定:滑块与输入框同步,避免无限循环
  6. 输入验证:边界检查、类型转换、格式化
  7. 颜色动态变化:根据进度/评分改变视觉反馈
  8. 性能优化:合理使用 useRef、避免不必要渲染

通过本文的学习,你应该能够:

  1. 理解滑块组件的多种应用场景
  2. 实现不同类型的进度条
  3. 构建完整的评分系统
  4. 掌握双向数据绑定的最佳实践
  5. 处理复杂的交互逻辑
  6. 优化组件性能

在鸿蒙平台上,@react-native-ohos/slider 组件提供了完整的支持,与 iOS/Android 平台保持一致的 API,让开发者可以轻松实现跨平台的进度控制和评分功能。

相关推荐
空白诗9 小时前
高级进阶React Native 鸿蒙跨平台开发:slider 滑块组件 - 音量调节器完整实现
react native·react.js·harmonyos
晓得迷路了9 小时前
栗子前端技术周刊第 116 期 - 2025 JS 状态调查结果、Babel 7.29.0、Vue Router 5...
前端·javascript·vue.js
How_doyou_do9 小时前
执行上下文、作用域、闭包 patch
javascript
叫我一声阿雷吧9 小时前
深入理解JavaScript作用域和闭包,解决变量访问问题
开发语言·javascript·ecmascript
iDao技术魔方9 小时前
深入Vue 3响应式系统:为什么嵌套对象修改后界面不更新?
javascript·vue.js·ecmascript
历程里程碑9 小时前
普通数组-----除了自身以外数组的乘积
大数据·javascript·python·算法·elasticsearch·搜索引擎·flask
摸鱼的春哥9 小时前
春哥的Agent通关秘籍07:5分钟实现文件归类助手【实战】
前端·javascript·后端
念念不忘 必有回响9 小时前
viepress:vue组件展示和源码功能
前端·javascript·vue.js
Amumu1213810 小时前
Vue3 Composition API(一)
开发语言·javascript·ecmascript