HarmonyOS APP开发—"步道"运动数据可视化App,需要用到这个库

做一个运动数据 App,首页要画圆环进度(今日步数完成度)、折线图(一周心率趋势)、SVG 路径(跑步轨迹)。用 PNG 画图表放大模糊,用三方图表库太重——@react-native-ohos/react-native-svg 直接用 Path/Circle/渐变画矢量图形,任意缩不失真。

📦 仓库地址:https://gitcode.com/CPF-RN/rntpc_react-native-svg | 安装:npm install @react-native-ohos/react-native-svg


写在前面

"步道"是一个运动数据可视化 App,首页 Dashboard 要展示:

  • 步数进度环:今日 8234/10000 步,圆环填充 82%
  • 心率折线图:最近 7 天静息心率趋势
  • 跑步轨迹:GPS 路径用 SVG Path 绘制在地图叠加层上
  • 卡路里仪表盘:半圆仪表盘 + 渐变填充

这些图形的共同特点:随数据动态变化、需要矢量精度、要用渐变和动画。PNG 图方案直接出局——放大模糊、不能动态变化。用 react-native-chart 库又太重,定制困难。

react-native-svg 让你像写 HTML SVG 一样在 RN 里画矢量图形——Path/Circle/Rect/Line/LinearGradient 全套元素,原生层渲染,任意缩放不失真,还能和 Reanimated 联动做动画。

这篇文章聊什么

  1. 步数进度环——Circle + strokeDashoffset 动画
  2. 心率折线图——动态生成 Path d 属性
  3. 卡路里仪表盘——渐变半圆 + 弧线

进度环

折线图

仪表盘

轨迹

运动数据 API

图表类型

Circle + strokeDashoffset

Path d 属性动态生成

Arc Path + LinearGradient

GPS 坐标 → SVG Path

Reanimated 驱动动画

原生渲染矢量图


第一步:安装

npm install @react-native-ohos/react-native-svg

第二步:步数进度环

用 Circle 的 strokeDasharray + strokeDashoffset 实现圆环进度:

import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg'
import { useEffect } from 'react'

const RADIUS = 70
const CIRCUMFERENCE = 2 * Math.PI * RADIUS // 周长
const TARGET = 10000
const CURRENT = 8234
const PERCENT = CURRENT / TARGET

function StepProgressRing() {
  const offset = CIRCUMFERENCE * (1 - PERCENT) // 未完成部分的长度

  return (
    <Svg width={180} height={180}>
      <Defs>
        <LinearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="100%">
          <Stop offset="0%" stopColor="#4ECDC4" />
          <Stop offset="100%" stopColor="#45B7D1" />
        </LinearGradient>
      </Defs>

      {/* 背景环 */}
      <Circle
        cx={90} cy={90} r={RADIUS}
        stroke="#F0F0F0" strokeWidth={12}
        fill="none"
      />

      {/* 进度环(渐变描边) */}
      <Circle
        cx={90} cy={90} r={RADIUS}
        stroke="url(#ringGrad)" strokeWidth={12}
        fill="none"
        strokeLinecap="round"
        // 旋转 -90 度让进度从顶部开始
        transform="rotate(-90, 90, 90)"
        strokeDasharray={CIRCUMFERENCE}  // 虚线长度=周长
        strokeDashoffset={offset}         // 偏移=未完成部分
      />

      {/* 中心文字 */}
      <SvgText x={90} y={85} textAnchor="middle" fontSize={32} fontWeight="bold" fill="#333">
        {CURRENT.toLocaleString()}
      </SvgText>
      <SvgText x={90} y={105} textAnchor="middle" fontSize={14} fill="#999">
        / {TARGET.toLocaleString()} 步
      </SvgText>
    </Svg>
  )
}

strokeDasharray={CIRCUMFERENCE} + strokeDashoffset={offset} 是 SVG 进度环的经典技巧——虚线长度等于圆周长,偏移量等于未完成部分,调整 offset 就能控制进度。


第三步:心率折线图

根据心率数据动态生成 Path 的 d 属性:

import Svg, { Path, Line, Text as SvgText } from 'react-native-svg'

const HEART_RATES = [62, 65, 61, 58, 60, 63, 59] // 最近 7 天静息心率
const WIDTH = 300
const HEIGHT = 120
const PADDING = 20

