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格式的图片可以直接在画布上展示。- 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[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(
- <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图片源码。源码里还有个滑块与指针可以拖拽选选择,会返回选中的睡眠阶段的开始时间点与结束时间点,用于显示当前选中的睡眠阶段信息。这块和绘制关系不大,看下源码即可了解。

完整源码- // 基于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; // 睡眠阶段 [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- [/code]sleep_downup.svg
- [code]
复制代码 sleep_updown.svg- [/code]sleep_upup.svg
- [code]
复制代码 欢迎交流关于睡眠质量图的各种实现方式~
不经常在线,有问题可在微信公众号或者掘金社区私信留言
更多内容可关注
我的公众号悬空八只脚
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |