Related
I'm working in app that needs flatlist animation with RTL devices. I built a small app to understand how it will work. When using the app with LTR devices, it works just fine, and when I use I18nManager.forceRTL(true) everything messed up.
I have three slides with two components, the first component shows the slide itself and the second shows the number of the slide.
When apply force RTL the first component starts with the first slide but the second component start form the last one!
Also, when I use two animations (transform and opacity) for one element of the first component. The first slide and the last slide disappeared!
Here is my code with force RTL:
import React, { useRef } from "react";
import {
SafeAreaView,
View,
StyleSheet,
Text,
StatusBar,
Dimensions,
Animated,
} from "react-native";
import { I18nManager } from "react-native";
I18nManager.allowRTL(true);
I18nManager.forceRTL(true);
const { width, height } = Dimensions.get("window");
const DATA = [
{
id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
title: "First Slide",
number: "1",
},
{
id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
title: "Second Slide",
number: "2",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d72",
title: "Third Slide",
number: "3",
},
];
const Item = ({ title, index, scrollX }) => {
const inputRange = [(index - 1) * width, index * width, (index + 1) * width];
const opacityInputRange = [
(index - 0.4) * width,
index * width,
(index + 0.4) * width,
];
const scale = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
});
const opacity = scrollX.interpolate({
inputRange: opacityInputRange,
outputRange: [0, 1, 0],
});
return (
<View style={styles.mainConatainer}>
<Animated.View
style={{
backgroundColor: "blue",
width: width / 1.6,
height: height / 3,
alignItems: "center",
justifyContent: "center",
borderRadius: width,
transform: [{ scale }],
opacity,
}}
>
<Animated.Text
style={{
fontSize: 32,
color: "white",
transform: [{ scale }],
}}
>
{title}
</Animated.Text>
</Animated.View>
</View>
);
};
const Footer = ({ scrollX }) => {
return (
<View>
{DATA.map(({ number }, index) => {
const inputRange = [
(index - 1) * width,
index * width,
(index + 1) * width,
];
const scale = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
});
return (
<Animated.View key={index} style={{ transform: [{ scale }] }}>
<Text
style={{
fontSize: 50,
alignItems: "center",
marginBottom: 50,
position: "absolute",
bottom: "40%",
right: "50%",
color: "blue",
}}
>
{number}
</Text>
</Animated.View>
);
})}
</View>
);
};
export default function App() {
const scrollX = useRef(new Animated.Value(0)).current;
return (
<SafeAreaView style={styles.container}>
<Animated.FlatList
data={DATA}
renderItem={({ item, index }) => (
<Item {...item} index={index} scrollX={scrollX} />
)}
keyExtractor={(item) => item.id}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
inverted={true}
/>
<Footer scrollX={scrollX} />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
backgroundColor: "lightgrey",
},
mainConatainer: {
width,
alignItems: "center",
justifyContent: "center",
},
item: {
backgroundColor: "blue",
width: width / 1.6,
height: height / 3,
alignItems: "center",
justifyContent: "center",
borderRadius: width,
},
title: {
fontSize: 64,
color: "white",
},
});
Without force RTL:
import React, { useRef } from "react";
import {
SafeAreaView,
View,
StyleSheet,
Text,
StatusBar,
Dimensions,
Animated,
} from "react-native";
const { width, height } = Dimensions.get("window");
const DATA = [
{
id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
title: "First Slide",
number: "1",
},
{
id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
title: "Second Slide",
number: "2",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d72",
title: "Third Slide",
number: "3",
},
];
const Item = ({ title, index, scrollX }) => {
const inputRange = [(index - 1) * width, index * width, (index + 1) * width];
const opacityInputRange = [
(index - 0.4) * width,
index * width,
(index + 0.4) * width,
];
const scale = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
});
const opacity = scrollX.interpolate({
inputRange: opacityInputRange,
outputRange: [0, 1, 0],
});
return (
<View style={styles.mainConatainer}>
<Animated.View
style={{
backgroundColor: "blue",
width: width / 1.6,
height: height / 3,
alignItems: "center",
justifyContent: "center",
borderRadius: width,
transform: [{ scale }],
opacity,
}}
>
<Animated.Text
style={{
fontSize: 32,
color: "white",
transform: [{ scale }],
}}
>
{title}
</Animated.Text>
</Animated.View>
</View>
);
};
const Footer = ({ scrollX }) => {
return (
<View>
{DATA.map(({ number }, index) => {
const inputRange = [
(index - 1) * width,
index * width,
(index + 1) * width,
];
const scale = scrollX.interpolate({
inputRange,
outputRange: [0, 1, 0],
});
return (
<Animated.View key={index} style={{ transform: [{ scale }] }}>
<Text
style={{
fontSize: 50,
alignItems: "center",
marginBottom: 50,
position: "absolute",
bottom: "40%",
right: "50%",
color: "blue",
}}
>
{number}
</Text>
</Animated.View>
);
})}
</View>
);
};
export default function App() {
const scrollX = useRef(new Animated.Value(0)).current;
return (
<SafeAreaView style={styles.container}>
<Animated.FlatList
data={DATA}
renderItem={({ item, index }) => (
<Item {...item} index={index} scrollX={scrollX} />
)}
keyExtractor={(item) => item.id}
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
inverted={true}
/>
<Footer scrollX={scrollX} />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
backgroundColor: "lightgrey",
},
mainConatainer: {
width,
alignItems: "center",
justifyContent: "center",
},
item: {
backgroundColor: "blue",
width: width / 1.6,
height: height / 3,
alignItems: "center",
justifyContent: "center",
borderRadius: width,
},
title: {
fontSize: 64,
color: "white",
},
});
I am having this weird behavior with react-native-gesture-handler's Swipeable component. Intro animations don't work but after pressing the button in Swipeable component, animations work fading out. Here is my BookmarkItem component with Swipeable component:
renderLeftActions = (progress, dragX) => {
const trans = dragX.interpolate({
inputRange: [0, 50, 100, 101],
outputRange: [-20, 0, 0, 1],
});
const localStyles = {
rectButton: {
alignItems: "stretch",
justifyContent: "center",
},
icon: {
transform: [{ translateX: trans }],
paddingHorizontal: 20,
flexDirection: 'row',
alignItems: 'center',
height: '100%',
backgroundColor: 'rgba(0,0,0,0.1)'
},
};
return (
<RectButton
style={localStyles.rectButton}
onPress={() => {
this.props.removeFromBookmarks(this.props.title, this.props.p);
this.swipableRef?.current?.close();
}}
>
<Animated.View style={localStyles.icon}>
<Icon name="heart" size={35} color="rgb(245, 66, 66)" />
</Animated.View>
</RectButton>
);
};
render() {
return (
<Swipeable
ref={this.swipableRef}
renderLeftActions={this.renderLeftActions}
overshootLeft={false}
>
<TouchableOpacity onPress={this.onPress}>
<Text numberOfLines={2} style={styles.sectionText}>
{this.props.p}
</Text>
</TouchableOpacity>
</Swipeable>
);
}
So I originally needed to pass states while navigating from a screen to another because I thought that would be enough to update a button as well as the screen iteself. However, the button that controlled the states is not being updated with the screen.
In the demo provided below(I included the code here as well) you can see that when navigating from Screen3, the state updates so that the red screen renders but the button at the top does not update as well.
How can I update the button along with the screen being updated?
I need to go from screen3 to the red screen while the button at the top shows we are on the red screen as well.
Here is the demo as well as the code below. Please keep in mind you must run the snack on IOS or android and you need to be running Expo Version 42(located in the bottom right of the screen)
Thank you for any insight at all! I appreciate it more than you know.
Home.js
import Slider from './components/slider'
const Home = ({ route }) => {
const [isVisile, setIsVisible] = React.useState(true);
const [whichComponentToShow, setComponentToShow] = React.useState("Screen1");
React.useEffect(() => {
if(route.params && route.params.componentToShow) {
setComponentToShow(route.params.componentToShow);
}
}, [route.params]);
const goToMap = () => {
setComponentToShow("Screen2");
}
const goToList = () => {
setComponentToShow("Screen1");
}
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
{whichComponentToShow === 'Screen1' && <ListHome />}
{whichComponentToShow === 'Screen2' && <MapHome />}
<View style={{position: 'absolute', top: 0, left: 0, right: 1}}>
<Slider
renderMap={goToMap}
renderList={goToList}
/>
</View>
</View>
);
}
Screen3.js
const Screen3 = (props) => {
const navigation = useNavigation();
const onPress = () => {
navigation.navigate('Home', {
screen: 'Home',
params: {
componentToShow: 'Screen2'
}
});
}
return (
<View
style={{
flex: 1,
backgroundColor: 'white',
justifyContent: 'center',
alignItems: 'center',
}}>
<TouchableOpacity onPress={onPress}>
<Text style={{ color: 'black', fontSize: 25 }}>
Navigate to home and change to map screen
</Text>
</TouchableOpacity>
</View>
);
};
Finally Slider.js
const Slider = (props) => {
const [active, setActive] = useState(false)
let transformX = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (active) {
Animated.timing(transformX, {
toValue: 1,
duration: 300,
useNativeDriver: true
}).start()
} else {
Animated.timing(transformX, {
toValue: 0,
duration: 300,
useNativeDriver: true
}).start()
}
}, [active]);
const rotationX = transformX.interpolate({
inputRange: [0, 1],
outputRange: [2, Dimensions.get('screen').width / 4]
})
return (
code for animation
)
Your Slider component needs to listen to screen focus with useFocusEffect and react accordingly. I added new props active to detect if the map screen is active.
Slider.js
import * as React from 'react';
import { useState, useEffect, useRef } from 'react'
import { View, Text, StyleSheet, Animated, TouchableOpacity, SafeAreaView, Dimensions, } from 'react-native';
import {
scale,
verticalScale,
moderateScale,
ScaledSheet,
} from 'react-native-size-matters';
import { useFocusEffect } from '#react-navigation/native';
const Slider = (props) => {
const [active, setActive] = useState(false)
let transformX = useRef(new Animated.Value(0)).current;
useFocusEffect( React.useCallback(()=>{
setActive(Boolean(props.active))
console.log()
},[props.active]))
useEffect(() => {
if (active) {
Animated.timing(transformX, {
toValue: 1,
duration: 300,
useNativeDriver: true
}).start()
} else {
Animated.timing(transformX, {
toValue: 0,
duration: 300,
useNativeDriver: true
}).start()
}
}, [active]);
const rotationX = transformX.interpolate({
inputRange: [0, 1],
outputRange: [2, Dimensions.get('screen').width / 4]
})
return (
<SafeAreaView style={{
alignItems: 'center',
backgroundColor:'transparent'
}}>
<View style={{
flexDirection: 'row',
position: 'relative',
height: 45,
width: 240,
borderRadius: 10,
backgroundColor: 'white',
marginHorizontal: 5
}}>
<Animated.View
style={{
position: 'absolute',
height: 45 - 2*2,
top: 2,
bottom: 2,
borderRadius: 10,
width: Dimensions
.get('screen').width / 3 - 3.5 ,
transform: [
{
translateX: rotationX
}
],
backgroundColor: '#d1cfcf',
}}
>
</Animated.View>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(false); props.renderList() }}>
<Text>
List
</Text>
</TouchableOpacity>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(true); props.renderMap() }}>
<Text>
Map
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
export default Slider
I update Slider Component in Home.js as below.
<Slider
renderMap={goToMap}
renderList={goToList}
active={route.params && route.params.componentToShow==='Screen2'}
/>
Check full working snack:
https://snack.expo.dev/#emmbyiringiro/283f93
I have been trying to solve this for ages now, I've posted here before and was given generic copy-paste from other places I've visited / looked at. I've stripped the component down to nothing but the gestures and the render. I'm really not getting how this is supposed to work.
There is an Animated.View. It has a child TouchableOpacity.
I can move the Animated.View around with pan gestures.
When I press the TouchableOpacity there is a (small) random chance that I will get the response from the onPress console.log().
Am I missing something still? I need the button to log to the console every time it's pressed, not 5% of the time. It's unreliable.
Suggestions? Advice? Is this not something that can actually be done in React native?
import React, { Component } from "react";
import styled from "styled-components";
import {
Text,
View,
Animated,
TouchableOpacity,
PanResponder
} from "react-native";
export default class TestComponent extends Component {
state = {
pan: new Animated.ValueXY()
};
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([
null,
{ dx: this.state.pan.x, dy: this.state.pan.y }
]),
onPanResponderRelease: () => {
const positionY = this.state.pan.y.__getValue();
if (positionY > 200) {
Animated.timing(this.state.pan, {
toValue: { x: 0, y: 1000 }
}).start(() => {
this.state.pan.setValue({ x: 0, y: 0 });
});
} else {
Animated.spring(this.state.pan, {
toValue: { x: 0, y: 0 }
}).start();
}
}
});
}
sayHello = () => {
console.log("hello");
};
render() {
return (
<Animated.View
style={[
{ height: 150, width: 150, borderWidth: 2, borderColor: "orange" },
{
transform: [
{ translateX: this.state.pan.x },
{ translateY: this.state.pan.y }
]
}
]}
{...this._panResponder.panHandlers}
>
<TouchableOpacity
style={{ height: 100, width: 100, borderWidth: 2, borderColor: "red" }}
onPress={() => console.log("hello?")}
>
<Text style={{ height: 50, width: 50, borderWidth: 2, borderColor: "blue" }}>
Hello
</Text>
</TouchableOpacity>
</Animated.View>
);
}
}
<Animated.View
style={[
{ height: 150, width: 150, borderWidth: 2, borderColor: "orange" },
{ transform: [
{ translateX: this.state.pan.x },
{ translateY: this.state.pan.y }
]
}
]}
{...this._panResponder.panHandlers}
>
<TouchableOpacity
style={{flex : 1, borderWidth: 2, borderColor: "red" }}
onPress={() => console.log("hello?")}
>
<Text style={{ height: 50, width: 50, borderWidth: 2, borderColor: "blue" }}>
Hello
</Text>
</TouchableOpacity>
</Animated.View>
);
}
I'm working on a React Native code that renders “cards” on an array using a map function. Each card is wrapped in a touchableOpacity component, so that when a user taps the card, it would flip. Currently the issue is that, if a user taps to flip one card, all neighboring cards flip as well. I would like to have the flip functionality for each card be independent. When a card is flipped it should not trigger the flipping of neighboring cards as well. Thanks in advance to reading this.
class SavedBooks extends Component {
componentWillMount() {
this.animatedValue = new Animated.Value(0);
this.value = 0;
this.animatedValue.addListener(({ value }) => { this.value = value })
}
frontCardStyle() {
this.frontInterpolate = this.animatedValue.interpolate({
inputRange: [0, 180],
outputRange: ['0deg', '180deg']
})
const frontAnimatedStyle = {
transform: [ { rotateY: this.frontInterpolate }]
}
return frontAnimatedStyle
}
backCardStyle() {
this.backInterpolate = this.animatedValue.interpolate({
inputRange: [0, 180],
outputRange: ['180deg', '360deg']
})
const backAnimatedStyle = { transform: [{ rotateY: this.backInterpolate }] }
return backAnimatedStyle
}
flipCard() {
if (this.value >= 90) {
Animated.spring(this.animatedValue, {
toValue: 0,
friction: 8,
tension: 10
}).start();
} else if (this.value < 90) {
Animated.spring(this.animatedValue, {
toValue: 180,
friction: 8,
tension: 10
}).start();
}
}
renderElemets(color) {
const { savedBooks } = this.props.book
return savedBooks.map((book, index) => {
return (
<View
key={index}
style={{ alignItems: 'center' }}>
<TouchableOpacity onPress={() => this.flipCard()} >
<Text
style={{ fontFamily: 'Helvetica',
fontSize: 25,
padding: 15 }}>
{book.title}
</Text>
<Animated.View>
<Animated.Image
style={[this.frontCardStyle(), styles.cardStyle]}
source={{ uri: book.image }}
/>
<Animated.View style={[this.backCardStyle(), styles.cardStyle, styles.flipCardBack]}>
<Text>{book.description}</Text>
</Animated.View>
</Animated.View>
</TouchableOpacity>
</View>
)
});
}
render() {
return (
<ScrollView>
{this.renderElemets(color)}
</ScrollView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFF',
},
imageStyle: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
cardStyle: {
height: 400,
width: 250,
backfaceVisibility: 'hidden',
},
flipCardBack: {
position: "absolute",
top: 0,
},
});
Because all the cards share the same style. You should create a card component so each card can have their own style and the press event won't affect other cards.
I had this exact problem and solved it by adding something like an identifier or tag to the clicked item in the array. The idea is quite similar to adding a class 'active' to the item clicked in the array, that way your flipCard() function will only be run on the item which has been labelled active. In short, only add the frontCardStyle() or backCardStyle() to the item that has been clicked and labelled 'active'. That way only the active item will flip.
I followed this example and came up with the solution below;
constructor(props) {
super(props);
this.state = {
activeItem: {},
}
this.toggleActiveItem = this.toggleActiveItem.bind(this);
}
toggleActiveItem(index) {
this.setState({
activeItem: {
[index]: true
}
});
this.flipCard();
}
renderElemets(color) {
const { savedBooks } = this.props.book
return savedBooks.map((book, index) => {
return (
<View
key={index}
style={{ alignItems: 'center' }}>
<TouchableOpacity onPress={() => this.toggleActiveItem(index)} >
<Text
style={{ fontFamily: 'Helvetica',
fontSize: 25,
padding: 15 }}>
{book.title}
</Text>
<Animated.View>
<Animated.Image
style={[this.state.activeItem[index] && this.frontCardStyle(), styles.cardStyle]}
source={{ uri: book.image }}
/>
<Animated.View style={[this.state.activeItem[index] && this.backCardStyle(), styles.cardStyle, styles.flipCardBack]}>
<Text>{book.description}</Text>
</Animated.View>
</Animated.View>
</TouchableOpacity>
</View>
)
});
}