Le but:

Créer une vue déroulante qui, lorsqu'elle est réduite, mesure 75 px de hauteur. lorsqu'il est étendu, sa hauteur de 225 pixels. Lorsque le bouton qui démarre les animations est enfoncé, il exécute deux animations en parallèle, une fonction de minutage pour la hauteur et une pour l'opacité. la deuxième partie de l'objectif est d'animer l'opacité de la partie inférieure de la liste déroulante qui est rendue disponible sur expanded == true.

Le problème:

L'animation s'exécute comme prévu lors de l'agrandissement de la vue, aucun problème. mais lors de la réduction de la liste déroulante, apparemment seule l'animation d'opacité s'exécute. le problème vient de la hauteur, qui n'est pas correctement reflétée dans la vue. il reste à la même hauteur que lorsqu'il est agrandi.

Voici le composant:

const ListItem = (props) => {
    const [checkInModal, setCheckInModal] = useState(false);
    const [animatedHeight, setAnimatedHeight] = useState(new Animated.Value(0))
    const [animatedOpacity] = useState(new Animated.Value(0))
    const [expanded, setExpanded] = useState(false);

    const toggleDropdown = () => {
        console.log("BEFORE ANIMATION =>" + "\n" + "expanded: " + expanded + "\n" + "animatedHeight: " + JSON.stringify(animatedHeight) + "\n" + "animatedOpacity: " + JSON.stringify(animatedOpacity))
        if (expanded == true) {
            // collapse dropdown
            Animated.parallel([
                Animated.timing(animatedHeight, {
                    toValue: 0,
                    duration: 200,
                }),
                Animated.timing(animatedOpacity, {
                    toValue: 0,
                    duration: 400,
                })
            ]).start()
            setTimeout(() => console.log("AFTER ANIMATION =>" + "\n" + "animatedHeight: " + JSON.stringify(animatedHeight) + "\n" + "animatedOpacity: " + JSON.stringify(animatedOpacity)), 3000)
            // This alone works properly*
            // Animated.timing(animatedHeight, {
            //  toValue: 0,
            //  duration: 200,
            // }).start()

        } else {
            // expand dropdown
            Animated.sequence([
                Animated.timing(animatedHeight, {
                    toValue: 100,
                    duration: 200,
                }),
                Animated.timing(animatedOpacity, {
                    toValue: 100,
                    duration: 400,
                })
            ]).start()
            setTimeout(() => console.log("AFTER ANIMATION =>" + "\n" + "animatedHeight: " + JSON.stringify(animatedHeight) + "\n" + "animatedOpacity: " + JSON.stringify(animatedOpacity)), 3000)
            // This alone works properly*
            // Animated.timing(animatedHeight, {
            //  toValue: 100,
            //  duration: 200,
            // }).start()
        }
        setExpanded(!expanded)
    }

    const interpolatedHeight = animatedHeight.interpolate({
        inputRange: [0, 100],
        outputRange: [75, 225]
    })

    const interpolatedOpacity = animatedOpacity.interpolate({
        inputRange: [0, 100],
        outputRange: [0.0, 1.0]
    })

    return (
        <Animated.View
            style={[styles.container, { height: interpolatedHeight }]}
        >
            <View style={{ flexDirection: 'row', justifyContent: 'space-between', }}>
                <View style={styles.leftContainer}>
                    <View style={{ flexDirection: 'row', alignItems: 'center' }}>
                        <Text style={styles.title}>{props.title}</Text>
                    </View>
                    <Text style={styles.subtitle}>{time()}</Text>
                </View>

                <View style={styles.rightContainer}>
                    <TouchableOpacity onPress={() => toggleDropdown()} style={styles.toggleBtn}>
                        <Image source={require('../assets/img/chevron-down.png')} resizeMode={'contain'} style={styles.chevron} />
                    </TouchableOpacity>
                </View>
            </View>

            {expanded == true ? (
                <Animated.View style={[styles.bottomContainer, { opacity: interpolatedOpacity }]}>
                    <Components.BodyText text="Subject:" style={{ fontFamily: Fonts.OPENSANS_BOLD }} />

                    <Components.BodyText text={props.subject} />
                </Animated.View>
            ) : null}
        </Animated.View>
    );
};

const styles = StyleSheet.create({
    container: {
        backgroundColor: '#fff',
        borderRadius: 25,
        width: width * 0.95,
        marginBottom: 5,
        marginHorizontal: 5,
        paddingVertical: 15,
        paddingHorizontal: 15
    },
    leftContainer: {
        justifyContent: 'space-between',
    },
    rightContainer: {
        flexDirection: 'row',
        alignItems: 'center'
    },
    title: {
        fontFamily: Fonts.OPENSANS_BOLD,
        fontSize: 20,
        color: '#454A66'
    },
    subtitle: {
        color: '#454A66',
        fontSize: 14
    },
    typeIcon: {
        height: 25,
        width: 25
    },
    chevron: {
        height: 15,
        width: 15
    },
    toggleBtn: {
        borderWidth: 1,
        borderColor: Colors.PRIMARY_DARK,
        borderRadius: 7,
        paddingTop: 4,
        paddingBottom: 2.5,
        paddingHorizontal: 4,
        marginLeft: 10
    },
    bottomContainer: {
        marginVertical: 20
    },
    buttonContainer: {
        flexDirection: 'row',
        width: 250,
        justifyContent: 'space-between',
        alignSelf: 'center',
        marginVertical: 20
    },
    noShadow: {
        elevation: 0,
        shadowOffset: {
            width: 0,
            height: 0
        },
        shadowRadius: 0,
    }
});

