找回密码
 立即注册
首页 业界区 安全 React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3 ...

React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3

邹语彤 2025-5-30 15:30:39
React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3

FBI WARNING:
The complete demo will be posted at the end of the series, so no need to worry.

还剩下最后一步:让睡眠阶段的区域形状上更加贴合边线,颜色上做些渐变。

首先想到的是切图,其次是自己去绘制这个区域。根据区域和区域左右两条线的样式,如果把线在下区域在上看做上升趋势up,反之则是下降趋势down,这样让UI切了四张图。

svg格式的图片可以直接在画布上展示。
  1. import SvgSleepUpUp from '../../svg/sleep_upup.svg';
  2. import SvgSleepUpDown from '../../svg/sleep_updown.svg';
  3. import SvgSleepDownDown from '../../svg/sleep_downdown.svg';
  4. import SvgSleepDownUp from '../../svg/sleep_downup.svg';
复制代码
根据上面提到的up-down来定义一个映射表
  1. // 图片映射表
  2. const SVG_MAP = {
  3.   [`${UpDownEnum.UP}-${UpDownEnum.UP}`]: SvgSleepUpUp,
  4.   [`${UpDownEnum.UP}-${UpDownEnum.DOWN}`]: SvgSleepUpDown,
  5.   [`${UpDownEnum.DOWN}-${UpDownEnum.DOWN}`]: SvgSleepDownDown,
  6.   [`${UpDownEnum.DOWN}-${UpDownEnum.UP}`]: SvgSleepDownUp,
  7. };
