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;
Related
I'm trying to create an auto scroll flatlist carousel that can also allow the user to scroll manually. The problem is that I get this error flatListRef.scrollToIndex is not a function. I tried searching how other people create an auto scroll flatlist but their solution is using class.
const flatListRef = useRef(null)
useEffect (() => {
const totalIndex = data.length - 1;
setInterval (() => {
if(flatListRef.current.index < totalIndex) {
flatListRef.scrollToIndex({animated: true, index: flatListRef.current.index + 1})
} else {
flatListRef.scrollToIndex({animated: true, index: 0})
}
}, 3000)
}, []);
const renderItem = ({item}) => {
return (
<View style={styles.cardView}>
<Image style={styles.image} source={item.image} resizeMode="contain"/>
</View>
)
}
return (
<View style={{paddingHorizontal: 10}} >
<FlatList
ref={flatListRef}
data={data}
keyExtractor={data => data.id}
horizontal
pagingEnabled
scrollEnabled
snapToAlignment="center"
scrollEventThrottle={16}
decelerationRate={"fast"}
showsHorizontalScrollIndicator={true}
persistentScrollbar={true}
renderItem={renderItem}
/>
</View>
);
styles:
cardView: {
flex: 1,
width: width - 20,
height: height * 0.21,
backgroundColor: Colors.empty,
alignItems: 'center',
justifyContent: 'center',
},
image: {
backgroundColor: Colors.empty,
width: width - 20,
height: height * 0.21,
},
useRef returns a mutable object whose .current property is initialized to initial value. So basically you need to access scrollIndex like this
flatListRef.current.scrollToIndex({animated: true, index: flatListRef.current.index + 1})
Edit: To answer your comment, you should've asked directly for auto-scrolling, however, The code below should work!
const flatListRef = useRef(null)
let index=0;
const totalIndex = datas.length - 1;
useEffect (() => {
setInterval (() => {
index++;
if(index < totalIndex) {
flatListRef.current.scrollToIndex({animated: true, index: index})
} else {
flatListRef.current.scrollToIndex({animated: true, index: 0})
}
}, 1000)
}, []);
as per the guide on React Native onScrollToIndexFailed
you have to override
onScroll={(e) => {
this.setState({ currentIndex: Math.floor(e.nativeEvent.contentOffset.x / (dimensions.wp(this.props.widthPercent || 100) - 1)) });
}}
onScrollToIndexFailed={info => {
const wait = new Promise(resolve => setTimeout(resolve, 500));
wait.then(() => {
flatListRef?.current?.scrollToIndex({ index: 0, animated: true });
});
}}
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 :)
I have tried to sychronise-scroll two Flatlist with state but my thought was that scrolling was shaking, lagging and not smooth due to re-rendering. But I tried with referencing React element but it didn't help. The result is the same, scrolling is like a person gotten electrified and shock, that is, shaking.
Code is below:
import * as React from 'react';
import { Text, View, StyleSheet, FlatList, Button } from 'react-native';
import Constants from 'expo-constants';
export default function App() {
const listRefOne = React.useRef();
const listRefTwo = React.useRef();
const handleRef = (listRef, offset) => {
if (listRef === listRefOne) {
listRefTwo.current.scrollToOffset({ animated: true, offset: offset });
console.log('One', offset);
}
if (listRef === listRefTwo) {
listRefOne.current.scrollToOffset({ animated: true, offset: offset });
console.log('Two', offset);
}
};
return (
<View style={styles.container}>
<MyListView listRef={listRefOne} handleRef={handleRef} />
<MyListView listRef={listRefTwo} handleRef={handleRef} />
</View>
);
}
const generateData = () => {
const temp = [];
for (var i = 1; i <= 100; i++) {
temp.push({ id: i, title: `# ${i} Hello` });
}
return temp;
};
const mydata = generateData();
const MyListView = ({ listRef, handleRef }) => {
const handleScroll = (offset) => handleRef(listRef, offset);
return (
<FlatList
ref={(list) => {
listRef.current = list;
}}
style={styles.itemView}
data={mydata}
renderItem={({ item }) => (
<Text style={{ fontSize: 20 }}>{item.title}</Text>
)}
keyExtractor={(item) => item.id}
onScroll={(e) => handleScroll(e.nativeEvent.contentOffset.y)}
/>
);
};
const styles = StyleSheet.create({
container: {
flex: 2,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
itemView: {
flexGrow: 1,
backgroundColor: '#efefef',
margin: 3,
},
});
Expo Link
So, I created a swipe using react native redash and reanimated. Here is my code:
const { width, height } = Dimensions.get("window");
const styles = StyleSheet.create({
timeContainer: {
},
text: {
fontSize: 30,
},
display: {
marginTop: height / 2,
alignItems: 'center',
},
});
const TEXT_HEIGHT = styles.text.fontSize * 1.34;
const App = () => {
const [containerHeight, setContainerHeight] = useState(height);
const SampleFunction = (item) => {
Alert.alert(item);
}
let numbers = new Array(41);
for (let i = 0, num = 0; i < numbers.length; i++, num += 5) {
numbers[i] = num;
}
const {
gestureHandler,
translation,
velocity,
state,
} = usePanGestureHandler();
const translateY = diffClamp(withDecay({
value: translation.y,
velocity: velocity.y,
state,
}),
-numbers.length * TEXT_HEIGHT + containerHeight - height,
0
);
return (
<View style={styles.display}>
<PanGestureHandler
{...gestureHandler}
onLayout={({
nativeEvent: {
layout: { height: h },
},
}) => setContainerHeight(h)}
>
<Animated.View style={styles.timeContainer}>
{numbers.map((item, index) => {
return (
<Animated.View
key={index}
style={[
{ transform: [{ translateY }] },
]}
>
<Text style={styles.text} onPress={SampleFunction.bind(item)}>{item}</Text>
</Animated.View>
);
})}
</Animated.View>
</PanGestureHandler>
</View>
)
};
export default App;
So, when I swipe down till the very end of the screen, swipe just doesn't work. That bug appeared when I added style prop 'marginTop: height / 2'. I added it cause I want my array to start at the middle of the screen. How can I fix that?
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