邹语彤 发表于 2025-5-30 15:30:39

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

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

FBI WARNING:
The complete demo will be posted at the end of the series, so no need to worry.
还剩下最后一步:让睡眠阶段的区域形状上更加贴合边线,颜色上做些渐变。
https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/e534d86f2ef24738bb6da8eb33d550d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oKs56m65YWr5Y-q6ISa:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjA0MTE1Mzk0NzE3Mzg1MyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1748871029&x-orig-sign=6v99wLLLgwzjjlZWzauwS3CjPgA%3D
首先想到的是切图,其次是自己去绘制这个区域。根据区域和区域左右两条线的样式,如果把线在下区域在上看做上升趋势up,反之则是下降趋势down,这样让UI切了四张图。
https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/dd220b3b1e374ad597451300661dfa42~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oKs56m65YWr5Y-q6ISa:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjA0MTE1Mzk0NzE3Mzg1MyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1748871029&x-orig-sign=k9lpXnV%2BMyw%2FXR3Qas2cItAkTIY%3D
svg格式的图片可以直接在画布上展示。
import SvgSleepUpUp from '../../svg/sleep_upup.svg';
import SvgSleepUpDown from '../../svg/sleep_updown.svg';
import SvgSleepDownDown from '../../svg/sleep_downdown.svg';
import SvgSleepDownUp from '../../svg/sleep_downup.svg';根据上面提到的up-down来定义一个映射表
// 图片映射表
const SVG_MAP = {
[`${UpDownEnum.UP}-${UpDownEnum.UP}`]: SvgSleepUpUp,
[`${UpDownEnum.UP}-${UpDownEnum.DOWN}`]: SvgSleepUpDown,
[`${UpDownEnum.DOWN}-${UpDownEnum.DOWN}`]: SvgSleepDownDown,
[`${UpDownEnum.DOWN}-${UpDownEnum.UP}`]: SvgSleepDownUp,
};还是通过在calcPointData的时候,替代原先Rect矩形区域即可,当然需要计算下矩形区域左右两条边线的样式来确定用哪哪个图片
if (index - 1 >= 0) {
const prevPoint = points;
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(
      <G
      key={`icon1-${index}`}
      transform={`translate(${areaData.aeraX}, ${areaData.aeraY})`}>
      <SvgComponent
          key={`connector-${areaData.areaIndex}`}
          x={areaData.aeraX}
          y={areaData.aeraY}
          width={areaData.areaWidth}
          height={areaData.areaHeight}
          fill={`url(#gradient${areaData.areaStage})`}
          preserveAspectRatio="none" // 禁用比例保持
          transform={`translate(0 0)`} // 消除SVG内置偏移
      />
      </G>,效果上还有点生硬,记得svg里可以设置允许缩放拉伸的范围。svg图片添加渐变色试了下似乎无效,需要后面再找找办法。后续正式开发的时候根据标注调整下,再细化下切图样式还有较大提升空间。正式开发到这个功能要到五一后了,届时再来交流下效果,先附上完整的demo代码与几个svg图片源码。源码里还有个滑块与指针可以拖拽选选择,会返回选中的睡眠阶段的开始时间点与结束时间点,用于显示当前选中的睡眠阶段信息。这块和绘制关系不大,看下源码即可了解。
https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ea5ac918dbc44fa58b4287b89eb9d4e7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oKs56m65YWr5Y-q6ISa:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiMjA0MTE1Mzk0NzE3Mzg1MyJ9&rk3s=f64ab15b&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1748871029&x-orig-sign=02e%2FK0NkHs6r3Bb4hvKYH7FrLmg%3D
完整源码
// 基于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';
import SvgSleepUpDown from '../../svg/sleep_updown.svg';
import SvgSleepDownDown from '../../svg/sleep_downdown.svg';
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 下降}// 图片映射表
const SVG_MAP = {
[`${UpDownEnum.UP}-${UpDownEnum.UP}`]: SvgSleepUpUp,
[`${UpDownEnum.UP}-${UpDownEnum.DOWN}`]: SvgSleepUpDown,
[`${UpDownEnum.DOWN}-${UpDownEnum.DOWN}`]: SvgSleepDownDown,
[`${UpDownEnum.DOWN}-${UpDownEnum.UP}`]: SvgSleepDownUp,
};interface SleepStageModel {time: string; // 采集时间点stage: SleepStageEnum; // 睡眠阶段: 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 = useState(MARGIN);// 滑块是否在拖拽的判定const = useState(false);// 睡眠区域数据,用于计算这段睡眠的详细数据let areaDataList: SleepAreaData[] = [];// 添加 ref 存储实时位置(针对目前在android设备上滑块拖动不如ios上丝滑)const sliderPositionRef = useRef(MARGIN);// 添加节流标识(针对目前在android设备上滑块拖动不如ios上丝滑)const lastUpdate = useRef(Date.now());const THROTTLE_DELAY = 16; // 约 60fpsconst 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.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.areaBeginIndex].x = newX            ) {                setCurrentAreaIndex(i);                break;            }            }          }      },      onPanResponderRelease: () => {          setIsDragging(false);                  // 结束时确保显示最终位置                  updateSliderPosition(sliderPositionRef.current);      },      }),    [],);// requestAnimationFrame优化视图更新(针对目前在android设备上滑块拖动不如ios上丝滑)const updateSliderPosition = (position: number) => {    requestAnimationFrame(() => {      setSliderPosition(position);    });};// 添加当前选中区域的状态const = 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,      })),    ,);// 添加点击处理函数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);}, );// 添加绘制参考线的函数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.shadow;      areaData.areaLeftUpDown = UpDownEnum.UP;      areaData.areaStage = point.stage;      areaData.areaBeginIndex = index;      }      if (index - 1 >= 0) {      const prevPoint = points;      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.color;          areaData.areaStoke = STAGE_CONFIG.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(() => (                                  {config.label}                  ))}            {/* 可视化图表 */}            {/* 添加参考线 */}      {renderReferenceLines()}      {/* 区域的渐变定义 */}                  {Object.entries(STAGE_CONFIG).map(() => (                                                            ))}                {/* 连接线绘制 */}      {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
sleep_downup.svg
sleep_updown.svg
sleep_upup.svg
欢迎交流关于睡眠质量图的各种实现方式~
不经常在线,有问题可在微信公众号或者掘金社区私信留言
更多内容可关注
我的公众号悬空八只脚

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3