expo-popcore-app/components/GradientText.tsx

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',
},
})