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

React Native中TranslateY动画异常:Logo动效未达预期

修复React Native Logo动画逻辑

问题分析

原代码的核心问题在于动画初始值设置错误和动画序列逻辑颠倒:

  • Logo初始translateY值为0,而容器设置了justifyContent: 'center',导致Logo一开始就在中心位置,没有从底部进入的效果
  • 初始opacity为0,且第一个动画没有先将透明度恢复为1,导致Logo一开始不可见
  • 动画序列的执行顺序不符合需求,没有先完成"从底部到中心"的入场动画

修复后的完整代码

import React, { useState, useEffect, useRef } from 'react';
import { View, Image, StyleSheet, Animated, TouchableOpacity, Text, Dimensions } from 'react-native';
import Modal from 'react-native-modal';
import PickerSelect from 'react-native-picker-select';

const { height: SCREEN_HEIGHT } = Dimensions.get('window');
const LOGO_HEIGHT = 200;
// 计算初始Y偏移:让Logo初始在屏幕底部外一点的位置
const INITIAL_TRANSLATE_Y = (SCREEN_HEIGHT / 2) + (LOGO_HEIGHT / 2);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  logo: {
    width: LOGO_HEIGHT,
    height: LOGO_HEIGHT,
    resizeMode: 'contain',
  },
  modalContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  modalContent: {
    backgroundColor: 'white',
    padding: 30,
    borderRadius: 10,
    width: '80%',
  },
  chooseLanguageText: {
    marginBottom: 20,
  },
  picker: {
    width: '100%',
    marginBottom: 20,
    borderBottomWidth: 1,
    borderColor: 'gray',
    paddingVertical: 10,
  },
  nextButton: {
    backgroundColor: '#4CAF50',
    padding: 15,
    borderRadius: 5,
    alignItems: 'center',
    marginTop: 20,
  },
  nextButtonText: {
    color: 'white',
    fontSize: 16,
  },
  background: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: 'lightblue',
    opacity: 0,
  },
  buttonContainer: {
    marginVertical: 10,
    opacity: 0,
  },
  yellowButton: {
    backgroundColor: 'yellow',
    padding: 15,
    borderRadius: 10,
    marginVertical: 10,
  },
  grayButton: {
    backgroundColor: 'gray',
    padding: 15,
    borderRadius: 10,
    marginVertical: 10,
  },
  orangeButton: {
    backgroundColor: 'orange',
    padding: 15,
    borderRadius: 10,
    marginVertical: 10,
  },
  greenButton: {
    backgroundColor: 'green',
    width: 50,
    height: 50,
    borderRadius: 25,
    marginVertical: 10,
  },
  buttonText: {
    textAlign: 'center',
    color: 'black',
  },
  grayButtonContainer: {
    flexDirection: 'row',
  },
});

