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>
  );
};