为什么只有数组中的最后一个组件具有动画效果?

Why is only the last component in array animating?

目标:创建一个 OptionFan 按钮,当按下该按钮时,它会在其 Z 轴上旋转,并且 FanItems 从主按钮后面释放并沿着它们各自的向量移动。

OptionFan.js:

import React, { useState, useEffect } from 'react';
import { Image, View, Animated, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
import FanItem from './FanItem';

const { height, width } = Dimensions.get('window');

export default class OptionFan extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            animatedRotate: new Animated.Value(0),
            expanded: false
        };
    }

    handlePress = () => {
        if (this.state.expanded) {
            // button is opened
            Animated.spring(this.state.animatedRotate, {
                toValue: 0
            }).start();
            this.refs.option.collapse();
            this.setState({ expanded: !this.state.expanded });
        } else {
            // button is collapsed
            Animated.spring(this.state.animatedRotate, {
                toValue: 1
            }).start();
            this.refs.option.expand();
            this.setState({ expanded: !this.state.expanded });
        }
    };

    render () {
        const animatedRotation = this.state.animatedRotate.interpolate({
            inputRange: [ 0, 0.5, 1 ],
            outputRange: [ '0deg', '90deg', '180deg' ]
        });
        return (
            <View>
                <View style={{ position: 'absolute', left: 2, top: 2 }}>
                    {this.props.options.map((item, index) => (
                        <FanItem ref={'option'} icon={item.icon} onPress={item.onPress} index={index} />
                    ))}
                </View>

                <TouchableOpacity style={styles.container} onPress={() => this.handlePress()}>
                    <Animated.Image
                        resizeMode={'contain'}
                        source={require('./src/assets/img/arrow-up.png')}
                        style={{ transform: [ { rotateZ: animatedRotation } ], ...styles.icon }}
                    />
                </TouchableOpacity>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        justifyContent: 'center',
        alignItems: 'center',
        borderRadius: 30,
        backgroundColor: '#E06363',
        elevation: 15,
        shadowOffset: {
            height: 3,
            width: 3
        },
        shadowColor: '#333',
        shadowOpacity: 0.5,
        shadowRadius: 5,
        height: width * 0.155,
        width: width * 0.155
    },
    icon: {
        height: width * 0.06,
        width: width * 0.06
    },
    optContainer: {
        justifyContent: 'center',
        alignItems: 'center',
        borderRadius: 30,
        backgroundColor: '#219F75',
        elevation: 5,
        shadowOffset: {
            height: 3,
            width: 3
        },
        shadowColor: '#333',
        shadowOpacity: 0.5,
        shadowRadius: 5,
        height: width * 0.13,
        width: width * 0.13,
        position: 'absolute'
    }
});

FanItem.js:

import React, { useState } from 'react';
import { Image, Animated, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
const { width } = Dimensions.get('window');

export default class FanItem extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            animatedOffset: new Animated.ValueXY(0),
            animatedOpacity: new Animated.Value(0)
        };
    }

    expand () {
        let offset = { x: 0, y: 0 };
        switch (this.props.index) {
            case 0:
                offset = { x: -50, y: 20 };
                break;
            case 1:
                offset = { x: -20, y: 50 };
                break;
            case 2:
                offset = { x: 20, y: 50 };
                break;
            case 3:
                offset = { x: 75, y: -20 };
                break;
        }
        Animated.parallel([
            Animated.spring(this.state.animatedOffset, { toValue: offset }),
            Animated.timing(this.state.animatedOpacity, { toValue: 1, duration: 600 })
        ]).start();
    }

    collapse () {
        Animated.parallel([
            Animated.spring(this.state.animatedOffset, { toValue: 0 }),
            Animated.timing(this.state.animatedOpacity, { toValue: 0, duration: 600 })
        ]).start();
    }

    render () {
        return (
            <Animated.View
                style={
                    (this.props.style,
                    {
                        left: this.state.animatedOffset.x,
                        top: this.state.animatedOffset.y,
                        opacity: this.state.animatedOpacity
                    })
                }
            >
                <TouchableOpacity style={styles.container} onPress={this.props.onPress}>
                    <Image resizeMode={'contain'} source={this.props.icon} style={styles.icon} />
                </TouchableOpacity>
            </Animated.View>
        );
    }
}
const styles = StyleSheet.create({
    container: {
        justifyContent: 'center',
        alignItems: 'center',
        borderRadius: 30,
        backgroundColor: '#219F75',
        elevation: 5,
        shadowOffset: {
            height: 3,
            width: 3
        },
        shadowColor: '#333',
        shadowOpacity: 0.5,
        shadowRadius: 5,
        height: width * 0.13,
        width: width * 0.13,
        position: 'absolute'
    },
    icon: {
        height: width * 0.08,
        width: width * 0.08
    }
});

实施:

import React from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';
import Component from './Component';

const { height, width } = Dimensions.get('window');

const testArr = [
    {
        icon: require('./src/assets/img/chat.png'),
        onPress: () => alert('start chat')
    },
    {
        icon: require('./src/assets/img/white_video.png'),
        onPress: () => alert('video chat')
    },
    {
        icon: require('./src/assets/img/white_voice.png'),
        onPress: () => alert('voice chat')
    },
    {
        icon: require('./src/assets/img/camera.png'),
        onPress: () => alert('request selfie')
    }
];
const App = () => {
    return (
        <View style={styles.screen}>
            <Component options={testArr} />
        </View>
    );
};

const styles = StyleSheet.create({
    screen: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: '#E6E6E6'
    }
});

export default App;

问题:问题是,只有最后一个 FanItem 项目运行它的动画。 (不透明度和矢量平移)。在实现不透明度动画之前,我可以看出前三个 FanItem 实际上确实呈现在主按钮后面,因为按下主按钮时我可以看到它们,因为在单击按钮期间不透明度会暂时改变。

我的问题是 1) 为什么前三个映射项没有动画?和 2) 如何解决这个问题?

您正在 option 中存储 FanItem 个中的 ref 个。但是,ref 在地图的每次迭代中都会被覆盖。所以,最后它只存储 option 中最后 FanItemref。因此,首先在构造函数中声明一个数组来存储每个 FanItem:

ref
constructor(props) {
    super(props);
    // your other code
    this.refOptions = [];
  }

像这样分别存储每个 FanItemref

{this.props.options.map((item, index) => (
    <FanItem ref={(ref) => this.refOptions[index] = ref} icon={item.icon} onPress={item.onPress} index={index} />
))}

然后为每个 FanItem:

设置动画
for(var i = 0; i < this.refOptions.length; i++){
    this.refOptions[i].expand(); //call 'expand' or 'collapse' as required
}

这是博览点心link供大家参考: https://snack.expo.io/BygobuL3JL