export default ListItem;

Les journaux de la console:

Une fois réduit, appuyez sur le bouton pour développer le journal dans la console:

 LOG  BEFORE ANIMATION =>
expanded: false
animatedHeight: 0
animatedOpacity: 0
 LOG  AFTER ANIMATION =>
animatedHeight: 100
animatedOpacity: 100

Une fois développé, appuyez sur le bouton pour réduire les journaux dans la console:

 LOG  BEFORE ANIMATION =>
expanded: true
animatedHeight: 100
animatedOpacity: 100
 LOG  AFTER ANIMATION =>
animatedHeight: 100
animatedOpacity: 100

Je ne sais pas exactement de quoi il s'agit, étant donné que, comme dit, l'animation d'opacité fonctionne sur l'effondrement et l'expansion. Si l'animation de hauteur n'est pas du tout touchée, sauf pour la retirer de Animated.parallel(), alors l'animation de hauteur fonctionne comme prévu lors de l'expansion et de la réduction.

Des idées sur ce qui se passe ici?

1
Jim 14 juin 2020 à 00:14

2 réponses

Meilleure réponse

Le problème survient lorsque vous essayez d'animer quelque chose qui n'est plus rendu. Apparemment, tout le bloc Animated.parallel ne fonctionnera pas si l'un des éléments n'est plus présent. Si une animation est arrêtée, toutes les animations à l'intérieur de Animated.parallel s'arrêteront. Vous pouvez remplacer ce comportement avec l'indicateur stopTogether:

    const toggleDropdown = () => {
        if (expanded === true) {
            // collapse dropdown
            Animated.parallel([
                Animated.timing(animatedHeight, {
                    toValue: 0,
                    duration: 200,
                }),
                Animated.timing(animatedOpacity, {
                    toValue: 0,
                    duration: 400,
                })
            ], { stopTogether: false }).start()
            // ...

Lorsque vous essayez de réduire la liste déroulante, l'indicateur expanded est défini sur false, ce qui fait que le composant "sujet" n'est plus rendu et que l'animation entière échoue. Lors du développement de la liste déroulante, l'indicateur expanded est défini sur true et le composant est maintenant rendu, l'animation fonctionne, mais cela n'a pas d'importance puisqu'elle n'était auparavant pas revenue à la valeur d'origine.

Essayez de supprimer la partie de rendu conditionnel expanded == true ? (je pense que cela n'affectera pas la sortie réelle). J'ai essayé votre code sans lui et semble fonctionner.

Une autre option consiste à setExpanded(true) avant de développer la liste déroulante afin que l'élément soit visible et prêt à être animé (et définissez-le sur false lorsque l'animation de réduction se termine):

    const toggleDropdown = () => {
        if (expanded === true) {
            // collapse dropdown
            Animated.parallel([
                Animated.timing(animatedHeight, {
                    toValue: 0,
                    duration: 200,
                }),
                Animated.timing(animatedOpacity, {
                    toValue: 0,
                    duration: 400,
                })
            ]).start(() => setExpanded(false))
        }
        else {
            setExpanded(true)
            // expand dropdown
            Animated.sequence([
                Animated.timing(animatedHeight, {
                    toValue: 100,
                    duration: 200,
                }),
                Animated.timing(animatedOpacity, {
                    toValue: 100,
                    duration: 400,
                })
            ]).start()
        }
    }

Vous rencontrerez des problèmes visuels (la partie "Objet" étant affichée à l'extérieur du composant, vous devriez peut-être ajouter un overflow: hidden ou modifier les horaires). Mais le problème d'animation devrait être résolu.

1
Carlos Jiménez 18 juin 2020 à 08:18

Bien que j'aie marqué correct et attribué une prime à une autre réponse, je poste le code que j'ai implémenté qui a abouti à la fonctionnalité souhaitée. Comme @Vivek Doshi l'a souligné dans un autre de mes messages, useEffect() m'a énormément aidé dans mes efforts.

Les deux principales choses que j'ai modifiées étaient à la place de basculer un simple booléen dans ma fonctionnalité de bascule:

const toggleTest = () => {
    setExpanded(!expanded)
}

Et implémenter useEffect ():

const ListItem = (props) => {

const [checkInModal, setCheckInModal] = useState(false);
const [animatedHeight] = useState(new Animated.Value(75))
const [animatedOpacity] = useState(new Animated.Value(0))
const [dynamicHeight, setDynamicHeight] = useState(0);
const [expanded, setExpanded] = useState(false);

useEffect(() => {
    if (expanded) {
        // expand dropdown
        Animated.parallel([
            Animated.timing(animatedHeight, {
                toValue: 225 + dynamicHeight,
                duration: 200,
            }),
            Animated.timing(animatedOpacity, {
                toValue: 100,
                duration: 200,
            })
        ]).start()
    } else {
        Animated.parallel([
            Animated.timing(animatedHeight, {
                toValue: 75,
                duration: 200,
            }),
            Animated.timing(animatedOpacity, {
                toValue: 0,
                duration: 400,
            })
        ]).start()
    }
}, [dynamicHeight, expanded])

Assurez-vous d'ajouter les dépendances appropriées à l'argument useEffect!

0
Jim 18 juin 2020 à 16:30