183 lines
6.6 KiB
TypeScript
183 lines
6.6 KiB
TypeScript
import React, { memo, useMemo } from 'react'
|
||
import { Pressable } from 'react-native'
|
||
import { Canvas, Group, Paragraph, Path, Skia, useFonts } from '@shopify/react-native-skia'
|
||
|
||
export type ParallelogramButtonProps = {
|
||
label: string
|
||
state: string
|
||
isActive: boolean
|
||
onPress: () => void
|
||
width: number
|
||
height: number
|
||
}
|
||
|
||
const ParallelogramButton = memo<ParallelogramButtonProps>(function ParallelogramButton({
|
||
label,
|
||
state,
|
||
isActive,
|
||
onPress,
|
||
width,
|
||
height,
|
||
}) {
|
||
const fontMgr = useFonts({
|
||
System: [],
|
||
})
|
||
|
||
// 创建英文 state 标签段落(白色字体,黑色描边,字重 700,激活时 #FAE307,未激活时白色)
|
||
const stateParagraph = useMemo(() => {
|
||
if (!fontMgr) return null
|
||
const para = Skia.ParagraphBuilder.Make({}, fontMgr)
|
||
.pushStyle({
|
||
color: isActive ? Skia.Color('#FAE307') : Skia.Color('#FFFFFF'),
|
||
fontSize: 7,
|
||
fontFamilies: ['System'],
|
||
fontStyle: { weight: 700 },
|
||
shadows: [
|
||
// 黑色描边效果(略微偏移,避免过粗)
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: -0.5, y: -0.5 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 0.5, y: -0.5 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: -0.5, y: 0.5 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 0.5, y: 0.5 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: -0.5, y: 0 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 0.5, y: 0 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 0, y: -0.5 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 0, y: 0.5 } },
|
||
],
|
||
})
|
||
.addText(state)
|
||
.build()
|
||
para.layout(40)
|
||
return para
|
||
}, [fontMgr, state, isActive])
|
||
|
||
// 创建白色字体带黑色描边的中文文本段落
|
||
const paragraph = useMemo(() => {
|
||
if (!fontMgr) return null
|
||
const para = Skia.ParagraphBuilder.Make({}, fontMgr)
|
||
.pushStyle({
|
||
color: Skia.Color('#FFFFFF'),
|
||
fontSize: 13,
|
||
fontFamilies: ['System'],
|
||
fontStyle: { weight: 900 },
|
||
shadows: [
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: -1, y: -1 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 1, y: -1 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: -1, y: 1 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 1, y: 1 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: -1, y: 0 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 1, y: 0 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 0, y: -1 } },
|
||
{ color: Skia.Color('#323232'), blurRadius: 0, offset: { x: 0, y: 1 } },
|
||
],
|
||
})
|
||
.addText(label)
|
||
.build()
|
||
para.layout(width - 12) // 减去左右 padding
|
||
return para
|
||
}, [fontMgr, label, width])
|
||
|
||
const skewOffset = 8 // 倾斜偏移量(像素)
|
||
const padding = 15 // 增加 padding,防止旋转后的文本被裁剪
|
||
|
||
// 创建平行四边形路径:只有左右边倾斜,上下边保持水平
|
||
const parallelogramPath = useMemo(() => {
|
||
const path = Skia.Path.Make()
|
||
// 四个顶点:左上、右上、右下、左下(加上 padding 偏移)
|
||
path.moveTo(skewOffset + padding, padding)
|
||
path.lineTo(width + padding, padding)
|
||
path.lineTo(width - skewOffset + padding, height + padding)
|
||
path.lineTo(padding, height + padding)
|
||
path.close()
|
||
return path
|
||
}, [width, height, skewOffset, padding])
|
||
|
||
// 创建边框路径
|
||
const borderPath = useMemo(() => {
|
||
const path = Skia.Path.Make()
|
||
path.moveTo(skewOffset + padding + 1, padding + 1)
|
||
path.lineTo(width + padding - 1, padding + 1)
|
||
path.lineTo(width - skewOffset + padding - 1, height + padding - 1)
|
||
path.lineTo(padding + 1, height + padding - 1)
|
||
path.close()
|
||
return path
|
||
}, [width, height, skewOffset, padding])
|
||
|
||
// 计算实际画布尺寸(包含 padding)
|
||
const canvasWidth = width + padding * 2
|
||
const canvasHeight = height + padding * 2
|
||
|
||
// 注意:布局占位仍然使用原始 width,避免改变 tab 间距
|
||
return (
|
||
<Pressable onPress={onPress} style={{ width, height: canvasHeight }}>
|
||
<Canvas
|
||
style={{
|
||
width: canvasWidth,
|
||
height: canvasHeight,
|
||
// 向左偏移 padding,让额外画布空间不参与横向布局计算
|
||
marginLeft: -padding,
|
||
}}
|
||
>
|
||
{/* 背景 - 平行四边形 */}
|
||
<Path
|
||
path={parallelogramPath}
|
||
color={isActive ? Skia.Color('#FFE500') : Skia.Color('#FFFFFF')}
|
||
/>
|
||
|
||
{/* 边框 - 平行四边形 */}
|
||
<Path path={borderPath} color={Skia.Color('#323232')} style="stroke" strokeWidth={2} />
|
||
|
||
{/* 英文 state 标签 - 左上角,以左下角为中心,逆时针旋转 15 度 */}
|
||
{stateParagraph && (() => {
|
||
const stateTextX = 8 + skewOffset / 2 + 10 + padding
|
||
const stateTextY = 4 + padding
|
||
const rotationAngle = (-15 * Math.PI) / 180
|
||
const centerX = padding
|
||
const centerY = height + padding
|
||
|
||
return (
|
||
<Group
|
||
transform={[
|
||
{ translateX: centerX },
|
||
{ translateY: centerY },
|
||
{ rotate: rotationAngle },
|
||
{ translateX: -centerX + stateTextX },
|
||
{ translateY: -centerY + stateTextY },
|
||
]}
|
||
>
|
||
<Paragraph paragraph={stateParagraph} x={0} y={0} width={40} />
|
||
</Group>
|
||
)
|
||
})()}
|
||
|
||
{/* 中文文本 - 以左下角为中心,逆时针旋转 15 度 */}
|
||
{paragraph && (() => {
|
||
const textX = 16 + skewOffset / 2 + 2 + padding
|
||
const textY = height - paragraph.getHeight() + 1 + padding
|
||
const rotationAngle = (-15 * Math.PI) / 180
|
||
const centerX = padding
|
||
const centerY = height + padding
|
||
|
||
return (
|
||
<Group
|
||
transform={[
|
||
{ translateX: centerX },
|
||
{ translateY: centerY },
|
||
{ rotate: rotationAngle },
|
||
{ translateX: -centerX + textX },
|
||
{ translateY: -centerY + textY },
|
||
]}
|
||
>
|
||
<Paragraph paragraph={paragraph} x={0} y={0} width={width - 32 - skewOffset} />
|
||
</Group>
|
||
)
|
||
})()}
|
||
</Canvas>
|
||
</Pressable>
|
||
)
|
||
})
|
||
|
||
ParallelogramButton.displayName = 'ParallelogramButton'
|
||
|
||
export default ParallelogramButton
|
||
|