function HeartRateChart() {
  const min = Math.min(...HEART_RATES) - 5
  const max = Math.max(...HEART_RATES) + 5
  const range = max - min

  // 生成 Path d 属性:M x0 y0 L x1 y1 L x2 y2 ...
  const points = HEART_RATES.map((hr, i) => {
    const x = PADDING + (i / (HEART_RATES.length - 1)) * (WIDTH - 2 * PADDING)
    const y = HEIGHT - PADDING - ((hr - min) / range) * (HEIGHT - 2 * PADDING)
    return `${i === 0 ? 'M' : 'L'} ${x} ${y}`
  })
  const linePath = points.join(' ')

  // 填充区域路径(折线下方填充)
  const fillPath = `${linePath} L ${WIDTH - PADDING} ${HEIGHT - PADDING} L ${PADDING} ${HEIGHT - PADDING} Z`

  return (
    <Svg width={WIDTH} height={HEIGHT}>
      {/* 填充区域 */}
      <Path d={fillPath} fill="#4ECDC4" opacity={0.1} />

      {/* 折线 */}
      <Path d={linePath} stroke="#4ECDC4" strokeWidth={2} fill="none" />

      {/* 数据点 */}
      {HEART_RATES.map((hr, i) => {
        const x = PADDING + (i / (HEART_RATES.length - 1)) * (WIDTH - 2 * PADDING)
        const y = HEIGHT - PADDING - ((hr - min) / range) * (HEIGHT - 2 * PADDING)
        return <Circle key={i} cx={x} cy={y} r={3} fill="#4ECDC4" />
      })}
    </Svg>
  )
}

全程用 Path/Circle 原生元素——矢量渲染,在高 DPI 鸿蒙屏幕上线条清晰锐利,不存在 PNG 放大模糊的问题。


第四步:卡路里仪表盘(渐变弧线)

半圆仪表盘,用渐变弧线填充:

function CalorieGauge({ value, max }: { value: number; max: number }) {
  const percent = Math.min(value / max, 1)
  const startAngle = 180 // 左侧
  const endAngle = 180 + percent * 180 // 右侧

  // 计算弧线路径
  const cx = 100, cy = 100, r = 70
  const startX = cx + r * Math.cos((startAngle * Math.PI) / 180)
  const startY = cy + r * Math.sin((startAngle * Math.PI) / 180)
  const endX = cx + r * Math.cos((endAngle * Math.PI) / 180)
  const endY = cy + r * Math.sin((endAngle * Math.PI) / 180)
  const arcPath = `M ${startX} ${startY} A ${r} ${r} 0 0 1 ${endX} ${endY}`

  return (
    <Svg width={200} height={120}>
      <Defs>
        <LinearGradient id="gaugeGrad" x1="0%" y1="0%" x2="100%" y2="0%">
          <Stop offset="0%" stopColor="#FFB344" />
          <Stop offset="100%" stopColor="#FF6B6B" />
        </LinearGradient>
      </Defs>

      {/* 背景弧 */}
      <Path d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy}`} stroke="#F0F0F0" strokeWidth={14} fill="none" strokeLinecap="round" />

      {/* 进度弧(渐变) */}
      <Path d={arcPath} stroke="url(#gaugeGrad)" strokeWidth={14} fill="none" strokeLinecap="round" />

      <SvgText x={cx} y={cy - 10} textAnchor="middle" fontSize={28} fontWeight="bold" fill="#333">
        {value}
      </SvgText>
      <SvgText x={cx} y={cy + 10} textAnchor="middle" fontSize={12} fill="#999">kcal</SvgText>
    </Svg>
  )
}

为什么"步道"选了 SVG?

需求PNG 图片三方图表库react-native-svg
动态进度环❌ 不能变
折线图定制⚠️ 样式有限✅ 全自定义
渐变填充⚠️
矢量不失真❌ 放大模糊
轻量❌ 重
动画联动⚠️✅ Reanimated

总结

"步道"这个场景里,@react-native-ohos/react-native-svg 解决了三件事:

  1. 进度环——Circle + strokeDashoffset,渐变描边 + 动画驱动
  2. 折线图——动态生成 Path d 属性,矢量渲染不模糊
  3. 仪表盘——Arc Path + LinearGradient,渐变弧线精确到角度

如果你也需要在 RN 鸿蒙应用里画矢量图形——进度环、自定义图表、GPS 轨迹、仪表盘——react-native-svg 是最轻量又最灵活的选择。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