141 lines
4.9 KiB
TypeScript
141 lines
4.9 KiB
TypeScript
import React, { useMemo } from 'react'
|
|
import { TextProps, StyleSheet, TextStyle, View, Text, ViewStyle } from 'react-native'
|
|
import Svg, { Defs, LinearGradient as SvgLinearGradient, Stop, Text as SvgText } from 'react-native-svg'
|
|
|
|
interface GradientTextProps extends Omit<TextProps, 'style'> {
|
|
colors: [string, string, ...string[]]
|
|
start?: { x: number; y: number }
|
|
end?: { x: number; y: number }
|
|
style?: TextStyle
|
|
}
|
|
|
|
/**
|
|
* 渐变文字组件(使用 SVG 实现,无需原生模块)
|
|
*
|
|
* @example
|
|
* <GradientText
|
|
* colors={['#FF9966', '#FF6699', '#9966FF']}
|
|
* start={{ x: 0, y: 0 }}
|
|
* end={{ x: 1, y: 0 }}
|
|
* style={{ fontSize: 24, fontWeight: 'bold' }}
|
|
* >
|
|
* 渐变文字
|
|
* </GradientText>
|
|
*/
|
|
export default function GradientText({
|
|
colors,
|
|
start = { x: 0, y: 0 },
|
|
end = { x: 1, y: 0 },
|
|
style,
|
|
children,
|
|
...textProps
|
|
}: GradientTextProps) {
|
|
const gradientId = useMemo(() => `gradient-${Math.random().toString(36).substr(2, 9)}`, [])
|
|
|
|
// 从 style 中提取字体相关属性
|
|
const fontSize = (style?.fontSize as number) || 14
|
|
const fontWeight = style?.fontWeight || 'normal'
|
|
const fontFamily = style?.fontFamily
|
|
const textAlign = style?.textAlign || 'left'
|
|
|
|
// 使用一个隐藏的 Text 来测量文字尺寸
|
|
const [textWidth, setTextWidth] = React.useState(0)
|
|
const [textHeight, setTextHeight] = React.useState(fontSize * 1.2)
|
|
|
|
// 计算渐变坐标(转换为百分比)
|
|
const x1 = `${start.x * 100}%`
|
|
const y1 = `${start.y * 100}%`
|
|
const x2 = `${end.x * 100}%`
|
|
const y2 = `${end.y * 100}%`
|
|
|
|
// 计算文字位置
|
|
const textX = textAlign === 'center' ? (textWidth / 2) : textAlign === 'right' ? textWidth : 0
|
|
const textY = textHeight * 0.75 // 垂直位置,稍微调整以匹配基线
|
|
|
|
// 从 style 中提取 ViewStyle 兼容的属性
|
|
const containerStyle: ViewStyle = {
|
|
...(style?.margin !== undefined && { margin: style.margin }),
|
|
...(style?.marginTop !== undefined && { marginTop: style.marginTop }),
|
|
...(style?.marginBottom !== undefined && { marginBottom: style.marginBottom }),
|
|
...(style?.marginLeft !== undefined && { marginLeft: style.marginLeft }),
|
|
...(style?.marginRight !== undefined && { marginRight: style.marginRight }),
|
|
...(style?.marginHorizontal !== undefined && { marginHorizontal: style.marginHorizontal }),
|
|
...(style?.marginVertical !== undefined && { marginVertical: style.marginVertical }),
|
|
}
|
|
|
|
return (
|
|
<View style={[styles.container, containerStyle]}>
|
|
{/* 隐藏的 Text 用于测量尺寸和占据空间 */}
|
|
<Text
|
|
style={[
|
|
styles.measureText,
|
|
{
|
|
fontSize,
|
|
fontWeight,
|
|
fontFamily,
|
|
textAlign,
|
|
},
|
|
]}
|
|
onLayout={(e) => {
|
|
const { width, height } = e.nativeEvent.layout
|
|
setTextWidth(width || 0)
|
|
setTextHeight(Math.max(height || fontSize * 1.2, fontSize * 1.2))
|
|
}}
|
|
>
|
|
{String(children)}
|
|
</Text>
|
|
|
|
{/* SVG 渐变文字 */}
|
|
{textWidth > 0 && textHeight > 0 && (
|
|
<Svg
|
|
width={textWidth}
|
|
height={textHeight}
|
|
style={styles.svgOverlay}
|
|
>
|
|
<Defs>
|
|
<SvgLinearGradient id={gradientId} x1={x1} y1={y1} x2={x2} y2={y2}>
|
|
{colors.map((color, index) => (
|
|
<Stop
|
|
key={index}
|
|
offset={`${(index / (colors.length - 1)) * 100}%`}
|
|
stopColor={color}
|
|
/>
|
|
))}
|
|
</SvgLinearGradient>
|
|
</Defs>
|
|
<SvgText
|
|
x={textX}
|
|
y={textY}
|
|
fontSize={fontSize}
|
|
fontWeight={fontWeight}
|
|
fontFamily={fontFamily}
|
|
textAnchor={textAlign === 'center' ? 'middle' : textAlign === 'right' ? 'end' : 'start'}
|
|
fill={`url(#${gradientId})`}
|
|
>
|
|
{String(children)}
|
|
</SvgText>
|
|
</Svg>
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
backgroundColor: 'transparent',
|
|
position: 'relative',
|
|
},
|
|
measureText: {
|
|
opacity: 0,
|
|
includeFontPadding: false,
|
|
// 确保文本占据空间
|
|
minHeight: 1,
|
|
},
|
|
svgOverlay: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
pointerEvents: 'none',
|
|
},
|
|
})
|