im trying to create a carousel effect upon scrolling using renimatedV2 and im realizing that because of the useAnimatedStyle hook dependency I cannot apply the animated style over to the view. Reason is it is a hook and I cannot place it inside the renderItem. The reason I need to place it inside the renderItem is because the interpolation depends on the index of the item. Is there a work around for this? surely the very amazing people at software mansion thought about this while creating renimatedV2 but I just cant find the solution.
const animatedScale = useSharedValue(1)
const animatedScaleStyle = useAnimatedStyle(() => {
return {
transform: [
{
scale: animatedScale.value,
},
],
}
})
const renderItem = useCallback(({ item, index }) => {
const inputRange = [-1, 0, 210 * index, 210 * (index + 0.5)]
const scale = interpolate(animatedScale.value, inputRange, [1, 1, 1, 0])
return (
<Animated.View
style={{
height: 200,
marginBottom: 10,
transform: [
{
scale: scale,
},
],
}}
>
<ThumbnailBig
ref={thumbnailRef}
images={item}
key={item.id}
oneEllipsisPressed={oneEllipsisPressed.bind(this, item.id)}
/>
</Animated.View>
)
}, [])
const onScroll = useAnimatedScrollHandler((event, context) => {
const { y } = event.contentOffset\
animatedScale.value = y
})
return (
<AnimatedFlatList
ref={bigListRef}
data={image}
renderItem={render}
keyExtractor={keyExtractor}
onScrollEndDrag={handleScroll}
initialNumToRender={5}
maxToRenderPerBatch={5}
initialScrollIndex={scrollIndex}
onScrollToIndexFailed={scrollFailed}
windowSize={4}
contentContainerStyle={{
paddingBottom: 40,
}}
alwaysBounceVertical={false}
bounces={false}
onScroll={onScroll}
scrollEventThrottle={16}
extraData={refreshFlatlist}
style={styles.flatList}
/>
)
I got the same problem and I finally managed to solve it by changing how renderItem is passed.
You need to change from
renderItem={renderitem}
to this
renderItem={({item,index}) => <Item item={item} index={index} />}
and from
const renderItem = useCallback(({ item, index }) => {
to this
const Item = useCallback(({ item, index }) => {
The reason you can't use useAnimatedStyle in renderItem is because it is not a Functional Component.
So instead of passing renderItem directly to the FlatList, you need to convert it into a Functional Component so that you can use hooks. I hope this helps :)
Related
I have a horizontal <Animated.ScrollView>, a "carousel" in my React-native app that displays one item at the center of the screen and the edges of previous and next item. I want to show data (lessons) below the ScrollView. Can someone tell me or point to a resource about how can I know what item the screen is now displaying and then showing data based on that? Do I need to calculate the current item in the scrollview or pass it as an argument to some function?
My goal:
Parent component:
return (
<View style={styles.screen}>
<View style={styles.thumbnailScrollContainer}>
<HorizontalContentScroll
data={LESSONS_DATA}
/>
</View>
<View style={styles.dataScrollContainer}>
<FlatList numColumns={2} data={lessonsByCategory} renderItem={renderLessonItem} />
</View>
</View> );
And here my horizontal Scrollview
const HorizontalContentScroll = ({ data}: HorizontalContentProps) => {
const { width, height } = Dimensions.get('window');
const scrollX = useRef(new Animated.Value(0)).current;
const ITEM_SIZE = width * 0.8;
const getInterval = (offset: any) => {
// console.log('offset', offset);
};
const scrollableData = (data as Array<ContentCategory>).map(
(item: ContentCategory, index: number) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
];
const translateY = scrollX.interpolate({
inputRange,
outputRange: [40, 10, 40],
// extrapolate: 'clamp',
});
return (
<Card
size="large"
style={{
...styles.titleCard,
transform: [{ translateY }],
width: ITEM_SIZE,
}}
key={`${item.category}-${index}`}
>
<Text>{item.category}</Text>
</Card>
);
}
);
return (
<Animated.ScrollView
contentContainerStyle={styles.contentContainer}
horizontal
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{
useNativeDriver: true,
listener: (event) => {
getInterval(event);
},
}
)}
scrollEventThrottle={16}
showsHorizontalScrollIndicator={false}
bounces={false}
pagingEnabled
snapToAlignment="center"
snapToInterval={330}
decelerationRate={'fast'}
>
{scrollableData}
</Animated.ScrollView>
);
};
export default HorizontalContentScroll;
I think I have to do something in this map function like pass the current item up to my parent component but how? If I try to call a function that sets the state in the parent I get an error of "Warning: Cannot update a component from inside the function body of a different component."
const scrollableData = (data as Array<ContentCategory>).map(
(item: ContentCategory, index: number) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
];
const translateY = scrollX.interpolate({
inputRange,
outputRange: [40, 10, 40],
});
// filterLessonsInTheParent(item)
return (
<Card
size="large"
style={{
...styles.titleCard,
transform: [{ translateY }],
width: ITEM_SIZE,
}}
key={`${item.category}-${index}`}
>
<Text>{item.category}</Text>
</Card>
);
}
Okay I solved it.
I used Animated.Flatlist instead of Animated.Scrollview so that I could get my hands on the onViewableItemsChanged prop and then I had to refactor my component to a class component so that viewabilityConfig prop would work properly.
I pass the current viewable item to the parent in a useCallback function that updates the local state. I then use that and React Pure Component to avoid re-rendering my HorizontalContentScroll which would mess up the animation positions. (I don't know if this is the most optimal way but it works so far).
// Parent
const handleViewableChange = useCallback((item: ContentCategory) => {
setContentsToShow((prevItem) => item.contents);
}, []);
return (
<View style={styles.screen}>
<View style={styles.thumbnailScrollContainer}>
<HorizontalContentScroll
data={LESSONS_DATA}
onViewableChange={handleViewableChange }
/>
</View>
<View>
<FlatList
numColumns={2}
data={contentsToShow}
renderItem={renderLessonItem}
/>
// HorizontalContentScroll
class HorizontalContentScroll extends PureComponent<HoriProps, any> {
viewabilityConfig: { viewAreaCoveragePercentThreshold: number };
scrollX: any;
constructor(props: any) {
super(props);
this.handleViewableItemsChanged = this.handleViewableItemsChanged.bind(
this
);
this.viewabilityConfig = { viewAreaCoveragePercentThreshold: 50 };
}
handleViewableItemsChanged = (info: any) => {
const currItemInView = info.viewableItems[0].item;
this.props.onViewableChange(currItemInView);
};
render() {
const { data } = this.props;
const { width, height } = Dimensions.get('window');
const scrollX = new Animated.Value(0);
const ITEM_SIZE = width * 0.8;
return (
<Animated.FlatList
data={data}
contentContainerStyle={styles.contentContainer}
horizontal
pagingEnabled
onViewableItemsChanged={this.handleViewableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
scrollEventThrottle={16}
showsHorizontalScrollIndicator={false}
bounces={false}
snapToAlignment="center"
snapToInterval={330}
decelerationRate={'fast'}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
{
useNativeDriver: true,
}
)}
renderItem={({
item,
index,
}: {
item: ContentCategory;
index: number;
}) => {
const inputRange = [
(index - 1) * ITEM_SIZE,
index * ITEM_SIZE,
(index + 1) * ITEM_SIZE,
];
const translateY = scrollX.interpolate({
inputRange,
// [x, y, x]
outputRange: [40, 10, 40],
// extrapolate: 'clamp',
});
return (
<Card
size="large"
style={{
...styles.titleCard,
transform: [{ translateY }],
width: ITEM_SIZE,
}}
key={`${item.category}-${index}`}
>
<Text style={styles.categoryText}>{item.category}</Text>
</Card>
);
}}
/>
);
}
}
export default HorizontalContentScroll;
I followed this tutorial in order to have a slider with images : https://www.youtube.com/watch?v=HAgJqaJc-ck
I have three main files :
Carousel.js
const Carousel = ({data}) => {
const scrollX = new Animated.Value(0)
let position = Animated.divide(scrollX, width)
if (data && data.length){
return (
<View>
<FlatList data = {data}
keyExtractor= {(item, index) => 'key' + index}
horizontal
pagingEnabled
scrollEnabled
snapToAlignment = 'center'
scrollEventThrottle = {16}
decelerationRate = {"fast"}
showsHorizontalScrollIndicator = {false}
renderItem = {({item}) => {
return <CarouselItem item= {item} navigation={props.navigation}/>
}}
onScroll = {Animated.event(
[{nativeEvent : {contentOffset: { x : scrollX}}}]
)}
/>
<View style = {styles.dotView}>
{data.map((_, i) => {
let opacity = position.interpolate({
inputRange : [i - 1, i, i + 1],
outputRange: [0.3, 1, 0.3],
extrapolate : 'clamp'
})
return <Animated.View
key={i}
style = {{opacity, height : 10, width : 10, background: '#595959', margin: 8 , borderRadius: 5}}
/>
})}
</View>
</View>
)
}
console.log('Please provide Images')
return null
}
CarouselItem.js
const CarouselItem = ({item, navigation}) => {
const url = item.url
return (
<View style={styles.cardView} >
<TouchableOpacity onPress={() => props.navigation.navigate("BoatInfo")}><Image style={styles.image} source={{uri: item.url}} /></TouchableOpacity>
<View style={styles.textView}>
<Text style={styles.itemTitle}>{item.title}</Text>
<Text style={styles.imageDescription}>{item.description}</Text>
</View>
</View>
)
}
And Data.js
export const dummyData = [
{
title: 'BOAT DETAILS', url : require('../component/schema.jpg'),
description: 'PDF containing the specificities of the boat', id : 1
},
{
title: 'TUTORIALS', url : require('../component/tuto.png'),
description: 'Become a capitain by watching these videos', id : 2
},
{
title: 'YOUR TRIP', url : require('../component/trip.png'),
description: 'Follow your trip details', id : 3
},
]
Now, As you see in CarouselItem, I am trying to add a functionality so that when I press on the image, it would take me to another page (each picture takes me to a different page). However, when I try to do so, I get the following error:
So, I understand that in props it is empty...
But when I switch const CarouselItem = ({ item }, props) => { to const CarouselItem = ( props, { item }) => { Then I get the following error:
What I don't understand is that when I remove {item} then the navigation works and when I remove props then {item} works... How can both work? What am I doing wrong ?
Only screens in React Native automatically have access to the navigation prop, so you'll need to pass it to the CarouselItem component, then use the always fun object destructuring to get your particular props in the component (like you did with item). It should look like this:
...
renderItem = {({item}) => {
return <CarouselItem item={item} navigation={navigation}/>
}}
...
and then:
const CarouselItem = ({ item, navigation }) => {
...
and that should work!
Good luck!
You have to pass navigation prop object from flatlist with item object too something like this
navigate ={props.navigation} item={item}
in crousal function you have to write like this
crousalItem =(item, navigation) =>
This way you got both item and navigation object.
use item to list data, navigation.navigate() to move to another screen
After getting data from API I set it to state, and render items in Flatlist,
when I select any item from it I manipulate data and add a new property to item object named as "toggle: true"
and it's works well when I select any item from list I add a border based on toggle,
But when I go back to previous screen then re open the lists screen I can see the border rendered around the items, although I reset the state when the unmounted screen
So what's the wrong I made here?
Code snippet
Data
export default {
...
services: [
{
id: 0,
name: 'nameS0',
logo:
'https://cdn2.iconfinder.com/data/icons/hotel-98/64/hair-dryer-tools-beauty-hairdressing-512.png',
price: 19.99,
},
],
employees: [
{
id: 0,
name: 'name0',
img:
'https://www.visualelementmedia.com/wp-content/uploads/2015/04/person-4-400x629.jpg',
},
...
],
};
const VendorProfile = ({navigation}) => {
const [services, setServices] = React.useState(null);
const [employees, setEmployees] = React.useState(null);
const [serviceSelected, setServiceSelected] = React.useState(null);
const [employeeSelected, setEmployeeSelected] = React.useState(null);
// For selected Item (services, employees)
const itemSelected = (data, id) => {
const updated = data.map((item) => {
item.toggle = false;
if (item.id === id) {
item.toggle = true;
data === services
? setServiceSelected(item)
: setEmployeeSelected(item);
}
return item;
});
data === services ? setServices(updated) : setEmployees(updated);
};
...
const renderEmployees = ({item}) => {
return (
<TouchableOpacity
onPress={() => itemSelected(employees, item.id)}
delayPressIn={0}
style={styles.employeeContainer}>
<EmployeePattern style={{alignSelf: 'center'}} />
<View style={styles.employeeLogo}>
<Image
source={{uri: item.img}}
style={[styles.imgStyle, {borderRadius: 25}]}
/>
</View>
<View style={{marginTop: 30}}>
<Text style={{textAlign: 'center'}}> {item.name}</Text>
</View>
<View style={{marginTop: 10, alignSelf: 'center'}}>
{item.toggle && <AntDesign name="check" size={25} color="#000" />} // here it's stuck after back and reopen the screen
</View>
</TouchableOpacity>
);
};
React.useEffect(() => {
setServices(VendorProfileData.services);
setEmployees(VendorProfileData.employees);
() => {
setServices(null);
setEmployees(null);
};
}, []);
return (
<View style={styles.container}>
<FlatList
data={services}
renderItem={renderServices}
horizontal
keyExtractor={(item) => item.id.toString()}
contentContainerStyle={{
justifyContent: 'space-between',
flexGrow: 1,
}}
/>
.....
</View>
);
};
Ok so after trying multiple times, i got it
change this
const updated = data.map((item) => {
to this
const updated = data.map((old) => {
let item = {...old};
and please make sure everything is working and we didn't break a thing :),
On your ItemSelected function you are passing the whole employees list, and going through it now thats fine, but when you changing one item inside this list without "recreating it" the reference to that item is still the same "because its an object" meaning that we are modifying the original item, and since we are doing so, the item keeps its old reference, best way to avoid that is to recreate the object,
hope this gives you an idea.
I'm having an issue with the FlatList component when calling scrollToIndex. Here is how I'm referencing the list.
<Modal visible={modal} transparent onRequestClose={this.onClose}>
<TouchableWithoutFeedback onPress={this.onClose}>
<View style={overlayStyle}>
<Animated.View style={[styles.picker, pickerStyle]}>
<FlatList
ref={(node) => { this.scroll = node; }}
style={styles.scroll}
data={data}
renderItem={this.renderItems}
getItemLayout={(_, index) => (
{ length: 24, offset: 24 * index, index }
)}
initialNumToRender={5}
// scrollEnabled={visibleItemCount < itemCount}
contentContainerStyle={styles.scrollContainer}
keyExtractor={(item) => item.value}
automaticallyAdjustContentInsets={false}
removeClippedSubviews={false}
indicatorStyle="white"
/>
</Animated.View>
</View>
</TouchableWithoutFeedback>
</Modal>
I included the surrounding code in case that would help. But for some reason I get back a different structure than normal with an error that says _scrollRef.scrollToIndex is undefined. When I check the tree and the ref component I seem to get back the right structure of _listRef -> _scrollRef but the contents are of that of a view ref or a textinput ref. Looking something like...
blur
context
focus
measure
measureInWindow
measureLayout
props
refs
setNativeProps
state
updater
I'm extremely confused on this situation and some insight as to why would be great.
EDIT:
Here is the code for the scrollToIndex. The promise method was recommended by someone in the RN community. I was using just a set timeout previously.
const wait = new Promise((resolve) => setTimeout(resolve, 500));
wait.then(() => {
if (this.mounted) {
console.log(this.scroll);
if (this.scroll) {
this.scroll
.scrollToIndex({ index: 0, animated: false });
}
Animated
.timing(opacity, {
duration: animationDuration,
toValue: 1,
})
.start(() => {
// if (this.mounted && Platform.OS === 'ios') {
// this.scroll.flashScrollIndicators();
// }
});
}
});
I'm trying to center an item within a horizontal listView when selected. My current strategy is to first measure the item and scroll to the x coordinates of the referenced item within the view.
Currently, any time I press an item ListView scrolls to the very end x: 538
Is there an easier way to implement this while keeping the code stateless / functional?
const ItemScroll = (props) => {
const createItem = (obj, rowID) => {
const isCurrentlySelected = obj.id === props.currentSelectedID
function scrollToItem(_scrollViewItem, _scrollView) {
// measures the item coordinates
_scrollViewItem.measure((fx) => {
console.log('measured fx: ', fx)
const itemFX = fx;
// scrolls to coordinates
return _scrollView.scrollTo({ x: itemFX });
});
}
return (
<TouchableHighlight
ref={(scrollViewItem) => { _scrollViewItem = scrollViewItem; }}
isCurrentlySelected={isCurrentlySelected}
style={isCurrentlySelected ? styles.selectedItemContainerStyle : styles.itemContainerStyle}
key={rowID}
onPress={() => { scrollToItem( _scrollViewItem, _scrollView); props.onEventFilterPress(obj.id, rowID) }}>
<Text style={isCurrentlySelected ? styles.selectedItemStyle : styles.itemStyle} >
{obj.title}
</Text>
</TouchableHighlight>
)
};
return (
<View>
<ScrollView
ref={(scrollView) => { _scrollView = scrollView; }}
horizontal>
{props.itemList.map(createItem)}
{props.onItemPress}
</ScrollView>
</View>
);
};
Update
With #Ludovic suggestions I have now switch to FlatList, I am not sure how trigger scrollToIndex with a functional component. Below is my new ItemScroll
const ItemScroll = (props) => {
const {
itemList,
currentSelectedItem
onItemPress } = props
const renderItem = ({item, data}) => {
const isCurrentlySelected = item.id === currentSelectedItem
const _scrollToIndex = () => { return { viewPosition: 0.5, index: data.indexOf({item}) } }
return (
<TouchableHighlight
// Below is where i need to run onItemPress in the parent
// and scrollToIndex in this child.
onPress={[() => onItemFilterPress(item.id), scrollToIndex(_scrollToIndex)]} >
<Text style={isCurrentlySelected ? { color: 'red' } : { color: 'blue' }} >
{item.title}
</Text>
</TouchableHighlight>
)
}
return (
<FlatList
showsHorizontalScrollIndicator={false}
data={itemList}
keyExtractor={(item) => item.id}
getItemLayout={(data, index) => (
// Max 5 items visibles at once
{ length: Dimensions.get('window').width / 5, offset: Dimensions.get('window').width / 5 * index, index }
)}
horizontal
// Here is the magic : snap to the center of an item
snapToAlignment={'center'}
// Defines here the interval between to item (basically the width of an item with margins)
snapToInterval={Dimensions.get('window').width / 5}
renderItem={({item, data}) => renderItem({item, data})} />
);
};
In my opinion, you should use FlatList
FlatList have a method scrollToIndex that allows to directly go to an item of your datas. It's almost the same as a ScrollView but smarter. Sadly the documentation is very poor.
Here is an example of a FlatList I did
let datas = [{key: 0, text: "Hello"}, key: 1, text: "World"}]
<FlatList
// Do something when animation ended
onMomentumScrollEnd={(e) => this.onScrollEnd(e)}
ref="flatlist"
showsHorizontalScrollIndicator={false}
data={this.state.datas}
keyExtractor={(item) => item.key}
getItemLayout={(data, index) => (
// Max 5 items visibles at once
{length: Dimensions.get('window').width / 5, offset: Dimensions.get('window').width / 5 * index, index}
)}
horizontal={true}
// Here is the magic : snap to the center of an item
snapToAlignment={'center'}
// Defines here the interval between to item (basically the width of an item with margins)
snapToInterval={Dimensions.get('window').width / 5}
style={styles.scroll}
renderItem={ ({item}) =>
<TouchableOpacity
onPress={() => this.scrollToIndex(/* scroll to that item */)}
style={styles.cell}>
<Text>{item.text}</Text>
</TouchableOpacity>
}
/>
More about FlatList : https://facebook.github.io/react-native/docs/flatlist#__docusaurus
viewPosition prop will center your item.
flatListRef.scrollToIndex({ index: index, animated: true, viewPosition: 0.5 })
flatlistRef.current.scrollToIndex({
animated: true,
index,
viewOffset: Dimensions.get('window').width / 2.5,
});
You can try to put viewOffset inside the scrollToIndex property, it will handle your component offset, I use 2.5 to make it in middle of screen , because I show 5 item each section and one of focused item will be in middle of screen