Related
I'm trying to make animation in flatlist, something like this, it will have spring animation when moving item
Something like apple music you can see: https://streamable.com/yg1j2j
With the help of #David Scholz, I was able to make exactly the same, here is expo for that:
https://snack.expo.dev/#quockhanh210199/animated-list-selection
But problem is, when i enable scroll it will have weird animation, so my question is, how to make the animation behavior correct if i enable scroll, you can see the weird animation if you enable scroll and and more item, or use below code:
import React, {useState, useCallback} from 'react';
import { StyleSheet, Text,View, SafeAreaView, ScrollView, StatusBar, Animated,FlatList,TouchableOpacity } from 'react-native';
const App = () => {
const data = [
{
id: "1",
text: "Results",
},
{
id: "2",
text: "Products",
},
{
id: "3",
text: "Stores",
},
{
id: "4",
text: "Stores",
},
{
id: "5",
text: "Stores",
},
{
id: "6",
text: "Stores",
},
]
const [translateValue] = useState(new Animated.Value(0))
const [selected, setSelected] = useState(0)
const onPress = React.useCallback(
(index) => {
setSelected(index)
Animated.spring(translateValue, {
toValue: index * 100,
velocity: 5,
useNativeDriver: true,
}).start()
},
[translateValue, setSelected]
)
return (
<View style={{ flexDirection: "row", marginTop: 100 }}>
<Animated.View
style={[
{
position: "absolute",
backgroundColor: "black",
top: -5,
right: 0,
bottom: 0,
left: 15,
width: 70,
height: 30,
borderRadius: 12,
transform: [{ translateX: translateValue }],
},
]}
/>
<FlatList
data={data}
horizontal={true}
scrollEnabled={true}
renderItem={({ item, index }) => {
return (
<TouchableOpacity onPress={() => onPress(index)}>
<View style={{ width: 100, borderRadius: 10 }}>
<Text style={[{ textAlign: "center" }, index === selected ? { color: "white" } : { color: "black" }]}>
{item.text}
</Text>
</View>
</TouchableOpacity>
)
}}
keyExtractor={(item) => item.id}
/>
</View>
)
}
export default App
Please help, thank you so much
I am a bit new to react native and I am having difficulty updating object Array State and screen style on button item click during run time in react native
I tried using useState but the style does not change at run time on the touchable item click
Please advise me on the right way to change the screens view style on item click
see my code below
import React, {useState} from 'react';
import {ScrollView, FlatList, View, Text, TouchableOpacity} from 'react-native';
import styles from './styles';
export default function Loans({}) {
const [selectedDurationId, setSelectedDurationId] = useState(1);
const [selectedAmountId, setSelectedAmountId] = useState(1);
const changeSelectedDuration = function (id) {
console.log('before selected Duration');
console.log(id);
//change all the selecteds to no
durationArray.forEach(function (arrayItem) {
var x = (arrayItem.selected = 'no');
console.log(x);
setSelectedDurationId(id);
updateDataArray(durationArray, selectedDurationId);
console.log('after selected Duration');
});
};
const changeSelectedAmount = function (id) {
console.log('before selected Amount');
console.log(id);
//change all the selecteds to no
loanAmountArray.forEach(function (arrayItem) {
var x = (arrayItem.selected = 'no');
console.log(x);
});
setSelectedAmountId(id);
updateDataArray(loanAmountArray, selectedAmountId);
console.log('after selected Amount');
};
const updateDataArray = function (array, id) {
array.forEach(function () {
//change selected to yes where id == id
if (array.id === id) {
var x = (array.selected = 'yes');
console.log(x);
}
});
};
const loanAmountArray = [
{
id: 1,
amount: '5,000',
selected: 'yes',
},
{
id: 2,
amount: '10,000',
selected: 'no',
},
{
id: 3,
amount: '20,000',
selected: 'no',
},
];
const durationArray = [
{
id: 1,
days: '30 days',
rate: '3.3% Interest',
selected: 'yes',
},
{
id: 2,
days: '60 days',
rate: '5% Interest',
selected: 'no',
},
{
id: 3,
days: '90 days',
rate: '7% Interest',
selected: 'no',
},
];
return (
<View style={{flex: 1}}>
<ScrollView>
<View style={styles.contain}>
<Text>Chose Loan Amount</Text>
<FlatList
numColumns={6}
data={loanAmountArray}
keyExtractor={(item, index) => {
console.log('index', index);
return index.toString();
}}
renderItem={({item}) => {
console.log('item', item);
return (
<View>
<TouchableOpacity
onPress={() => {
changeSelectedAmount(item.id);
}}>
<Text
style={
item.selected === 'yes'
? styles.textBoxSelected
: styles.textBox
}>
{item.amount}
</Text>
</TouchableOpacity>
</View>
);
}}
/>
<Text>Chose Payment Duration</Text>
<FlatList
numColumns={3}
data={durationArray}
keyExtractor={(item, index) => {
console.log('index', index);
return index.toString();
}}
renderItem={({item}) => {
console.log('item', item);
return (
<View>
<TouchableOpacity
style={
item.selected === 'yes'
? styles.durationViewPressed
: styles.durationView
}
onPress={() => {
changeSelectedDuration(item.id);
}}>
<View>
<Text style={styles.interest}>{item.rate}</Text>
</View>
<View>
<Text style={styles.days}>{item.days}</Text>
</View>
</TouchableOpacity>
</View>
);
}}
/>
</View>
</ScrollView>
</View>
);
}
here's the style below
import {StyleSheet} from 'react-native';
export default StyleSheet.create({
textBox: {
marginTop: 13,
marginBottom: 30,
paddingTop: 10,
paddingLeft: 16,
paddingRight: 6,
fontSize: 18,
borderColor: '#1a2856',
borderWidth: 5,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20,
},
textBoxSelected: {
marginTop: 13,
marginBottom: 30,
paddingTop: 10,
paddingLeft: 16,
paddingRight: 6,
fontSize: 18,
backgroundColor: '#1a2856',
color: '#fff',
borderWidth: 5,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20,
},
durationView: {
marginTop: 10,
marginBottom: 20,
paddingLeft: 5,
paddingRight: 5,
},
durationViewPressed: {
marginTop: 10,
marginBottom: 20,
paddingLeft: 5,
paddingRight: 5,
borderColor: '#1a2856',
borderWidth: 5,
},
interest: {
color: '#fff',
backgroundColor: '#1a2856',
paddingLeft: 5,
paddingRight: 5,
},
days: {
borderColor: '#1a2856',
borderWidth: 5,
paddingTop: 10,
paddingLeft: 16,
paddingRight: 6,
fontSize: 18,
},
});
Issues
Both loanAmountArray and durationArray are defined in the function body, so they are actually redeclared each render cycle, so any mutations you thought you did in the previous render cycle are wiped out. When this happens the style attribute is never able to match anything different each render.
Solution
Since it seems you don't really update the elements of the array you can move the arrays out of the component. They can remain const and won't be redeclared each render.
Don't bother trying to update the selected property of each element in the array, you can easily derive the selected state from the selectedDurationId and selectedAmountId state values and the current item.id when rendering.
Use the extraData prop to indicate the list should rerender.
Code:
const loanAmountArray = [
{
id: 1,
amount: "5,000",
selected: "yes"
},
{
id: 2,
amount: "10,000",
selected: "no"
},
{
id: 3,
amount: "20,000",
selected: "no"
}
];
const durationArray = [
{
id: 1,
days: "30 days",
rate: "3.3% Interest",
selected: "yes"
},
{
id: 2,
days: "60 days",
rate: "5% Interest",
selected: "no"
},
{
id: 3,
days: "90 days",
rate: "7% Interest",
selected: "no"
}
];
export default function Loans({}) {
const [selectedDurationId, setSelectedDurationId] = useState(1);
const [selectedAmountId, setSelectedAmountId] = useState(1);
const changeSelectedDuration = function (id) {
setSelectedDurationId(id);
};
const changeSelectedAmount = function (id) {
setSelectedAmountId(id);
};
return (
<View style={{ flex: 1 }}>
<ScrollView>
<View style={styles.contain}>
<Text>Chose Loan Amount</Text>
<FlatList
numColumns={6}
data={loanAmountArray}
extraData={selectedAmountId} // <-- prop used to rerender
keyExtractor={(item, index) => {
return index.toString();
}}
renderItem={({ item }) => {
return (
<View>
<TouchableOpacity
onPress={() => {
changeSelectedAmount(item.id);
}}
>
<Text
style={
item.id === selectedAmountId // <-- match id property
? styles.textBoxSelected
: styles.textBox
}
>
{item.amount}
</Text>
</TouchableOpacity>
</View>
);
}}
/>
<Text>Chose Payment Duration</Text>
<FlatList
numColumns={3}
data={durationArray}
extraData={selectedDurationId} // <-- prop used to rerender
keyExtractor={(item, index) => {
return index.toString();
}}
renderItem={({ item }) => {
return (
<View>
<TouchableOpacity
style={
item.id === selectedDurationId // <-- match id property
? styles.durationViewPressed
: styles.durationView
}
onPress={() => {
changeSelectedDuration(item.id);
}}
>
<View>
<Text style={styles.interest}>{item.rate}</Text>
</View>
<View>
<Text style={styles.days}>{item.days}</Text>
</View>
</TouchableOpacity>
</View>
);
}}
/>
</View>
</ScrollView>
</View>
);
}
Expo Snack Demo
when I run my application it's okay and work If I create an array and put it in the data in FlatList like this array
const photos = [
{ id: 1, title: "Photo 1" },
{ id: 2, title: "Photo 2" },
{ id: 3, title: "Photo 3" },
{ id: 4, title: "Photo 4" },
{ id: 5, title: "Photo 5" },
{ id: 6, title: "Photo 6" },
];
But when I replace the photos array with an API, The app doesn't work. I tried more than API, I think the error is in my code not in the API,
This error appears to me " scrollToIndex out of range: request index1 but maximum is -1 "
What's wrong with my code?
import React, { useState, useRef, useEffect } from "react";
import {
StyleSheet,
View,
FlatList,
Dimensions,
Text,
TouchableOpacity,
} from "react-native";
import { AntDesign } from "#expo/vector-icons";
import axios from "axios";
const phoneWidth = Dimensions.get("screen").width;
const phoneHeight = Dimensions.get("screen").height;
function ScrollScreen() {
const [index, setIndex] = useState(0);
const [border, setBorder] = useState(0);
const refContainer = useRef();
const refBox = useRef();
const [data, setData] = useState([]);
useEffect(() => {
photos();
}, []);
function photos() {
axios
.get("https://jsonplaceholder.typicode.com/photos")
.then(async function (response) {
setData(response.data);
})
.catch((err) => console.error(err));
}
useEffect(() => {
refContainer.current.scrollToIndex({ animated: true, index });
}, [index]);
useEffect(() => {
refBox.current.scrollToIndex({ animated: true, index });
}, [index]);
const theNext = () => {
if (index < photos.length - 1) {
setIndex(index + 1);
setBorder(index + 1);
}
};
const thePrevious = () => {
if (index > 0) {
setIndex(index - 1);
setBorder(index - 1);
}
};
return (
<View style={styles.con}>
<AntDesign
style={[styles.iconConPosition, { left: phoneWidth * 0.05 }]}
onPress={thePrevious}
size={55}
color="#0dddcb"
name="caretleft"
/>
<AntDesign
style={[styles.iconConPosition, { right: phoneWidth * 0.05 }]}
onPress={theNext}
size={55}
color="#0dddcb"
name="caretright"
/>
<FlatList
scrollEnabled={false}
ref={refContainer}
data={data}
// data={photos}
keyExtractor={(item, index) => item.id.toString()}
style={styles.flatList}
renderItem={({ item, index }) => (
<View
style={{
height: 150,
width: phoneWidth * 0.7,
margin: 50,
backgroundColor: "red",
alignSelf: "center",
justifyContent: "center",
alignItems: "center",
}}
>
<Text>{item.id}</Text>
<Text>{item.title}</Text>
</View>
)}
horizontal
pagingEnabled //تفعيل خاصية التمرير
showsHorizontalScrollIndicator={false}
/>
<FlatList
ref={refBox}
data={data}
// data={photos}
keyExtractor={(item, index) => item.id.toString()}
style={styles.flatList}
renderItem={({ item, index }) => (
<TouchableOpacity
onPress={() => {
setIndex(index);
setBorder(index);
}}
style={
border === index
? {
height: 100,
width: phoneWidth * 0.4,
margin: 7,
backgroundColor: "gray",
alignSelf: "center",
justifyContent: "center",
alignItems: "center",
borderWidth: 2,
borderColor: "blue",
}
: {
height: 100,
width: phoneWidth * 0.4,
margin: 7,
backgroundColor: "gray",
alignSelf: "center",
justifyContent: "center",
alignItems: "center",
}
}
>
<Text>{item.id}</Text>
<Text>{item.title}</Text>
</TouchableOpacity>
)}
horizontal
/>
<Text>{index}</Text>
</View>
);
}
export default ScrollScreen;
Initially data is an empty array, but index is set to 0. On the first invocation of the useEffect that tries to scroll, there is an error because scrollToIndex(0) is an error when data is empty, since there is no item for index 0.
Try initializing the border and index state to -1 instead of 0 like:
const [index, setIndex] = useState(-1);
const [border, setBorder] = useState(-1);
On a separate but related note the theNext function has an error, it should be checking data.length instead of photos.length.
Ran into the same issue just now. Using ScrollToOffSet({offset: number, animated: boolean}) instead of scrollToIndex solved the issue for me
I am using react-native-tab-view, and trying to change the indicator width. I would like indicator width to be the same with the tab text. But What I did is just the default. I have tried in many ways, but always it gave me the wrong result. The tab bar should be scrollable horizontally as well. Could you check which part I should change?
This is the expected result :
ShowAllIndex Code :
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.dark,
},
});
const ShowAllIndex = () => {
const { seller } = useSelector((s) => s.auth, shallowEqual);
const [routes] = useState([
{ key: 'best', title: 'BEST' },
{ key: 'jacket', title: '아우터' },
{ key: 'pants', title: '바지' },
{ key: 'skirts', title: '스커트' },
{ key: 'topClothe', title: '원피스' },
{ key: 'one', title: '바지' },
{ key: 'two', title: '스커트' },
{ key: 'three', title: '상의' },
]);
const renderScene = SceneMap({
best: ShowAllMainRoutes,
jacket: JacketRoutes,
pants: PantsRoutes,
skirts: SkirtsRoutes,
topClothe: TopClotheRoutes,
one: ShowAllMainRoutes,
two: JacketRoutes,
three: PantsRoutes,
});
return (
<ScrollView style={[styles.container, { marginTop: Global() }]}>
<CustomTabView
routes={routes}
renderScene={renderScene}
scrollEnabled={true}
tabStyle={{ width: 'auto' }}
showAll={true}
/>
</ScrollView>
);
};
export default ShowAllIndex;
CustomTabView code :
const initialLayout = { width: Dimensions.get('window').width };
const CustomTabView = ({
routes,
renderScene,
numberOfTabs,
indicatorWidth,
scrollEnabled = false,
tabStyle,
showAll,
indicatorStyle,
}) => {
const [index, setIndex] = useState(0);
const renderTabBar = (props) => (
<TabBar
{...props}
scrollEnabled={scrollEnabled}
indicatorStyle={[
indicatorStyle,
{
backgroundColor: colors.barbie_pink,
height: 2.5,
bottom: -1,
},
]}
style={[styles.tabBar]}
renderLabel={({ route, focused }) => {
return (
<Text
style={[styles.label, focused ? styles.activeLabel : styles.label]}
>
{route.title}
</Text>
);
}}
tabStyle={tabStyle}
/>
);
return (
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
renderTabBar={renderTabBar}
onIndexChange={setIndex}
initialLayout={initialLayout}
style={[styles.container]}
/>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: colors.dark,
},
scene: {
flex: 1,
marginTop: 5,
},
tabBar: {
backgroundColor: 'transparent',
shadowOpacity: 0,
elevation: 0,
borderBottomWidth: 0.5,
borderColor: colors.very_light_pink_two,
marginBottom: 5,
},
label: {
color: colors.very_light_pink_four,
fontSize: 14,
lineHeight: 20.8,
fontFamily: 'NotoSansCJKkr-Regular',
letterSpacing: -0.35,
},
activeLabel: {
color: colors.barbie_pink,
},
});
Thank you for answers!
Check this solution:
const TAB_MARGIN = 24;
<TabBar
...
scrollEnabled
renderIndicator={indicatorProps => {
const width = indicatorProps.getTabWidth(this.state.index) - TAB_MARGIN
return <TabBarIndicator {...indicatorProps} width={width} />
}}
indicatorStyle={{
backgroundColor: '#333',
height: 4,
left: TAB_MARGIN / 2,
}}
...
/>
I think the prop indicatorStyle in TabBar that can resolves you problem.
You can do the following:
<TabBar
scrollEnabled //add this line to make it scrollable
tabStyle={{width: 100}} //and this one to change the tab width
/>
I am using a flatlist to expose all the list of ingredients I have saved on my database, but once I update the state, it is not reflecting on the flatlist component.
The flatlist component
<FlatList
horizontal
bounces={false}
key={ingredientList.id}
data={ingredientList}
renderItem={({ item }) => (
<TouchableOpacity onPress={() => selectedIngredient(item)}>
<Card key={item.id}>
<Text key={item.title} style={styles.titleText}>{item.name}</Text>
<Text key={item.title} style={styles.titleText}>{item.isSelected?'selected':'not selected'}
</Text>
<ImageBackground key={item.illustration} source={item.illustration} style={styles.cardImage}>
</ImageBackground>
</Card>
</TouchableOpacity>
)}
keyExtractor={(item) => item.index}
/>
This is the function that find the selected item "selectedIngredient"
function selectedIngredient(item) {
console.log('received: ', item.name)
item.isSelected = !item.isSelected;
return { ...item.isSelected }
}
That component call is working when I try debbug with console.log after the "item.isSelected = !item.isSelected", but the IU is not updated. Can someone help me to understand how to fix it?
You need to set state by using either setState in a class-based component or either using useState hook in the functional component.
function selectedIngredient(item) {
console.log('received: ', item.name)
item.isSelected = !item.isSelected; //this is not how you set state
return { ...item.isSelected }
}
Screenshot:
here is our old example which I modified for your current scenario:
import React, { useState } from 'react';
import {
Text,
View,
StyleSheet,
FlatList,
TouchableOpacity,
} from 'react-native';
import Constants from 'expo-constants';
// You can import from local files
// or any pure javascript modules available in npm
const ingredientList = [
{
id: 1,
name: 'item1',
selected: false,
},
{
id: 2,
name: 'item 2',
selected: false,
},
{
id: 3,
name: 'item 3',
selected: false,
},
{
id: 8,
name: 'item 4',
selected: false,
},
{
id: 4,
name: 'item 5',
selected: false,
},
{
id: 5,
name: 'item 6',
selected: false,
},
];
export default function App() {
const [selectedItem, setSelectedItem] = useState(null);
const [allItems, setAllItems] = useState(ingredientList);
const selectedIngredient = (item) => {
console.log('selecionado: ' + item.name);
setSelectedItem(item);
/* Below operation can be improved by passing index to the function itself.
so filtering would not be required
*/
let temp = allItems.filter((parentItem) => parentItem.id !== item.id);
item.selected = !item.selected;
temp = temp.concat(item);
temp.sort((a, b) => parseInt(a.id) - parseInt(b.id));
setAllItems(temp);
console.log(allItems);
};
return (
<View style={styles.container}>
<FlatList
style={styles.flatlist}
horizontal
bounces={false}
data={allItems}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.flatListItem}
key={item.id}
onPress={() => selectedIngredient(item)}>
<Text>{item.name}</Text>
{!item.selected ? (
<Text style={{ color: 'red' }}>{'Not Selected'}</Text>
) : (
<Text style={{ color: 'green' }}>{'Selected'}</Text>
)}
</TouchableOpacity>
)}
keyExtractor={(item) => item.index}
/>
{selectedItem ? (
<View style={styles.selectedTextView}>
<Text style={styles.selectedText}>{`${selectedItem.name} ${
selectedItem.selected ? 'selected' : 'not selected'
}`}</Text>
</View>
) : (
<View style={styles.selectedTextView}>
<Text style={styles.selectedText}>{`Nothing selected`}</Text>
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
flatListItem: {
width: 100,
height: 100,
backgroundColor: 'white',
margin: 5,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
},
selectedTextView: {
flex: 8,
backgroundColor: 'white',
margin: 5,
borderRadius: 10,
justifyContent: 'center',
alignItems: 'center',
fontSize: 20,
},
selectedText: {
fontSize: 30,
},
});
Live Demo