如何在 React Native 中创建动画标签栏?

How to create an animated tab bar in react native?

我从 dribble 看到了这个动画标签栏,我很想学习如何构建它。 tab bar animation

这就是您要找的东西

需要两个库react-native-svg and d3-shape来绘制弧形

完整代码


import React, {Component} from 'react';
import {
  View,
  Dimensions,
  SafeAreaView,
  StyleSheet,
  Animated,
  TouchableWithoutFeedback,
} from 'react-native';
import Svg, {Path} from 'react-native-svg';
import * as shape from 'd3-shape';

const {width} = Dimensions.get('window');
const height = 80;
const tabs = [
  {
    name:
      'https://www.clipartmax.com/png/full/255-2556971_computer-icons-user-management-clip-art-default-profile-picture-green.png',
  },
  {
    name: 'https://img.icons8.com/material/4ac144/256/user-male.png',
  },
  {
    name: 'https://img.icons8.com/material/4ac144/256/camera.png',
  },
  {
    name:
      'https://images.vexels.com/media/users/3/154655/isolated/preview/71dccbb077597dea55dfc5b7a7af52c4-location-pin-contact-icon-by-vexels.png',
  },
  {
    name:
      'https://img.pngio.com/corona-icono-vectorial-gratis-diseado-por-freepik-ingredientes-corona-png-gratis-1200_630.png',
  },
];
const tabWidth = width / tabs.length;
const AnimatedSvg = Animated.createAnimatedComponent(Svg);

const left = shape
  .line()
  .x((d) => d.x)
  .y((d) => d.y)([
  {x: 0, y: 0},
  {x: width, y: 0},
]);

const tab1 = shape
  .line()
  .x((d) => d.x)
  .y((d) => d.y)
  .curve(shape.curveBasis)([
  {x: width - 10, y: 0},
  {x: width + 5, y: 0},
  {x: width + 15, y: 10},
  {x: width + tabWidth / 2 - 20, y: (height / 3) * 2},
  {x: width + tabWidth / 2 + 20, y: (height / 3) * 2},
  {x: width + tabWidth - 15, y: 10},
  {x: width + tabWidth - 5, y: 0},
  {x: width + tabWidth + 10, y: 0},
]);

const right = shape
  .line()
  .x((d) => d.x)
  .y((d) => d.y)([
  {x: width + tabWidth, y: 0},
  {x: width * 2 + tabWidth, y: 0},
  {x: width * 2 + tabWidth, y: height},
  {x: 0, y: height},
  {x: 0, y: 0},
]);

const d = `${left} ${tab1} ${right}`;

export default class tabBar extends Component {
  value = new Animated.Value(-width);

  render() {
    const {value} = this;
    return (
      <View style={styles.container}>
        <View {...{width}} style={{backgroundColor: 'transparent'}}>
          <AnimatedSvg
            width={width * 2 + tabWidth}
            {...{height}}
            style={{
              transform: [{translateX: value}],
            }}>
            <Path key="path" {...{d}} fill="white" />
          </AnimatedSvg>
          <View style={StyleSheet.absoluteFill}>
            <StaticTabBar {...{value}} />
          </View>
          <SafeAreaView style={styles.safeArea} />
        </View>
      </View>
    );
  }
}

class StaticTabBar extends Component {
  constructor(props) {
    super(props);
    this.value = tabs.map(
      (item, index) => new Animated.Value(index === 0 ? 1 : 0),
    );
  }

  onPress = (index) => {
    const {value} = this.props;
    Animated.parallel([
      Animated.parallel([
        ...this.value.map((item, i) => {
          if (index !== i) {
            return Animated.timing(item, {
              toValue: 0,
              duration: 400,
              useNativeDriver: true,
            });
          }
        }),
      ]),

      Animated.parallel([
        Animated.timing(value, {
          toValue: -width + tabWidth * index,
          useNativeDriver: true,
          duration: 450,
        }),

        ...this.value.map((item, i) => {
          if (index === i) {
            return Animated.timing(item, {
              toValue: 1,
              duration: 400,
              useNativeDriver: true,
            });
          }
        }),
      ]),
    ]).start();
  };

  render() {
    const {value} = this.props;
    return (
      <View style={styles.container1}>
        {tabs.map(({name}, index) => {
          const activeValue = this.value[index];
          const opacity = value.interpolate({
            inputRange: [
              -width + tabWidth * (index - 1),
              -width + tabWidth * index,
              -width + tabWidth * (index + 1),
            ],
            outputRange: [1, 0, 1],
            extrapolate: 'clamp',
          });
          const translateIcons = value.interpolate({
            inputRange: [
              -width + tabWidth * (index - 1),
              -width + tabWidth * index,
              -width + tabWidth * (index + 1),
            ],
            outputRange: [0, 10, 0],
            extrapolate: 'clamp',
          });
          const translateY = activeValue.interpolate({
            inputRange: [0, 1],
            outputRange: [tabWidth, 0],
          });
          const opacityValue = activeValue.interpolate({
            inputRange: [0, 0.7, 1],
            outputRange: [0, 0, 1],
          });
          const translateX = value.interpolate({
            inputRange: [
              -width + tabWidth * (index - 1),
              -width + tabWidth * index,
              -width + tabWidth * (index + 1),
            ],
            outputRange: [-tabWidth, 0, tabWidth],
          });
          return (
            <>
              <Animated.View
                style={[
                  {
                    left: index * tabWidth,
                    width: tabWidth,
                    transform: [{translateY}, {translateX}],
                    opacity: opacityValue,
                  },
                  styles.movingCircle,
                ]}>
                <View style={styles.circle}>
                  <Animated.Image
                    source={{
                      uri: name,
                    }}
                    style={styles.activeIcon}
                  />
                </View>
              </Animated.View>
              <TouchableWithoutFeedback onPress={() => this.onPress(index)}>
                <View style={styles.active}>
                  <Animated.Image
                    source={{
                      uri: name,
                    }}
                    style={{
                      ...styles.inactive,
                      opacity,
                      transform: [{translateY: translateIcons}],
                    }}
                  />
                </View>
              </TouchableWithoutFeedback>
            </>
          );
        })}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'flex-end',
  },
  safeArea: {
    backgroundColor: 'white',
  },
  container1: {
    height,
    width,
    flexDirection: 'row',
  },
  circle: {
    height: 60,
    width: 60,
    borderRadius: 30,
    backgroundColor: 'white',
    alignItems: 'center',
    justifyContent: 'center',
  },
  movingCircle: {
    position: 'absolute',
    alignItems: 'center',
    top: -30,
  },
  inactive: {
    height: 25,
    width: 25,
    tintColor: '#192f6a',
  },
  active: {
    width: tabWidth,
    height,
    alignItems: 'center',
    justifyContent: 'center',
  },
  activeIcon: {
    height: 25,
    width: 25,
    tintColor: '#192f6a',
  },
});