React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3
React-Native开发鸿蒙NEXT-svg绘制睡眠质量图part3FBI 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]