Fab Expand Button - 浮动展开动画按钮
一个基于 React Spring 实现的优雅浮动操作按钮动画组件,支持自定义尺寸和内容展示。
预览
使用示例
import { Button, Input, Text, View } from '@tarojs/components';
export default function Page() {
return (
<View className="container">
<FabButton size={48} contentSize={240} contentClassName="bg-black p-4 flex flex-col">
<Text className="font-bold text-white">扩展功能</Text>
<Text className="mt-4 text-sm text-gray-300">使用CodeSnip Mini 快速实现现代化的动画</Text>
<Text className="mt-4 text-sm text-gray-300">请输入邮箱</Text>
<Input
className="mt-4 flex h-8 items-center rounded-lg bg-[#322E32] px-2 text-sm text-white"
placeholder="请输入内容"
placeholderClass="text-sm text-gray-300"
/>
<Button size="mini" className="mt-4 w-full bg-yellow-400">
确定
</Button>
</FabButton>
</View>
);
}
下载
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);
源代码
import { AnimationText, AnimationView } from '@/components/animation';
import { cn } from '@/utils';
import { useSpring, useTransition } from '@react-spring/web';
import { PropsWithChildren, useState } from 'react';
interface FabButtonProps extends PropsWithChildren {
/** 按钮初始大小(单位:像素) */
size: number;
/** 展开后内容区域大小(单位:像素) */
contentSize: number;
/** 内容区域自定义样式类名 */
contentClassName?: string;
/** 组件根节点自定义样式类名 */
className?: string;
}
const FabButton = ({ children, size, contentSize, contentClassName, className }: FabButtonProps) => {
const [isOpen, setIsOpen] = useState(false);
const fromBorderRadius = '100%';
const toBorderRadius = isOpen ? '5%' : '100%';
const taggerStyle = useSpring({
from: { size: size, borderRadius: fromBorderRadius },
to: { size: isOpen ? contentSize : size, borderRadius: toBorderRadius },
});
const transitions = useTransition(isOpen, {
from: {
opacity: 0,
transform: `scale(${0.1})`,
transformOrigin: 'bottom',
borderRadius: fromBorderRadius,
},
enter: { opacity: 1, transform: `scale(${1})`, borderRadius: toBorderRadius },
leave: { opacity: 0, transform: `scale(${0.1})`, borderRadius: fromBorderRadius },
});
return (
<View className={cn('relative flex items-center justify-center', className)}>
<AnimationView
style={{ ...taggerStyle, height: taggerStyle.size, width: taggerStyle.size }}
className="absolute bottom-0 z-10 bg-black flex-center"
onClick={() => setIsOpen(state => !state)}
>
<AnimationText className={cn('icon-[lucide--plus] text-white')}></AnimationText>
</AnimationView>
{transitions(
(style, item) =>
item && (
<AnimationView
style={{ ...style, height: contentSize, width: contentSize }}
className={cn('absolute bottom-0 z-20 overflow-hidden bg-black', contentClassName)}
>
<AnimationText
onClick={() => setIsOpen(state => !state)}
className={cn('icon-[lucide--plus] absolute right-4 top-4 rotate-45 text-white')}
></AnimationText>
{children}
</AnimationView>
)
)}
</View>
);
};