I have created an image carousel wherein as you scroll, image gets auto selected. The selected image should expand and push the images on left and right aside.
Consumer code
import { View, Animated } from 'react-native';
import React, { useEffect, useState } from 'react';
import { StyleSheet } from 'react-native';
import { AutoSelectScrollList } from './AutoSelectScrollList';
import { IListData } from './AutoSelectScrollList/types';
export default function AutoSelectScrollView() {
const collectionStyles = (theme: Theme) =>
StyleSheet.create({
singleCollections: {
width: 80,
marginVertical: 20,
},
collectionImage: {
width: '100%',
borderRadius: 12,
},
collectionText: {
marginTop: 8,
fontSize: 12,
fontWeight: '400',
color: 'grey,
textAlign: 'center',
},
contentContainer: {
paddingHorizontal: 16,
},
colorSelected: {
color: 'red',
},
imageSelected: {
width: 80,
},
imageSelected2: {
width: 116,
},
});
const testData = [
{
image: require('../../../common/assets/images/1.png'),
title: 'Friendly Classic0',
type: '',
id: 0,
},
{
image: require('../../../common/assets/images/2.png'),
title: 'Southeast asia1',
type: '',
id: 1,
},
{
image: require('../../../common/assets/images/3.png'),
title: 'Camping Adventure2',
type: '',
id: 2,
},
{
image: require('../../../common/assets/images/4.png'),
title: 'Off the grid3',
type: '',
id: 3,
},
{
image: require('../../../common/assets/images/1.png'),
title: 'Camping Adventure4',
type: '',
id: 4,
},
{
image: require('../../../common/assets/images/2.png'),
title: 'Off the grid5',
type: '',
id: 5,
},
{
image: require('../../../common/assets/images/3.png'),
title: 'Camping Adventure6',
type: '',
id: 6,
},
{
image: require('../../../common/assets/images/4.png'),
title: 'Off the grid7',
type: '',
id: 7,
},
];
const [{ style: themedStyle }] = useTheme(collectionStyles);
const [selectedId, setSelectedId] = useState(4);
const animatedValue = new Animated.Value(0);
useEffect(() => {
Animated.timing(animatedValue, {
toValue: 1, // The final value
duration: 1000, // Animation duration in milliseconds
delay: 500, // Delay the animation start by 500 milliseconds
useNativeDriver: false, // Set this to true whenever possible for better performance
}).start();
}, [selectedId]);
const translateXToRight = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 10], // Adjust as needed
});
const translateXToLeft = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, -10], // Adjust as needed
});
const scaleX = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [1, 1.45], // Adjust as needed
});
const width = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [80, 116], // Adjust as needed
});
const renderFn = (
item: IListData,
enableAutoSelectMode: boolean,
selectedIndex: number,
) => {
setSelectedId(selectedIndex);
return (
<Animated.View
style={[
themedStyle.singleCollections,
selectedIndex < item.id &&
!enableAutoSelectMode && {
transform: [{ translateX: translateXToRight }],
},
selectedIndex > item.id &&
!enableAutoSelectMode && {
transform: [{ translateX: translateXToLeft }],
},
]}
>
<View
style={[
{
alignItems: 'center',
justifyContent: 'center',
},
]}
>
<Animated.Image
source={item.image}
style={[
themedStyle.collectionImage,
selectedIndex === item.id &&
!enableAutoSelectMode && {
transform: [{ scaleX }],
},
]}
/>
<Animated.Text
numberOfLines={2}
style={[
themedStyle.collectionText,
selectedIndex === item.id &&
themedStyle.colorSelected,
selectedIndex === item.id &&
!enableAutoSelectMode && { width },
]}
>
{item.title}
</Animated.Text>
</View>
</Animated.View>
);
};
return (
<AutoSelectScrollList
initialSelectedIndex={4}
listData={testData}
renderListData={renderFn}
onPress={item => {
console.log(`Pressed ${item}`);
}}
peekOffset={60}
contentContainerStyle={themedStyle.contentContainer}
/>
);
}
Generic component to create the auto select carousel
import React from 'react';
import {
FlatList,
NativeSyntheticEvent,
NativeScrollEvent,
LayoutChangeEvent,
View,
Pressable
} from 'react-native';
import {
IAutoSelectScrollList,
IListData,
IState,
ITabMeasurements,
ITabsLayoutRectangle,
} from './types';
const SCREEN_WIDTH = 400
export class AutoSelectScrollList extends React.Component<
IAutoSelectScrollList,
IState
> {
state: IState = {
enableAutoSelectMode: false,
startViewPort: 0,
endViewPort: SCREEN_WIDTH,
selectedIndex: 0,
isScrollOngoing: false,
};
static defaultProps = {
initialSelectedIndex: 0,
autoScrollDuration: 300,
peekOffset: 10,
};
private flatListRef: React.RefObject<FlatList> = React.createRef();
private _tabsMeasurements: ITabsLayoutRectangle = {};
componentDidMount() {
const { initialSelectedIndex, listData, autoScrollDuration } =
this.props;
if (
initialSelectedIndex !== undefined &&
initialSelectedIndex >= 0 &&
initialSelectedIndex < listData.length
) {
this.setState(prevState => ({
...prevState,
selectedIndex: initialSelectedIndex,
}));
setTimeout(() => {
this.flatListRef.current?.scrollToIndex({
animated: true,
index: initialSelectedIndex,
});
}, autoScrollDuration);
}
}
onTabLayout = (key: number) => (ev: LayoutChangeEvent) => {
const { x, width, height } = ev.nativeEvent.layout;
this._tabsMeasurements[key] = {
left: x,
right: x + width,
width,
height,
};
};
getScrollAmount = (position: number, measurements: ITabMeasurements) => {
const { listData, peekOffset } = this.props;
const { startViewPort, endViewPort } = this.state;
const lastIndexSupported = listData.length - 1;
if (position >= lastIndexSupported) {
return SCREEN_WIDTH;
}
const tabWidth = measurements.width;
const tabOffset = measurements.left;
const newScrollX = tabOffset + tabWidth;
const diff = SCREEN_WIDTH - newScrollX;
if (startViewPort <= tabOffset && newScrollX <= endViewPort) {
return null;
}
if (diff < 0) {
return -diff + peekOffset;
} else {
return tabOffset - peekOffset;
}
};
scrollToOffset = (offset: number) => {
const { autoScrollDuration } = this.props;
if (this.flatListRef.current) {
setTimeout(() => {
this.flatListRef.current?.scrollToOffset({
animated: true,
offset,
});
}, autoScrollDuration);
}
};
onPillPress = (item: IListData) => {
const getScrollXPos = this.getScrollAmount(
item.id,
this._tabsMeasurements[item.id],
);
getScrollXPos && this.scrollToOffset(getScrollXPos);
this.setState(prevState => ({
...prevState,
selectedIndex: item.id,
}));
this.props.onPress && this.props.onPress(item);
};
renderData = (item: IListData, id: number) => {
const { enableAutoSelectMode, selectedIndex } = this.state;
const { renderListData } = this.props;
return (
<View
onLayout={this.onTabLayout(id)}
key={id}
style={{ marginHorizontal: 8 }}
>
<Pressable onPress={() => this.onPillPress(item)}>
{renderListData(item, enableAutoSelectMode, selectedIndex)}
</Pressable>
</View>
);
};
handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { enableAutoSelectMode, startViewPort, selectedIndex } =
this.state;
const { listData } = this.props;
const { contentOffset, contentSize } = event.nativeEvent;
const contentOffsetX = contentOffset.x;
const avgPerItemLength = SCREEN_WIDTH / listData.length;
const isRightScroll = contentOffsetX > startViewPort ? true : false;
this.setState(prevState => ({
...prevState,
isScrollOngoing: true,
}));
if (enableAutoSelectMode) {
if (contentOffsetX < 10) {
this.setState(prevState => ({
...prevState,
selectedIndex: 0,
startViewPort: contentOffsetX,
endViewPort: contentOffsetX + SCREEN_WIDTH,
}));
} else if (contentOffsetX > contentSize.width - 10) {
this.setState(prevState => ({
...prevState,
selectedIndex: listData.length - 1,
startViewPort: contentOffsetX,
endViewPort: contentOffsetX + SCREEN_WIDTH,
}));
} else {
let newIndex = Math.floor(contentOffsetX / avgPerItemLength);
if (newIndex >= listData.length) {
newIndex = listData.length - 1;
} else if (newIndex < 0) {
newIndex = 0;
}
if (isRightScroll && newIndex > selectedIndex) {
this.setState(prevState => ({
...prevState,
selectedIndex: newIndex,
startViewPort: contentOffsetX,
endViewPort: contentOffsetX + SCREEN_WIDTH,
}));
} else if (!isRightScroll && newIndex < selectedIndex) {
this.setState(prevState => ({
...prevState,
selectedIndex: newIndex,
startViewPort: contentOffsetX,
endViewPort: contentOffsetX + SCREEN_WIDTH,
}));
}
}
}
this.setState(prevState => ({
...prevState,
startViewPort: contentOffsetX,
endViewPort: contentOffsetX + SCREEN_WIDTH,
}));
};
handleScrollBegin = () => {
this.setState(prevState => ({
...prevState,
enableAutoSelectMode: true,
}));
};
handleOnMomentumScrollEnd = () => {
/**
* There is a bug which calls onMomentumScrollEnd 3 times.
* Using isScrollOngoing flag for this
* https://github.com/facebook/react-native/pull/32433
*/
console.log('hit');
this.state.isScrollOngoing &&
this.onPillPress(this.props.listData[this.state.selectedIndex]);
this.setState(prevState => ({
...prevState,
enableAutoSelectMode: false,
isScrollOngoing: false,
}));
};
render() {
const {
listData,
contentContainerStyle = null,
itemSeparatorView = null,
} = this.props;
return (
/**
* We need to use CellRendererComponent instead of renderItem
* renderItem wraps each data inside a view.
* Due to this we will not be able to dimensions of each item in list
*/
<FlatList
ref={this.flatListRef}
data={listData}
contentContainerStyle={contentContainerStyle}
ItemSeparatorComponent={() => itemSeparatorView}
alwaysBounceHorizontal={false}
keyExtractor={item => item.id.toString()}
CellRendererComponent={({ item }) =>
this.renderData(item, item.id)
}
maintainVisibleContentPosition={{
minIndexForVisible: this.state.selectedIndex,
}}
horizontal
bounces={true}
onScroll={this.handleScroll}
onScrollBeginDrag={this.handleScrollBegin}
onMomentumScrollEnd={this.handleOnMomentumScrollEnd}
getItemLayout={(data, index) => ({
length: 200,
offset: 200,
index,
})}
/>
);
}
}
The code works perfectly in android but in ios there are constant flickers! On scroll the image is getting higlighted correctly, but all images are getting re-rendered. This is happening only in iOS and not in Android which is causing the flicker view