复制代码
还是通过在calcPointData的时候,替代原先Rect矩形区域即可,当然需要计算下矩形区域左右两条边线的样式来确定用哪哪个图片
  1. if (index - 1 >= 0) {
  2.   const prevPoint = points[index - 1];
  3.   const isSameStage = point.stage === prevPoint.stage;
  4.   areaData.areaWidth += point.x - prevPoint.x;
  5.   let deltaY1 = 0;
  6.   let deltaY2 = 0;
  7.   if (!isSameStage) {
  8.     if (point.stage > prevPoint.stage) {
  9.       areaData.areaRightUpDown = UpDownEnum.UP;
  10.       deltaY1 = -AREA_HEIGHT;
  11.     } else {
  12.       areaData.areaRightUpDown = UpDownEnum.DOWN;
  13.       deltaY2 = -AREA_HEIGHT;
  14.     }
  15.     areaData.areaEndIndex = index;
  16.     console.log('areaData.areaEndIndex = ' + index);
  17.     const SvgComponent =
  18.       SVG_MAP[`${areaData.areaLeftUpDown}-${areaData.areaRightUpDown}`];
  19.     const beginIndex = areaData.areaBeginIndex;
  20.     const endIndex = areaData.areaEndIndex;
  21.     areaDataList.push(areaData);
  22.     // 输出
  23.     svgsTemp.push(
  24.       <G
  25.         key={`icon1-${index}`}
  26.         transform={`translate(${areaData.aeraX}, ${areaData.aeraY})`}>
  27.         <SvgComponent
  28.           key={`connector-${areaData.areaIndex}`}
  29.           x={areaData.aeraX}
  30.           y={areaData.aeraY}
  31.           width={areaData.areaWidth}
  32.           height={areaData.areaHeight}
  33.           fill={`url(#gradient${areaData.areaStage})`}
  34.           preserveAspectRatio="none" // 禁用比例保持
  35.           transform={`translate(0 0)`} // 消除SVG内置偏移
  36.         />
  37.       </G>,
复制代码
效果上还有点生硬,记得svg里可以设置允许缩放拉伸的范围。svg图片添加渐变色试了下似乎无效,需要后面再找找办法。后续正式开发的时候根据标注调整下,再细化下切图样式还有较大提升空间。正式开发到这个功能要到五一后了,届时再来交流下效果,先附上完整的demo代码与几个svg图片源码。源码里还有个滑块与指针可以拖拽选选择,会返回选中的睡眠阶段的开始时间点与结束时间点,用于显示当前选中的睡眠阶段信息。这块和绘制关系不大,看下源码即可了解。

完整源码
  1. // 基于react-native-svg实现的绘制睡眠阶段图标import {color} from 'echarts';import React, {  JSX,  useMemo,  useState,  useCallback,  useEffect,  useRef,} from 'react';import {View, Dimensions, Text, StyleSheet, PanResponder, InteractionManager} from 'react-native';import Svg, {  G,  Rect,  LinearGradient,  Defs,  Filter,  FeDropShadow,  Circle,  Line,  Stop,  Path,  Image,  Polygon,} from 'react-native-svg';import SvgSleepUpUp from '../../svg/sleep_upup.svg';
  2. import SvgSleepUpDown from '../../svg/sleep_updown.svg';
  3. import SvgSleepDownDown from '../../svg/sleep_downdown.svg';
  4. import SvgSleepDownUp from '../../svg/sleep_downup.svg';// 组件目前默认按照屏幕宽度为基准进行布局const {width: SCREEN_WIDTH} = Dimensions.get('window');const SHOW_DATA_POINT = false; // 是否在图标展示数据点(调试阶段可以更清晰核对数据与图是否一致)const MARGIN = 24; // 左右间隙,用于支持底部指针图标的左右拖拽const POINT_RADIUS = 4; // 数据点的弧度const AREA_HEIGHT = 30; // 阶段形状的高度const CHART_HEIGHT = AREA_HEIGHT * 9; // 图表的整体高度,可以调整// const AREA_LINE_HEIGHT = 20; // 阶段形状线的高度// 自定义形状配置字典const STAGE_CONFIG = {  0: {    gradient: ['#8314f3', '#3800FF'], // 渐变色设置    useColorTransform: true, // 是否使用颜色变换    transformX1: '0%',    transformX2: '0%',    transformY1: '0%',    transformY2: '100%',    shadow: '#3800FF', // 阴影颜色    color: '#3800FF', // 默认颜色    label: '深睡眠', // 文字标签  },  1: {    gradient: ['#d248b0', '#ba2eec'],    useColorTransform: true,    transformX1: '0%',    transformX2: '0%',    transformY1: '0%',    transformY2: '100%',    color: '#ba2eec',    shadow: '#ba2eec',    label: '浅睡眠',  },  2: {    gradient: ['#e05891', '#f86d5a'],    useColorTransform: true,    transformX1: '0%',    transformX2: '0%',    transformY1: '0%',    transformY2: '100%',    shadow: '#f86d5a',    color: '#f86d5a',    label: '快速眼动',  },  3: {    gradient: ['#fb8b44', '#fcbb29'],    useColorTransform: true,    transformX1: '0%',    transformX2: '0%',    transformY1: '0%',    transformY2: '100%',    shadow: '#fcbb29',    color: '#fcbb29',    label: '清醒',  },};// 示例数据const sleepData: SleepStageModel[] = [  {time: '0:00', stage: 3},  {time: '5:00', stage: 3},  {time: '10:00', stage: 1},  {time: '15:00', stage: 1},  {time: '20:00', stage: 3},  {time: '25:00', stage: 0},  {time: '30:00', stage: 0},  {time: '35:00', stage: 0},  {time: '40:00', stage: 0},  {time: '45:00', stage: 1},  {time: '50:00', stage: 1},  {time: '55:00', stage: 3},  {time: '60:00', stage: 3},  {time: '65:00', stage: 2},  {time: '70:00', stage: 2},  {time: '75:00', stage: 1},  {time: '80:00', stage: 3},  {time: '85:00', stage: 2},  {time: '0:00', stage: 3},  {time: '5:00', stage: 3},  {time: '10:00', stage: 1},  {time: '15:00', stage: 1},  {time: '20:00', stage: 3},  {time: '25:00', stage: 0},  // {time: '30:00', stage: 0},  // {time: '35:00', stage: 0},  // {time: '40:00', stage: 0},  // {time: '45:00', stage: 1},  // {time: '50:00', stage: 1},  // {time: '55:00', stage: 3},  // {time: '60:00', stage: 3},  // {time: '65:00', stage: 2},  // {time: '70:00', stage: 2},  // {time: '75:00', stage: 1},  // {time: '80:00', stage: 3},  // {time: '85:00', stage: 2},  // ...更多数据];enum SleepStageEnum {  Deep = 0, // 0 深睡眠  Light, // 1 浅睡眠  REM, // 2 快速眼动  AWAKE, // 3 清醒}enum UpDownEnum {  NONE = 'none', // none 未设置  UP = 'up', // up 上升  DOWN = 'down', // down 下降}// 图片映射表
  5. const SVG_MAP = {
  6.   [`${UpDownEnum.UP}-${UpDownEnum.UP}`]: SvgSleepUpUp,
  7.   [`${UpDownEnum.UP}-${UpDownEnum.DOWN}`]: SvgSleepUpDown,
  8.   [`${UpDownEnum.DOWN}-${UpDownEnum.DOWN}`]: SvgSleepDownDown,
  9.   [`${UpDownEnum.DOWN}-${UpDownEnum.UP}`]: SvgSleepDownUp,
  10. };interface SleepStageModel {  time: string; // 采集时间点  stage: SleepStageEnum; // 睡眠阶段  [key: string]: any; // fixme:允许任意数量的其他属性}interface SleepAreaData {  areaIndex: number; // 区域索引  aeraX: number; // 区域X坐标  aeraY: number; // 区域Y坐标  areaWidth: number; // 区域宽度  areaHeight: number; // 区域宽度  areaColor: any | null; // 区域颜色  areaStoke: any | null; // 区域阴影  areaLeftUpDown: UpDownEnum; // 区域左边上升下降趋势  areaRightUpDown: UpDownEnum; // 区域左边上升下降趋势  areaStage: number; // 睡眠阶段  areaBeginIndex: number; // 区域开始索引  areaEndIndex: number; // 区域结束索引}const SleepStageChart = ({data}: {data: SleepStageModel[]}) => {  // 添加滑块位置状态  const [sliderPosition, setSliderPosition] = useState(MARGIN);  // 滑块是否在拖拽的判定  const [isDragging, setIsDragging] = useState(false);  // 睡眠区域数据,用于计算这段睡眠的详细数据  let areaDataList: SleepAreaData[] = [];  // 添加 ref 存储实时位置(针对目前在android设备上滑块拖动不如ios上丝滑)  const sliderPositionRef = useRef(MARGIN);  // 添加节流标识(针对目前在android设备上滑块拖动不如ios上丝滑)  const lastUpdate = useRef(Date.now());  const THROTTLE_DELAY = 16; // 约 60fps  const panResponder = useMemo(    () =>      PanResponder.create({        onStartShouldSetPanResponder: () => true,        onMoveShouldSetPanResponder: () => true,        onPanResponderGrant: () => {          setIsDragging(true);        },        onPanResponderMove: (_, gestureState) => {          // const newX = Math.min(SCREEN_WIDTH - MARGIN, gestureState.moveX);          // const newPosition = Math.max(MARGIN, newX);          // setSliderPosition(newPosition);          // // 区域计算          // for (let i = 0; i < areaDataList.length - 1; i++) {          //   if (          //     points[areaDataList[i].areaBeginIndex].x = newX          //   ) {          //     setCurrentAreaIndex(i);          //     break;          //   }          // }          const now = Date.now();          const newX = Math.min(SCREEN_WIDTH - MARGIN, gestureState.moveX);          const newPosition = Math.max(MARGIN, newX);          // 使用 ref 存储实时位置          sliderPositionRef.current = newPosition;          // 添加节流逻辑          if (now - lastUpdate.current >= THROTTLE_DELAY) {            updateSliderPosition(newPosition);            lastUpdate.current = now;            // 区域计算            for (let i = 0; i < areaDataList.length - 1; i++) {              if (                points[areaDataList[i].areaBeginIndex].x = newX              ) {                setCurrentAreaIndex(i);                break;              }            }          }        },        onPanResponderRelease: () => {          setIsDragging(false);                    // 结束时确保显示最终位置                    updateSliderPosition(sliderPositionRef.current);        },      }),    [],  );  // requestAnimationFrame优化视图更新(针对目前在android设备上滑块拖动不如ios上丝滑)  const updateSliderPosition = (position: number) => {    requestAnimationFrame(() => {      setSliderPosition(position);    });  };  // 添加当前选中区域的状态  const [currentAreaIndex, setCurrentAreaIndex] = useState(null);  const points = useMemo(    () =>      data.map((item, index) => ({        x: (index * (SCREEN_WIDTH - MARGIN * 2)) / (data.length - 1) + MARGIN,        y: CHART_HEIGHT - (item.stage * 2 + 1) * AREA_HEIGHT - AREA_HEIGHT,        stage: item.stage,        data: item,      })),    [data],  );  // 添加点击处理函数  const handleAreaPress = useCallback(    (beginIndex: number, endIndex: number) => {      InteractionManager.runAfterInteractions(() => {        console.log('点击区域 - 阶段数据:', beginIndex, '-', endIndex);              // 这里可以添加更多处理逻辑...      });    },    [],  ); // 空依赖数组,因为函数不依赖任何外部变量  const initAreaData = () => {    let areaData: SleepAreaData = {      areaIndex: -1,      aeraX: -1,      aeraY: -1,      areaWidth: 0,      areaHeight: AREA_HEIGHT,      areaColor: null,      areaStoke: null,      areaLeftUpDown: UpDownEnum.NONE,      areaRightUpDown: UpDownEnum.NONE,      areaStage: 0,      areaBeginIndex: 0,      areaEndIndex: 0,    };    return areaData;  };  // 修改 useEffect,移除自动吸附逻辑  useEffect(() => {    console.log('选中了' + currentAreaIndex);  }, [currentAreaIndex]);  // 添加绘制参考线的函数  const renderReferenceLines = () => {    const lines: JSX.Element[] = [];    // 计算需要绘制的虚线数量    const lineCount = Math.floor(CHART_HEIGHT / AREA_HEIGHT);    for (let i = 0; i  {    let areaData = initAreaData();    let svgsTemp: JSX.Element[] = [];    points.map((point, index) => {      if (index == 0) {        areaData.areaIndex = index;        areaData.aeraX = point.x;        areaData.aeraY = point.y;        areaData.areaWidth = 0;        areaData.areaColor = `url(#gradient${point.stage})`;        areaData.areaStoke = STAGE_CONFIG[point.stage].shadow;        areaData.areaLeftUpDown = UpDownEnum.UP;        areaData.areaStage = point.stage;        areaData.areaBeginIndex = index;      }      if (index - 1 >= 0) {        const prevPoint = points[index - 1];        const isSameStage = point.stage === prevPoint.stage;        areaData.areaWidth += point.x - prevPoint.x;        let deltaY1 = 0;        let deltaY2 = 0;        if (!isSameStage) {          if (point.stage > prevPoint.stage) {            areaData.areaRightUpDown = UpDownEnum.UP;            deltaY1 = -AREA_HEIGHT;          } else {            areaData.areaRightUpDown = UpDownEnum.DOWN;            deltaY2 = -AREA_HEIGHT;          }          areaData.areaEndIndex = index;          console.log('areaData.areaEndIndex = ' + index);          const SvgComponent =            SVG_MAP[`${areaData.areaLeftUpDown}-${areaData.areaRightUpDown}`];          const beginIndex = areaData.areaBeginIndex;          const endIndex = areaData.areaEndIndex;          areaDataList.push(areaData);          // 输出          svgsTemp.push(                                      ,            // ,          );          {            /* 添加透明的可点击背景 */          }          svgsTemp.push(             handleAreaPress(beginIndex, endIndex)}              // 可选:添加点击反馈效果              // opacity={selectedArea === areaData.areaIndex ? 0.1 : 0}            />,          );          // 出一条线          if (index != data.length - 1) {            svgsTemp.push(              ,            );          }          // 重新开始绘制矩形          areaData = initAreaData();          areaData.areaIndex = index;          areaData.aeraX = point.x;          areaData.aeraY = point.y;          areaData.areaWidth = 0;          areaData.areaColor = STAGE_CONFIG[point.stage].color;          areaData.areaStoke = STAGE_CONFIG[point.stage].shadow;          areaData.areaStage = point.stage;          areaData.areaBeginIndex = index;          if (point.stage > prevPoint.stage) {            areaData.areaLeftUpDown = UpDownEnum.UP;          } else {            areaData.areaLeftUpDown = UpDownEnum.DOWN;          }        }      }      if (index == points.length - 1) {        areaData.areaRightUpDown = UpDownEnum.DOWN;        areaData.areaEndIndex = index;        const beginIndex = areaData.areaBeginIndex;        const endIndex = areaData.areaEndIndex;        const SvgComponent =          SVG_MAP[`${areaData.areaLeftUpDown}-${areaData.areaRightUpDown}`];        // 输出        areaDataList.push(areaData);        svgsTemp.push(                                ,            // ,        );        // 添加透明的可点击背景        svgsTemp.push(           handleAreaPress(beginIndex, endIndex)}            // 可选:添加点击反馈效果            // opacity={selectedArea === areaData.areaIndex ? 0.1 : 0}          />,        );      }    });    return svgsTemp;  };  return (          {/* 日 周 月 */}      {/* 阶段图例 */}              {Object.entries(STAGE_CONFIG).map(([stage, config]) => (                                  {config.label}                  ))}            {/* 可视化图表 */}              {/* 添加参考线 */}        {renderReferenceLines()}        {/* 区域的渐变定义 */}                  {Object.entries(STAGE_CONFIG).map(([stage, config]) => (                                                              ))}                {/* 连接线绘制 */}        {data.length > 1 && calcPointData()}        {/* 数据点,在睡眠图里不表示,打开可以更清楚的观察数据(用于调试阶段) */}        {SHOW_DATA_POINT &&          points.map((point, index) => (                                                ))}        {/* 添加指示线 */}                {/* 添加滑块 */}        {           {/* 滑块轨道 */}                    {/* 滑块拖拽的圆圈 */}                  }            );};const styles = StyleSheet.create({  container: {    backgroundColor: 'white',    borderRadius: 0,    padding: 0,    margin: 0,    shadowColor: '#000',    shadowOffset: {width: 0, height: 2},    shadowOpacity: 0.1,    shadowRadius: 4,  },  legend: {    flexDirection: 'row',    justifyContent: 'space-around',    marginBottom: 16,  },  legendItem: {    flexDirection: 'row',    alignItems: 'center',  },  legendDot: {    width: 12,    height: 12,    borderRadius: 6,    marginRight: 4,  },  legendText: {    color: '#666',    fontSize: 12,  },  sliderContainer: {    position: 'relative',    height: 40,    marginTop: 10,  },  slider: {    position: 'absolute',    width: 20,    height: 20,    borderRadius: 10,    backgroundColor: 'white',    borderWidth: 2,    borderColor: '#666',    transform: [{translateX: -10}, {translateY: -10}],  },});export {SleepStageChart};const DefaultExport = () => ;export default DefaultExport;
复制代码
几个svg图片的源码。
sleep_downdown.svg
  1. [/code]sleep_downup.svg
  2. [code]
复制代码
sleep_updown.svg
  1. [/code]sleep_upup.svg
  2. [code]
复制代码
欢迎交流关于睡眠质量图的各种实现方式~
不经常在线,有问题可在微信公众号或者掘金社区私信留言
更多内容可关注
我的公众号悬空八只脚

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册