You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在React Native中实现按钮左右滑动切换动画?

React Native 按钮滑动切换动画实现方案

需求说明

实现Ebook与Audiobook按钮切换时,右侧按钮向左滑动、左侧按钮向右滑动的平滑过渡动画,按钮样式如下:

按钮样式截图

实现方案

使用React Native自带的Animated库实现平移动画,重构状态管理逻辑让动画触发更简洁,以下是修改后的完整代码:

import React, { useEffect, useState, useRef } from 'react';
import {
  Dimensions,
  Pressable,
  SafeAreaView,
  StyleSheet,
  Text,
  View,
  Animated,
} from 'react-native';
import { Book } from '../types';
import BooksList from '../json/books.json';
import AudiosList from '../json/audios.json';
import Menu from 'react-native-vector-icons/Entypo';
import Glass from 'react-native-vector-icons/Entypo';
import { ScrollView } from 'react-native-gesture-handler';
import AudioBox from '../components/AudiosBooksBox';

export default function HomeScreen() {
  const [books, setBooks] = useState<Book[]>([]);
  const [audios, setAudios] = useState<Book[]>([]);
  // 用单个字符串标记激活标签,替代两个独立布尔值
  const [activeTab, setActiveTab] = useState<'ebook' | 'audiobook'>('ebook');
  // 动画驱动值:0对应Ebook激活,1对应Audiobook激活
  const slideAnim = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    setBooks(BooksList);
    setAudios(AudiosList);
  }, []);

  // 切换标签时触发平滑动画
  const switchTab = (tab: 'ebook' | 'audiobook') => {
    if (tab === activeTab) return;
    setActiveTab(tab);
    Animated.timing(slideAnim, {
      toValue: tab === 'audiobook' ? 1 : 0,
      duration: 300,
      useNativeDriver: true,
    }).start();
  };

  // 计算按钮平移距离:基于动画值生成插值
  const buttonTranslateX = slideAnim.interpolate({
    inputRange: [0, 1],
    outputRange: [0, -10], // 右侧按钮左滑距离
  });
  const reverseButtonTranslateX = slideAnim.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 10], // 左侧按钮右滑距离
  });

  const grand = Dimensions.get('window').width / 2;

  return (
    <ScrollView style={styles.scrollContainer}>
      <View style={styles.searchContainer}>
        <Menu name={'menu'} size={30} color={'#1C0D45'} />
        <Text style={styles.textSearch}>All Books</Text>
        <Glass name={'magnifying-glass'} size={30} color={'#1C0D45'} />
      </View>
      <View style={styles.AudioOrEbookContainer}>
        {/* 左侧Ebook按钮:切换到Audiobook时右滑 */}
        <Animated.View style={{ transform: [{ translateX: activeTab === 'audiobook' ? reverseButtonTranslateX : 0 }] }}>
          <Pressable
            onPress={() => switchTab('ebook')}
            style={activeTab === 'ebook' ? styles.btn2 : styles.btn}
          >
            <Text style={styles.AorBText}>Ebook</Text>
          </Pressable>
        </Animated.View>

        {/* 右侧Audiobook按钮:切换到Ebook时左滑 */}
        <Animated.View style={{ transform: [{ translateX: activeTab === 'ebook' ? buttonTranslateX : 0 }] }}>
          <Pressable
            onPress={() => switchTab('audiobook')}
            style={activeTab === 'audiobook' ? styles.btn2 : styles.btn}
          >
            <Text style={styles.AorBText}>Audiobook</Text>
          </Pressable>
        </Animated.View>
      </View>
      <SafeAreaView style={styles.container}>
        {activeTab === 'ebook' && (
          <View style={{ flexDirection: 'row' }}>
            <View>
              {books
                .filter((_, i) => i % 2 === 0)
                .map(book => (
                  <AudioBox
                    width={grand}
                    key={book.id}
                    aspectRatio={book.aspectRatio}
                    title={book.title}
                    author={book.author}
                    imageLink={book.imageLink}
                    country={book.country}
                    overview={book.overview}
                    vote={book.vote}
                    duration={book.duration}
                    actor={book.actor}
                    year={book.year}
                    pages={book.pages}
                    link={book.link}
                    language={book.language}
                  />
                ))}
            </View>
            <View>
              {books
                .filter((_, i) => i % 2 !== 0)
                .map(book => (
                  <AudioBox
                    width={grand}
                    key={book.id}
                    aspectRatio={book.aspectRatio}
                    title={book.title}
                    author={book.author}
                    imageLink={book.imageLink}
                    country={book.country}
                    overview={book.overview}
                    vote={book.vote}
                    duration={book.duration}
                    actor={book.actor}
                    year={book.year}
                    pages={book.pages}
                    link={book.link}
                    language={book.language}
                  />
                ))}
            </View>
          </View>
        )}

        {activeTab === 'audiobook' && (
          <View style={{ flexDirection: 'row' }}>
            <View>
              {audios
                .filter((_, i) => i % 2 === 0)
                .map(audio => (
                  <AudioBox
                    width={grand}
                    key={audio.id}
                    aspectRatio={audio.aspectRatio}
                    title={audio.title}
                    author={audio.author}
                    imageLink={audio.imageLink}
                    country={audio.country}
                    overview={audio.overview}
                    vote={audio.vote}
                    duration={audio.duration}
                    actor={audio.actor}
                    year={audio.year}
                    pages={audio.pages}
                    link={audio.link}
                    language={audio.language}
                  />
                ))}
            </View>
            <View>
              {audios
                .filter((_, i) => i % 2 !== 0)
                .map(audio => (
                  <AudioBox
                    width={grand}
                    key={audio.id}
                    aspectRatio={audio.aspectRatio}
                    title={audio.title}
                    author={audio.author}
                    imageLink={audio.imageLink}
                    country={audio.country}
                    overview={audio.overview}
                    vote={audio.vote}
                    duration={audio.duration}
                    actor={audio.actor}
                    year={audio.year}
                    pages={audio.pages}
                    link={audio.link}
                    language={audio.language}
                  />
                ))}
            </View>
          </View>
        )}
      </SafeAreaView>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  scrollContainer: {
    backgroundColor: 'white',
  },
  container: {
    marginTop: 50,
    flexDirection: 'row',
    justifyContent: 'center',
    backgroundColor: 'white',
  },
  searchContainer: {
    marginTop: 100,
    flexDirection: 'row',
    justifyContent: 'space-around',
  },
  btn: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'lightgray',
    borderRadius: 10,
    height: 40,
    width: '47%',
  },
  btn2: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white',
    borderRadius: 10,
    height: 40,
    width: '47%',
  },

  AudioOrEbookContainer: {
    marginTop: 50,
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center',
    backgroundColor: 'lightgray',
    height: 50,
    width: '80%',
    marginLeft: 35,
    borderRadius: 10,
  },
  AorBText: {
    color: '#1C0D45',
    fontWeight: '700',
    fontSize: 17,
  },

  textSearch: {
    fontSize: 30,
    color: '#1C0D45',
  },
});

关键修改说明

  1. 状态重构:合并两个布尔状态为activeTab字符串标记,简化切换逻辑。
  2. 动画驱动:用Animated.Value控制动画进度,通过Animated.timing实现300ms平滑过渡。
  3. 平移插值:通过interpolate将动画值转换为按钮平移距离,精准实现切换时的滑动效果。
  4. 原生动画:使用Animated.View包裹按钮,绑定平移变换到transform属性,利用原生驱动保证动画流畅度。

内容的提问来源于stack exchange,提问作者greta calamari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 18:31:50