Scroll Transition Tabbar - 动画标签栏

一个基于滚动位置的动画标签栏组件。

预览

使用示例

import { AnimationView } from '@/components/animation';
import { cn } from '@/utils';
import { useSpring } from '@react-spring/web';
import { View } from '@tarojs/components';
import { getWindowInfo, usePageScroll } from '@tarojs/taro';
import { useState } from 'react';

definePageConfig({
  navigationBarBackgroundColor: '#000000',
  navigationBarTextStyle: 'white',
  navigationBarTitleText: 'Animation tabbar',
});

const DATA = Array.from({ length: 100 }).map((_, index) => ({
  id: index,
  color: `#${Math.floor(Math.random() * 16777215).toString(16)}`,
}));

const optionsData: AnimationTabbarProps['options'] = [
  {
    icon: 'icon-[lucide--house]',
    value: 'home',
  },
  {
    icon: 'icon-[lucide--search]',
    value: 'search',
  },
  {
    icon: 'icon-[lucide--heart]',
    value: 'favorite',
  },
  {
    icon: 'icon-[lucide--user-round]',
    value: 'user',
  },
  {
    icon: 'icon-[lucide--settings]',
    value: 'settings',
  },
];

export default function Page() {
  return (
    <View className="grid grid-cols-4 gap-2.5 bg-black p-4">
      {DATA.map(({ id, color }) => {
        return (
          <View
            key={id}
            className="col-span-1 flex h-16 items-center justify-center rounded"
            style={{
              backgroundColor: color,
            }}
          ></View>
        );
      })}

      <AnimationTabbar options={optionsData} activeValue="home" />
    </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 { AnimationView } from '@/components/animation';
import { cn } from '@/utils';
import { useSpring } from '@react-spring/web';
import { View } from '@tarojs/components';
import { getWindowInfo, usePageScroll } from '@tarojs/taro';
import { useState } from 'react';

interface AnimationTabbarProps {
  options: {
    icon: string;
    value: string;
  }[];
  activeValue?: string;
  onChange?: (value: string) => void;
}


const AnimationTabbar = ({ options, activeValue, onChange }: AnimationTabbarProps) => {
  const { safeArea, screenHeight, windowHeight, statusBarHeight } = getWindowInfo();

  const safeButtom = screenHeight - (safeArea?.bottom ?? 0);

  const [isScroll, setIsScroll] = useState(false);

  usePageScroll(e => {
    if (e.scrollTop === 0) {
      setIsScroll(false);
    }

    if (!isScroll && e.scrollTop > 100) {
      setIsScroll(true);
    }
  });

  const fromStyle = { paddingBottom: safeButtom, bottom: 0, insetX: 0, borderRadius: '16px 16px 0px 0px' };
  const toStyle = { paddingBottom: 0, bottom: safeButtom + 10, insetX: 16, borderRadius: '25px 25px 25px 25px' };

  const { insetX, ...style } = useSpring({
    from: fromStyle,
    to: isScroll ? toStyle : fromStyle,
  });

  return (
    <AnimationView
      style={{ ...style, left: insetX, right: insetX }}
      className="fixed overflow-hidden border border-gray-400 bg-black/40 backdrop-blur-md"
    >
      {/* 可以替换以下内容,传递子组件,完全自定义 */}
      <View className="flex items-center p-4">
        {options.map(({ icon, value }) => {
          return (
            <View key={value} className="flex flex-1 items-center justify-center">
              <View
                className={cn('size-6 text-gray-400', icon, activeValue === value && 'text-white')}
                onClick={() => onChange?.(value)}
              ></View>
            </View>
          );
        })}
      </View>
    </AnimationView>
  );
};