expo-duooomi-app/components/ParallelogramGridItem.tsx

194 lines
4.7 KiB
TypeScript

import React, { memo, useMemo } from 'react'
import { Pressable } from 'react-native'
import {
Canvas,
Group,
Image as SkiaImage,
LinearGradient,
Paragraph,
Path,
Rect,
Skia,
useFonts,
useImage,
vec,
} from '@shopify/react-native-skia'
type MediaItem = {
id: string
url: string
webpPreviewUrl?: string
authorName?: string
likeCount?: number
title?: string
}
type Props = {
item: MediaItem
isSelected: boolean
itemWidth: number
isVisible: boolean
onSelect: () => void
}
const ParallelogramGridItem = memo<Props>(function ParallelogramGridItem({
item,
isSelected,
itemWidth,
isVisible,
onSelect,
}) {
const previewUrl = item.webpPreviewUrl || item.url || ''
// 整个卡片(图片区域 + 底部文字区域)的高度
const labelHeight = 24
const cardHeight = itemWidth + labelHeight
// 平行四边形路径(只裁剪内容,不拉伸内容),覆盖整张卡片
const skewOffset = 8
const padding = 2 // 四周内缩,避免描边被裁掉
const parallelogramPath = useMemo(() => {
const p = Skia.Path.Make()
// 顶点全部在 Canvas 内部,留出 padding 空间
p.moveTo(skewOffset + padding, padding)
p.lineTo(itemWidth - padding, padding)
p.lineTo(itemWidth - skewOffset - padding, cardHeight - padding)
p.lineTo(padding, cardHeight - padding)
p.close()
return p
}, [cardHeight, itemWidth])
const image = useImage(isVisible ? previewUrl : undefined)
// 字体管理,用于绘制作者和点赞数文本
const fontMgr = useFonts({
System: [],
})
const authorParagraph = useMemo(() => {
if (!fontMgr) return null
const builder = Skia.ParagraphBuilder.Make(
{
textAlign: 0,
},
fontMgr,
)
builder.pushStyle({
color: Skia.Color('#323232'),
fontSize: 10,
fontFamilies: ['System'],
fontStyle: { weight: 400 },
})
builder.addText(item.authorName || '未知作者')
const para = builder.build()
para.layout(40)
return para
}, [fontMgr, item.authorName])
const likeParagraph = useMemo(() => {
if (!fontMgr) return null
const builder = Skia.ParagraphBuilder.Make(
{
textAlign: 0,
},
fontMgr,
)
// 红色小心形符号
builder.pushStyle({
color: Skia.Color('#FF0000'),
fontSize: 8,
fontFamilies: ['System'],
fontStyle: { weight: 900 },
})
builder.addText('❤ ')
// 白色数字
builder.pushStyle({
color: Skia.Color('#323232'),
fontSize: 10,
fontFamilies: ['System'],
fontStyle: { weight: 400 },
})
builder.addText(String(item.likeCount ?? 0))
const para = builder.build()
para.layout(40)
return para
}, [fontMgr, item.likeCount])
return (
<Pressable
onPress={onSelect}
style={{
marginBottom: 12,
width: itemWidth,
height: cardHeight,
}}
>
<Canvas style={{ width: itemWidth, height: cardHeight }}>
{/* 整个卡片(图片 + 渐变 + 文本 + 黄色条)都被平行四边形裁剪 */}
<Group clip={parallelogramPath}>
{/* 上半部分:背景图片 */}
{image && (
<SkiaImage
image={image}
x={0}
y={0}
width={itemWidth}
height={itemWidth}
fit="cover"
/>
)}
{/* 图片区域上的底部遮罩渐变 */}
<LinearGradient
start={vec(0, itemWidth)}
end={vec(0, itemWidth * 0.6)}
colors={['rgba(0,0,0,0.8)', 'transparent']}
/>
{/* 底部黄色信息条(同样被平行四边形裁剪) */}
<Rect
x={0}
y={itemWidth}
width={itemWidth}
height={labelHeight}
color={isSelected ? '#FFE500' : '#FFFFFF'}
/>
{/* 底部作者 + 喜欢数文本(在平行四边形内部,显示在黄色条上) */}
{authorParagraph && (
<Paragraph
paragraph={authorParagraph}
x={8}
y={itemWidth + (labelHeight - 10) / 3}
width={60}
/>
)}
{likeParagraph && (
<Paragraph
paragraph={likeParagraph}
x={itemWidth - 32}
y={itemWidth + (labelHeight - 10) / 3}
width={40}
/>
)}
</Group>
{/* 边框 */}
<Path
path={parallelogramPath}
style="stroke"
strokeWidth={isSelected ? 3 : 2}
color={isSelected ? '#FFE500' : '#000000'}
/>
</Canvas>
</Pressable>
)
})
export default ParallelogramGridItem