Show different data based on the active carousel item - javascript

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

How to create flatlist auto scroll with Hooks in React Native

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 });
});
}}

How to use useAnimatedStyle inside a FlatList renderItem?

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 :)

Smooth scrollToOffset in FlatList React Native

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

How can I fix a bug when swipe stops to work when I swipe down till the end of the screen?

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?

How do I center an item within a React Native ListView?

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

Categories