Vertical Swiper - 垂直滚动组件
一个基于滚动位置的垂直滚动组件。
预览
使用示例
import { AnimationView } from '@/components/animation';
import { useSwiperAnimationValue } from '@/hooks/useSwiperAnimationValue';
import { pt } from '@/utils';
import { SpringValue } from '@react-spring/web';
import { Image, Swiper, SwiperItem, View } from '@tarojs/components';
import { createSelectorQuery, NodesRef, useReady } from '@tarojs/taro';
import { useState } from 'react';
definePageConfig({
navigationBarTitleText: '',
navigationStyle: 'custom',
});
const DTTA = [
{
title: '清纯可人',
desc: '邻家女孩般的清新气质,让人心动不已',
image: 'https://images.unsplash.com/photo-1648740366598-7fb7c5e73fa5?w=800&auto=format&fit=crop&q=10',
},
{
title: '性感妖娆',
desc: '曼妙身姿与迷人眼神,散发成熟魅力',
image: 'https://images.unsplash.com/photo-1549768960-6e7b0c8b5d94?w=800&auto=format&fit=crop&q=10',
},
{
title: '甜美可爱',
desc: '灿烂笑容如阳光般温暖,让人忍不住想要呵护',
image: 'https://images.unsplash.com/photo-1728711632084-58f02a71657d?w=800&auto=format&fit=crop&q=10',
},
{
title: '知性优雅',
desc: '端庄大方的气质,展现女性的智慧与魅力',
image: 'https://images.unsplash.com/photo-1732965757891-368c1f235e7c?w=800&auto=format&fit=crop&q=10',
},
{
title: '运动活力',
desc: '健康阳光的形象,充满朝气和活力',
image: 'https://images.unsplash.com/photo-1729025004944-5aa651b2a62d?w=800&auto=format&fit=crop&q=10',
},
];
const MARGIN = pt(120);
export default function Page() {
const [itemHeight, setItemHeight] = useState<number>(pt(532));
const { transitionValue, currentIndex, setCurrentIndex, setTransitionValue } = useSwiperAnimationValue({
defaultIndex: 1,
itemSize: itemHeight,
});
useReady(() => {
const query = createSelectorQuery();
query
.select(`#item-${currentIndex}`)
.boundingClientRect((rect: NodesRef.BoundingClientRectCallbackResult) => {
setItemHeight(rect.height);
transitionValue.set(currentIndex * (rect.height ?? 0));
})
.exec();
});
return (
<Swiper
current={currentIndex}
className="h-screen w-screen bg-black p-5"
vertical
snapToEdge
previousMargin={MARGIN.toString()}
nextMargin={MARGIN.toString()}
onAnimationFinish={e => {
setCurrentIndex(e.detail.current);
}}
onTransition={e => {
setTransitionValue(e.detail.dy);
}}
>
{DTTA.map((item, index) => (
<SwiperItem key={index} className="bg-transparent" id={`item-${index}`}>
<Card key={index} item={item} itemHeight={itemHeight} index={index} transitionValue={transitionValue} />
</SwiperItem>
))}
</Swiper>
);
}
const Card = ({
index,
item,
itemHeight,
transitionValue,
}: { index: number; itemHeight: number; transitionValue: SpringValue<number> } & {
item: (typeof DTTA)[number];
}) => {
const RANGE = [(index - 1) * itemHeight, index * itemHeight, (index + 1) * itemHeight];
return (
<AnimationView
style={{
opacity: transitionValue.to(RANGE, [0.5, 1, 0.5], 'clamp'),
transform: transitionValue.to(RANGE, [0.95, 1, 0.95], 'clamp').to(value => `scale(${value})`),
backdropFilter: transitionValue.to(RANGE, ['blur(10px)', 'blur(20px)', 'blur(10px)'], 'clamp'),
backgroundColor: transitionValue.to(
RANGE,
['rgba(255, 255, 255, 0.05)', 'rgba(255, 255, 255, 0.1)', 'rgba(255, 255, 255, 0.05)'],
'clamp'
),
boxShadow: transitionValue.to(
RANGE,
[
'0 2px 4px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.04)',
'0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08)',
'0 2px 4px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.04)',
],
'clamp'
),
}}
className="box-border h-full w-full overflow-hidden rounded-lg p-2.5 opacity-90"
>
<Image mode="aspectFill" className="mb-2.5 h-[80%] w-full rounded" src={item.image} />
<View className="flex flex-col space-y-2.5">
<View className="text-2xl text-white">{item.title}</View>
<View className="text-sm text-muted-foreground">{item.desc}</View>
</View>
</AnimationView>
);
};
下载
1
添加依赖
npm i @react-spring/web clsx tailwind-merge
2
添加工具函数
// @/utils
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
// 合并 Tailwind CSS 类名和 clsx 类名
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// 根据屏幕宽度计算响应式值
export const pt = (num: number) => {
const { screenWidth } = getWindowInfo();
return num * (screenWidth / 375);
};
3
添加动画组件
// @/components/animation
import { animated } from '@react-spring/web';
import { Label, Text, View } from '@tarojs/components';
export const AnimationView = animated(View);
export const AnimationText = animated(Text);
export const AnimationLabel = animated(Label);
4
添加Hook
// @/hooks/useSwiperAnimationValue
import { useSpringValue } from '@react-spring/web';
import { useReady } from '@tarojs/taro';
import { useEffect, useState } from 'react';
export function useSwiperAnimationValue({ defaultIndex, itemSize }: { defaultIndex: number; itemSize: number }) {
const transitionValue = useSpringValue(0);
const [currentIndex, setIndex] = useState(defaultIndex);
const [isFirstChange, setIsFirstChange] = useState(true);
useReady(() => {
transitionValue.set(currentIndex * itemSize);
});
useEffect(() => {
transitionValue.set(currentIndex * itemSize);
}, [itemSize]);
const setCurrentIndex = (indexValue: number) => {
setIndex(indexValue);
if (isFirstChange) {
setIsFirstChange(false);
}
};
const setTransitionValue = (value: number) => {
transitionValue.set(currentIndex * itemSize + value - (isFirstChange ? itemSize * currentIndex : 0));
};
return { transitionValue, setCurrentIndex, currentIndex, setTransitionValue };
}