You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

react-native-svg中如何避免TextPath内的Text组件被截断?

问题

我正在开发一个带有北(North)、东(East)、南(South)、西(West)方向的简易指南针,使用TextPath实现文字沿圆周曲线排列。目前遇到核心问题:因设置textAnchor为middle,Text组件开头部分被截断,无法完整显示“North”标签。如何让文字完全显示?待显示正常后,我会将各方向按25%间距沿TextPath排列。

代码如下:

/* eslint-disable react/jsx-key */
import React, { Fragment, useState } from 'react'
import { Dimensions, View, Text } from 'react-native'
import {
    Line,
    Circle,
    Text as SVGText,
    Defs,
    Path,
    Use,
    Svg,
    G,
    TextPath,
    ForeignObject,
    TSpan,
} from 'react-native-svg'
import { ReactNativeZoomableView } from '@openspacelabs/react-native-zoomable-view'


const getPoint = (R = 0, degree = 0) => {
    // Convert the angle to a value between 0 and 360 degrees
    const angle = ((degree % 360) + 360) % 360
    // Calculate the coordinates using Math.sin and Math.cos
    const x = R * Math.cos(angle * (Math.PI / 180))
    const y = R * Math.sin(angle * (Math.PI / 180))
    return [x, y]
}


const Compass = (props) => {
    const [X, setW] = useState(1000)
    const [Y, setH] = useState(1000)
    const CoreRadius = 100
    const PartRadius = 50

    const [slider, setSlider] = useState(0)
    const [point, setPoint] = useState([0, CoreRadius])

    const rings = [
        {
            offset: -45,
            degree: 90,
            parts: [{ text: 'North' }, { text: 'East' }, { text: 'South' }, { text: 'West' }],
        },
    ]

    return (
        <View style={{ flex: 1 }}>
            <View style={{ position: 'absolute', padding: 12, top: 0, left: 0, right: 0, zIndex: 1 }}></View>
            <ReactNativeZoomableView maxZoom={30} bindToBorders={false}>
                <Svg
                    style={{ backgroundColor: '#141414', width: X, height: Y }}
                    viewBox={`-${X / 2} -${Y / 2} ${X} ${Y}`}
                    onLayout={(event) => {
                        const { layout } = event.nativeEvent
                    }}
                >
                    {/* Inside Ring */}
                    <Circle cx="0" cy="0" r={CoreRadius} fill="none" strokeWidth={2} stroke={'white'} />
                    {rings.map((ring, ringIndex) => {
                        //Begin at [0, 1] (bottom of the circle)
                        let totalDegrees = 0

                        const prevRing = CoreRadius + ringIndex * PartRadius
                        const newRing = prevRing + (ringIndex + 1 * PartRadius)

                        //This is the Path for the Text
                        const pathRadius = prevRing + (newRing - prevRing) / 2
                        const centerX = 0
                        const centerY = 0

                        return (
                            <Fragment>
                                <Circle cx="0" cy="0" r={newRing} fill="none" strokeWidth={2} stroke={'white'} />
                                <Defs>
                                    {/* This path is used for text to be displayed along the circular path. */}
                                    <Path
                                        d={`M ${centerX} ${centerY + pathRadius}
                                           A ${pathRadius} ${pathRadius} 0 0 1 ${centerX} ${centerY - pathRadius}
                                           A ${pathRadius} ${pathRadius} 0 0 1 ${centerX} ${centerY + pathRadius}
                                           `}
                                        fill="none"
                                        stroke="green"
                                        strokeWidth={1}
                                        id={`path-${ringIndex}`}
                                    />
                                </Defs>
                                <Use href={`#path-${ringIndex}`} />
                                {ring.parts.map((part, partIndex) => {
                                    const partDegree = part.degree || ring.degree
                                    totalDegrees = totalDegrees + partDegree
                                    const innerStartPoint = getPoint(prevRing, totalDegrees - ring.offset)
                                    const outerStartPoint = getPoint(newRing, totalDegrees - ring.offset)

                                    return (
                                        <Fragment>
                                            <Line
                                                x1={innerStartPoint[0]}
                                                y1={innerStartPoint[1]}
                                                x2={outerStartPoint[0]}
                                                y2={outerStartPoint[1]}
                                                stroke={'white'}
                                                strokeWidth={2}
                                            />

                                            {/* Fix startOffset so that each Direction begins at a different point. */}
                                            <TextPath href={`#path-${ringIndex}`}>
                                                <TSpan
                                                    fill={'white'}
                                                    textAnchor="middle"
                                                    alignmentBaseline="middle"
                                                    verticalAlign={'middle'}
                                                >
                                                    {part.text}
                                                </TSpan>
                                            </TextPath>
                                        </Fragment>
                                    )
                                })}
                            </Fragment>
                        )
                    })}
                </Svg>
            </ReactNativeZoomableView>
        </View>
    )
}

export default Compass

问题图示:
指南针文字截断图示


解决方案

核心问题原因

当前TextPath未设置startOffset,文字默认从路径起点开始排列;搭配textAnchor="middle"后,文字中点对齐路径起点,导致左侧内容被截断。另外,闭合圆形路径下需调整文字方向,避免部分方向文字倒置。

具体修改步骤

  1. 添加动态startOffset
    按每个方向在圆周上的占比(25%)计算偏移量,让文字中点精准对齐目标角度的路径位置。比如North对应顶部,从路径起点(底部)顺时针偏移25%。

  2. 调整文字显示方向
    给TextPath添加method="align"和side="left",确保文字始终朝向指南针外侧,不会出现倒置。

  3. 清理冗余属性
    移除TSpan上的alignmentBaseline和verticalAlign,保留textAnchor="middle"即可实现居中对齐。

修改后的关键代码片段

{ring.parts.map((part, partIndex) => {
    const partDegree = part.degree || ring.degree
    totalDegrees = totalDegrees + partDegree
    const innerStartPoint = getPoint(prevRing, totalDegrees - ring.offset)
    const outerStartPoint = getPoint(newRing, totalDegrees - ring.offset)
    // 计算每个方向的偏移百分比,实现25%均匀分布
    const startOffset = `${25 * partIndex}%`

    return (
        <Fragment key={`ring-${ringIndex}-part-${partIndex}`}>
            <Line
                x1={innerStartPoint[0]}
                y1={innerStartPoint[1]}
                x2={outerStartPoint[0]}
                y2={outerStartPoint[1]}
                stroke={'white'}
                strokeWidth={2}
            />

            <TextPath 
                href={`#path-${ringIndex}`}
                startOffset={startOffset}
                method="align"
                side="left"
            >
                <TSpan
                    fill={'white'}
                    textAnchor="middle"
                >
                    {part.text}
                </TSpan>
            </TextPath>
        </Fragment>
    )
})}

额外优化点

  • 给Fragment添加唯一key,消除React渲染警告
  • 可给TSpan设置固定fontSize,避免缩放时显示异常
  • 路径d属性可简化为紧凑格式,不影响功能

内容的提问来源于stack exchange,提问作者TIMEX

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 23:55:17