I’m getting a behaviour I wasn’t expecting in my React Native project using Animation.View on Android.
React-native 0.72
I’m loading a screen MyProfile and showing up user’s info and badges. When I press on a badge it opens a Popup component that displays as children a BadgePopUp component that displays badge’s info with multiple animations for each element.
It works nice except if I press on the badge immediately when badges are shown (page might still be loading). By using console.logs I see that it waits for page to fully load and then launches the pop up thus creating a slight delay.
I’m ok with it but the problem is it opens the pop up all blank then after the timing of the animations it shows the items (sometimes only the badge img is delayed). Then if I close it and press it again, or if on screen load I wait a coupla seconds before I press on it, it works fine.
This does not happen in ios, only in android, and only in this page, not in the rest of the screens.
My implementation:
MyProfile.js:
...
const MyProfile = () => {
const { user, favoriteCourts, addedCourts, badges } = useSelector(
state => state.userReducer,
);
...
const verifiedBadges = JSON.parse(Storage.getString('badges'));
const getVerifiedBadge = badgeId => {
const badge = verifiedBadges?.find(item => item.id === badgeId);
return badge;
};
const badge = getVerifiedBadge(user?.user_badge_id);
...
const [isPopUpVisible, setIsPopUpVisible] = useState(false);
const [highestLevelBadges, setHighestLevelBadges] = useState(
getHighestLevelBadges(badges),
);
const [badgeDetailsToDisplay, setBadgeDetailsDataToDisplay] = useState(null);
...
const handleMyBadges = () => {
navigation.navigate('MyBadges');
};
const handleBadgeDetails = (
badgeLevel,
badgeImg,
badgeName,
badgeQuantity,
badgeDescription,
) => {
setBadgeDetailsDataToDisplay({
badgeLevel: badgeLevel,
badgeImg: badgeImg,
badgeName: badgeName,
badgeQuantity: badgeQuantity,
badgeDescription: badgeDescription,
});
setIsPopUpVisible(true);
};
const getBadgeQuantityText = actionName => {
switch (actionName) {
case 'court_registrations':
return `${addedCourts?.length} ${t('Court_hunter_add_info')}`;
break;
case 'court_checkins':
return `${user?.num_checkins} ${t('King_of_the_court_add_info')}`;
break;
default:
return t('Court_hunter_add_info');
break;
}
};
const handleBadgesToDisplay = () => {
badgesToDisplay = highestLevelBadges.map((badge, index) => {
const badgeImageSource = gatBadgeImage(badge.id);
const badgeLevel = getBadgeLevelName(badge.options.level);
const badgeName =
badge.options.action_name === 'court_registrations'
? t('Court_hunter')
: badge.name.split(' - ', 1);
const badgeQuantity = getBadgeQuantityText(badge.options.action_name);
const badgeDescription =
lang !== 'en-US' ? badge.description : badge.description_en;
return (
<View key={index} style={{ left: 4, marginHorizontal: 10 }}>
<ContainedImage
image={badgeImageSource}
size={'extra-small'}
action={() => {
console.log('pressed on badge');
handleBadgeDetails(
badgeLevel,
badgeImageSource,
badgeName,
badgeQuantity,
badgeDescription,
);
}}
/>
</View>
);
});
return badgesToDisplay;
};
useEffect(() => {
console.log('isFocused, navigationState');
const scrollIfNeeded = () => {
if (scrollViewRef.current) {
scrollViewRef.current.scrollTo({ y: 0, animated: true });
}
};
// Get the current screen's route key
const currentRouteKey = navigationState.routes[navigationState.index].key;
// Check if the tab is focused and the current screen is the starting screen
const isTabFocused = isFocused && currentRouteKey === 'StartingScreen';
if (isTabFocused) {
scrollIfNeeded();
}
}, [isFocused, navigationState]);
useEffect(() => {
console.log('in addedCourts, favoriteCourts useEffect');
if (addedCourts !== null && favoriteCourts !== null && user !== null) {
setFavCourtsToSend(favoriteCourts);
handleGetMyEvents();
setIsLoading(false);
}
}, [addedCourts, favoriteCourts]);
useEffect(() => {
console.log('in events and filteredEvents useEffect');
if (events && !filteredEvents) {
setFilteredEvents(filterEventsByDate(events));
}
}, [events, filteredEvents]);
return (
<FullContainer
header={true}
hasActions={true}
logo={true}
hasBackButton={false}
hideSearch={true}
hideAddCourt={true}
showConfig={true}
hasNotifications={true}>
{isPageLoading && <LoaderView />}
{isPopUpVisible === true && badgeDetailsToDisplay !== null && (
<PopUp
visible={isPopUpVisible}
onClose={() => {
setIsPopUpVisible(false);
}}
fixedHeight={true}
hasDarkTransparency={true}>
<BadgePopUp
title={badgeDetailsToDisplay.badgeName}
image={badgeDetailsToDisplay.badgeImg}
badgeName={badgeDetailsToDisplay.badgeLevel}
additionalInfo={badgeDetailsToDisplay.badgeQuantity}
description={badgeDetailsToDisplay.badgeDescription}
hasAnimations={true}
/>
</PopUp>
)}
<AlertModal
modalVisible={modalVisible}
setModalVisible={setModalVisible}
title={modalTitle}
text={modalText}
hasCloseButton={true}
/>
<ScrollView
ref={scrollViewRef}
style={[{ paddingHorizontal: 0 }, Styles.flexOne]}
showsVerticalScrollIndicator={false}>
<View
style={[
ProfilePageStyles.profileInfoContainer,
ProfilePageStyles.paddingPage,
]}>
<Text style={ProfilePageStyles.title}>{t('tab_profile')}</Text>
<View style={ProfilePageStyles.profileHeaderContainer}>
<View style={ProfilePageStyles.boxPicture}>
<CircleDisplay
size={'small'}
pressable={false}
type={'image'}
src={user?.picture_url}
colorType={'grey'}
/>
</View>
<View style={ProfilePageStyles.boxSocialBar}>
<SocialBar
checkins={user?.num_checkins}
followers={user?.num_followers}
following={user?.num_following}
onPress={handleGoToFollowersPage}
/>
</View>
</View>
<View style={Styles.flexRow}>
<Text style={ProfilePageStyles.labelBold}>
{user?.first_name} {user?.last_name}
</Text>
{user?.user_badge_id && (
<Pressable
onPress={() => {
handleToast(
lang === 'en-US' ? badge.description_en : badge.description,
badge.name,
'gray',
);
}}
style={[
ProfilePageStyles.badgePressable,
{ backgroundColor: badge?.hex_color },
]}>
<Text style={ProfilePageStyles.badgeText}>{badge?.name}</Text>
</Pressable>
)}
</View>
<Text style={ProfilePageStyles.labelRegular}>@{user?.username}</Text>
<Text style={[ProfilePageStyles.labelRegular, { marginTop: 6 }]}>
{user?.profile_info}
</Text>
<View style={ProfilePageStyles.profileEditContainer}>
<HoopersButton
size={'small'}
theme1={'primary'}
onPress1={() => {
handleEditProfile();
}}
text1={t('Edit_profile')}
/>
</View>
</View>
<View style={{ paddingBottom: 20, backgroundColor: Colors.HPS_WHITE }}>
<View
style={[
Styles.alignDisplayFlexRow,
Styles.rowStartCenter,
ProfilePageStyles.paddingPage,
]}>
<Text style={[ProfilePageStyles.labelBold, { marginRight: 8 }]}>
{t('My_badges')}
</Text>
<HoopersButton
size={'tiny'}
theme1={'outline'}
onPress1={() => {
handleMyBadges();
}}
text1={t('See_all')}
/>
</View>
{badges.length > 0 ? (
<View style={[Styles.alignDisplayFlexRow, Styles.rowStartCenter]}>
<ScrollView
horizontal={true}
style={{ width: width, marginTop: 13 }}>
{handleBadgesToDisplay()}
</ScrollView>
</View>
) : (
<View style={[Styles.center, { marginTop: 13 }]}>
<Text style={ProfilePageStyles.labelRegular}>
{t('User_has_no_badges')}
</Text>
</View>
)}
</View>
<View style={ProfilePageStyles.profileCourtsContainer}>
{isLoading && <LoaderView color={Colors.HPS_OFWHITE} />}
{!isLoading && renderAddedCourtsSection()}
{!isLoading && renderFavoriteCourtsSection()}
{!isLoading && filteredEvents && renderEventsSection()}
</View>
</ScrollView>
</FullContainer>
);
};
export default MyProfile;
PopUp.js:
...
const PopUp = ({
onClose,
children,
visible,
fixedHeight,
hasDarkTransparency,
hasConfettis,
}) => {
const { width, height } = useWindowDimensions();
const confettiRef = useRef(null);
useEffect(() => {
if (visible && hasConfettis && confettiRef.current) {
confettiRef.current.start();
}
}, [visible]);
return (
<Modal
animationType="fade"
transparent={true}
visible={visible}
onRequestClose={onClose}>
<View
style={[
PopUpStyles.container,
hasDarkTransparency
? {
backgroundColor: Colors.darkTransparency,
}
: {
backgroundColor: Colors.lightTransparency,
},
]}>
<View
style={[
PopUpStyles.secondContainer,
{ width: width },
fixedHeight === true && { height: height * 0.8, maxHeight: 600 },
]}>
<View
style={[
PopUpStyles.popup,
{
width: width * 0.85,
maxWidth: 400,
},
]}>
<Pressable
style={{
position: 'absolute',
zIndex: 99,
right: -10,
top: -10,
}}
onPress={onClose}>
<Icon
name="closeCircle"
width="30"
height="30"
fill={Colors.HPS_ORANGE}
stroke={Colors.HPS_GREY}
/>
</Pressable>
{children}
</View>
</View>
</View>
{hasConfettis && (
<ConfettiCannon
ref={confettiRef}
count={120} // Number of confetti particles
origin={{ x: -10, y: 0 }} // Starting position (in this case, bottom-left corner)
autoStart={true} // Don't auto-start confetti, start it manually after other animations
fadeOut={true} // Make confetti fade out after some time
/>
)}
</Modal>
);
};
export default PopUp;
BadgePopUp.js:
const BadgePopUp = ({
title,
subtitle,
image,
badgeName,
additionalInfo,
description,
action,
hasAnimations,
}) => {
console.log('BadgePopUp: Rendering component');
...
const spinValue = useRef(new Animated.Value(0.01)).current;
const scaleValue = useRef(new Animated.Value(2.5)).current;
const laterOpacityAnimValue = useRef(new Animated.Value(0.01)).current;
const firstOpacityAnimValue = useRef(new Animated.Value(0.01)).current;
const slideDownValue = useRef(new Animated.Value(-70)).current;
const slideRightValue = useRef(new Animated.Value(-200)).current;
const slideLeftValue = useRef(new Animated.Value(200)).current;
const setAnimation = (toValue, duration, easing, useNativeDriver) => {
return {
toValue: toValue,
duration: duration,
easing: easing,
useNativeDriver: useNativeDriver,
};
};
const renderImage = () => {
return <Image source={image} width={130} height={130} />;
};
useEffect(() => {
if (hasAnimations === true && image) {
Animated.parallel([
Animated.sequence([
Animated.delay(100),
Animated.timing(firstOpacityAnimValue, {
toValue: 1,
duration: 500,
easing: Easing.ease,
useNativeDriver: true,
}),
]),
Animated.sequence([
Animated.delay(200),
Animated.timing(laterOpacityAnimValue, {
toValue: 1,
duration: 350,
easing: Easing.ease,
useNativeDriver: true,
}),
]),
Animated.timing(spinValue, {
toValue: 1,
duration: 500,
easing: Easing.ease,
useNativeDriver: true,
}),
Animated.timing(scaleValue, {
toValue: 1,
duration: 500,
easing: Easing.ease,
useNativeDriver: true,
}),
Animated.timing(slideDownValue, {
toValue: 1,
duration: 350,
easing: Easing.ease,
useNativeDriver: true,
}),
Animated.timing(slideRightValue, {
toValue: 1,
duration: 500,
easing: Easing.ease,
useNativeDriver: true,
}),
Animated.timing(slideLeftValue, {
toValue: 1,
duration: 500,
easing: Easing.ease,
useNativeDriver: true,
}),
]).start(() => {
console.log('BadgePopUp: Animations completed');
});
}
}, [
spinValue,
scaleValue,
laterOpacityAnimValue,
firstOpacityAnimValue,
slideDownValue,
slideRightValue,
slideLeftValue,
hasAnimations,
image,
]);
const spin = spinValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
return (
<>
<View
style={[
BadgePopUpStyles.container,
{
alignItems: 'center',
justifyContent: 'space-around',
maxHeight: propsVol > 5 ? 500 : 400,
},
]}>
{title && (
<View style={BadgePopUpStyles.titleContainer}>
{hasAnimations === true ? (
<Animated.View
style={{
transform: [{ translateY: slideDownValue }],
opacity: firstOpacityAnimValue,
}}>
<Text style={BadgePopUpStyles.title}>{title}</Text>
</Animated.View>
) : (
<Text style={BadgePopUpStyles.title}>{title}</Text>
)}
</View>
)}
{subtitle && (
<View style={BadgePopUpStyles.subtitleContainer}>
{hasAnimations === true ? (
<Animated.View
style={{
transform: [{ translateX: slideRightValue }],
opacity: laterOpacityAnimValue,
}}>
<Text style={BadgePopUpStyles.subtitle}>{subtitle}</Text>
</Animated.View>
) : (
<Text style={BadgePopUpStyles.subtitle}>{subtitle}</Text>
)}
</View>
)}
{image && (
<>
{hasAnimations === true ? (
<Animated.View
style={{
transform: [{ rotate: spin }, { scale: scaleValue }],
opacity: laterOpacityAnimValue,
}}>
<View style={BadgePopUpStyles.imageContainer}>
{renderImage()}
</View>
</Animated.View>
) : (
<View style={BadgePopUpStyles.imageContainer}>
{renderImage()}
</View>
)}
</>
)}
{(badgeName || additionalInfo) && (
<View style={BadgePopUpStyles.badgeContainer}>
{badgeName &&
(hasAnimations === true ? (
<Animated.View
style={{
transform: [{ translateX: slideRightValue }],
opacity: laterOpacityAnimValue, // Apply opacity animation to ContainedImage
}}>
<Text
style={[BadgePopUpStyles.subtitle, { marginBottom: 5 }]}>
{badgeName}
</Text>
</Animated.View>
) : (
<Text style={[BadgePopUpStyles.subtitle, { marginBottom: 5 }]}>
{badgeName}
</Text>
))}
{additionalInfo &&
(hasAnimations === true ? (
<Animated.View
style={{
transform: [{ translateX: slideLeftValue }],
opacity: laterOpacityAnimValue,
}}>
<Text style={BadgePopUpStyles.description}>
{additionalInfo}
</Text>
</Animated.View>
) : (
<Text style={BadgePopUpStyles.description}>
{additionalInfo}
</Text>
))}
</View>
)}
{description && (
<View style={BadgePopUpStyles.descriptionContainer}>
{hasAnimations === true ? (
<Animated.View
style={{
transform: [{ translateX: slideRightValue }],
opacity: laterOpacityAnimValue,
}}>
<Text style={BadgePopUpStyles.description}>{description}</Text>
</Animated.View>
) : (
<Text style={BadgePopUpStyles.description}>{description}</Text>
)}
</View>
)}
{action &&
(hasAnimations === true ? (
<Animated.View
style={{
opacity: laterOpacityAnimValue,
}}>
<TextLink
text={action.title ? action.title : 'See more'}
type={'orange'}
size={16}
onPress={action.onPress}
/>
</Animated.View>
) : (
<TextLink
text={action.title ? action.title : 'See more'}
type={'orange'}
size={16}
onPress={action.onPress}
/>
))}
</View>
</>
);
};
export default BadgePopUp;
Any idea why this might be happening?