const DisplayLogo = () => {
  const translateY = useRef(new Animated.Value(INITIAL_TRANSLATE_Y)).current;
  const opacity = useRef(new Animated.Value(0)).current;
  const buttonOpacity = useRef(new Animated.Value(0)).current;
  const backgroundOpacity = useRef(new Animated.Value(0)).current;
  const [isModalVisible, setModalVisible] = useState(false);
  const [selectedLanguage, setSelectedLanguage] = useState('');
  const [selectedRegion, setSelectedRegion] = useState('');
  const [step, setStep] = useState(1);

  const handleButtonPress = (buttonName) => {
    console.log(`Button pressed: ${buttonName}`);
  };

  useEffect(() => {
    // 仅在初始加载时执行一次动画
    if (step === 1) {
      const imageAnimation = Animated.sequence([
        // 第一步:从底部移动到中心,同时显示Logo
        Animated.parallel([
          Animated.timing(translateY, {
            toValue: 0,
            duration: 500,
            useNativeDriver: true,
          }),
          Animated.timing(opacity, {
            toValue: 1,
            duration: 500,
            useNativeDriver: true,
          }),
        ]),
        // 停留2秒
        Animated.delay(2000),
        // 第二步:移动到顶部并淡出
        Animated.parallel([
          Animated.timing(translateY, {
            toValue: -(SCREEN_HEIGHT / 2) - (LOGO_HEIGHT / 2),
            duration: 500,
            useNativeDriver: true,
          }),
          Animated.timing(opacity, {
            toValue: 0,
            duration: 500,
            useNativeDriver: true,
          }),
        ]),
      ]);

      imageAnimation.start(() => {
        setModalVisible(true);
      });
    }

    return () => {
      translateY.setValue(INITIAL_TRANSLATE_Y);
      opacity.setValue(0);
    };
  }, [step]);

  const handleNext = () => {
    if (step === 1) {
      setStep(2);
    } else if (step === 2) {
      console.log('Selected Language:', selectedLanguage || 'English');
      setModalVisible(false);
      Animated.parallel([
        Animated.timing(buttonOpacity, {
          toValue: 1,
          duration: 500,
          useNativeDriver: true,
        }),
        Animated.timing(backgroundOpacity, {
          toValue: 1,
          duration: 500,
          useNativeDriver: true,
        }),
      ]).start();
      console.log('Selected Region:', selectedRegion || 'North India');
    }
  };

  return (
    <View style={styles.container}>
      <Animated.Image
        style={{ ...styles.logo, transform: [{ translateY }], opacity }}
        source={require('./assets/Logo.png')}
      />

      <Modal
        isVisible={isModalVisible}
        animationIn="slideInUp"
        animationOut="slideOutDown"
        onBackdropPress={null}
        style={styles.modalContainer}
      >
        <View style={styles.modalContent}>
          {step === 1 && (
            <>
              <Text style={styles.chooseLanguageText}>Select Your Region:</Text>
              <PickerSelect
                style={styles.picker}
                value={selectedRegion}
                onValueChange={(value) => setSelectedRegion(value)}
                items={[
                  { label: 'North India', value: 'North India' },
                  { label: 'South India', value: 'South India' },
                ]}
                placeholder={{}}
                Icon={() => (
                  <View
                    style={{
                      backgroundColor: 'transparent',
                      borderTopWidth: 10,
                      borderTopColor: 'gray',
                      borderRightWidth: 10,
                      borderRightColor: 'transparent',
                      borderLeftWidth: 10,
                      borderLeftColor: 'transparent',
                      width: 0,
                      height: 0,
                      alignSelf: 'center',
                    }}
                  />
                )}
              />
            </>
          )}

          {step === 2 && (
            <>
              <Text style={styles.chooseLanguageText}>Choose Your Preferred Language:</Text>
              <PickerSelect
                style={styles.picker}
                value={selectedLanguage}
                onValueChange={(value) => setSelectedLanguage(value)}
                items={[
                  { label: 'English', value: 'English' },
                  { label: 'Hindi', value: 'Hindi' },
                  { label: 'Telugu', value: 'Telugu' },
                ]}
                placeholder={{}}
                Icon={() => (
                  <View
                    style={{
                      backgroundColor: 'transparent',
                      borderTopWidth: 10,
                      borderTopColor: 'gray',
                      borderRightWidth: 10,
                      borderRightColor: 'transparent',
                      borderLeftWidth: 10,
                      borderLeftColor: 'transparent',
                      width: 0,
                      height: 0,
                      alignSelf: 'center',
                    }}
                  />
                )}
              />
            </>
          )}

          <TouchableOpacity style={styles.nextButton} onPress={handleNext}>
            <Text style={styles.nextButtonText}>Next</Text>
          </TouchableOpacity>
        </View>
      </Modal>

      <Animated.View style={{ ...styles.background, opacity: backgroundOpacity }} />

      <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}>
        {[
          'Tips For Farming',
          'Weather Updates',
          'Buy Implements',
          'Sell Harvest',
          'Discuss On Forums',
          'Donate',
        ].map((buttonName, index) => (
          <TouchableOpacity
            key={index}
            style={styles.yellowButton}
            onPress={() => handleButtonPress(buttonName)}
          >
            <Text style={styles.buttonText}>{buttonName}</Text>
          </TouchableOpacity>
        ))}
      </Animated.View>

      <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}>
        <View style={styles.grayButtonContainer}>
          {['Help', 'Settings'].map((buttonName, index) => (
            <TouchableOpacity
              key={index}
              style={styles.grayButton}
              onPress={() => handleButtonPress(buttonName)}
            >
              <Text style={styles.buttonText}>{buttonName}</Text>
            </TouchableOpacity>
          ))}
        </View>
      </Animated.View>

      <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}>
        <TouchableOpacity
          style={styles.orangeButton}
          onPress={() => handleButtonPress('Return Feedback')}
        >
          <Text style={styles.buttonText}>Return Feedback</Text>
        </TouchableOpacity>
      </Animated.View>

      <Animated.View style={{ ...styles.buttonContainer, opacity: buttonOpacity }}>
        <TouchableOpacity
          style={styles.greenButton}
          onPress={() => handleButtonPress('Contact')}
        />
      </Animated.View>
    </View>
  );
};

export default DisplayLogo;

关键优化点

  1. 动态计算初始位置:通过Dimensions获取屏幕高度,计算出Logo初始在屏幕底部外的位置,确保动画从底部开始
  2. 修正动画序列:
    • 先执行入场动画:同时将translateY从初始值变为0(中心)、opacity从0变为1(显示Logo)
    • 延迟2秒后执行退场动画:同时将translateY移到屏幕顶部外、opacity变为0(淡出)
  3. 简化依赖逻辑:仅在step为1时执行初始动画,避免不必要的重复触发
  4. 精确的退场位置:计算顶部偏移量,确保Logo完全移出屏幕后消失

内容的提问来源于stack exchange,提问作者Aditya Raj

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 01:10:57