I am trying to create a react native animation with some images as a count down like this:
3 --> ease out down duration 1 second each case
2 --> ease out down
1 --> ease out down
I found a way to do this with react animatable, but the results don't convince me, and if there is a better way to do it please let me know.
I think everytime I run into a new render of the element start counting down I say to react animatable to make an iteration of 3 ease out down for each number changing the image number, this is not a natural way of solving the problem I have.
For the moment is not using redux, may be later I will add it.
The properties of the state: time, current time to begin are not being used yet.
All that I need is the effect that a count down is happening with the images I show, in a well defined animation.
I think is almost getting there, but surely there is a better way to face to this problem, any suggestion is well received, even though it is as a guide. I also tried with react-native-animate-number but no luck...
Thanks in advance
import React from 'react';
import util from '../utils.js';
import * as Animatable from 'react-native-animatable';
import {
View,
Dimensions,
Image,
StyleSheet
} from 'react-native';
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
const styles = StyleSheet.create({
mainOption: {
},
container: {
width: width,
height: height,
flex: 1,
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center'
}
});
const timeLapse = [
<Image source={require('../images/1.png')} key={util.uniqueId()}/>,
<Image source={require('../images/2.png')} key={util.uniqueId()}/>,
<Image source={require('../images/3.png')} key={util.uniqueId()}/>
];
export default class StartingGameCountDown extends React.Component {
constructor(props) {
super(props);
this.state = {
time: props.time,
currentTimeToBegin: props.currentTimeToBegin,
imagePosition: 0
};
}
componentWillReceiveProps(nextProps) {
const nextImage = this.state.imagePosition + 1;
this.setState({
...this.state,
letters: nextProps.time,
currentTimeToBegin: nextProps.currentTimeToBegin,
imagePosition: nextImage
});
}
render() {
return (
<View style={styles.container}>
<Animatable.View
ref='countDown'
duration={1000}
delay={0}
iterationCount={3}
animation="fadeOutDown"
style={styles.mainOption}>
{timeLapse[this.state.imagePosition]}
</Animatable.View>
</View>
);
}
}
Ok after taking a look into the documentation I found out that may be using sequence and timing could be an option:
import React from 'react';
import {
Animated,
Text,
View,
Dimensions,
Image,
Easing,
StyleSheet
} from 'react-native';
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
let counter = 0;
const util = {
uniqueId: () => {
return counter++;
}
};
const styles = StyleSheet.create({
mainOption: {
},
container: {
width: width,
height: height,
top: -100,
flex: 1,
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center'
}
});
const timeLapse = [
require('../images/1.png'),
require('../images/2.png'),
require('../images/3.png')
];
export default class StartingGameCountDown extends React.Component {
constructor(props) {
super(props);
this.state = {
time: props.time,
countDown: new Animated.Value(0),
currentTimeToBegin: props.currentTimeToBegin,
quantity: 3,
sizes: [],
duration: props.duration,
animatedValues: [new Animated.Value(0), new Animated.Value(0), new Animated.Value(0)]
};
}
animations() {
let result = [];
state = this.state;
for(let i = 0; i < state.quantity; i++) {
//Start
result.push(Animated.timing(
state.countDown,
{
toValue: 0,
duration: 0
}
)
);
let parallel = [];
//Animate
for(let j = 0; j < state.quantity; j++) {
parallel.push(
Animated.timing(
state.animatedValues[j],
{
easing: Easing.bezier(.07,.42,.85,.5),
toValue: i === j ? 1 : 0,
duration: i === j ? state.duration : 0
}
)
);
}
//Stop
parallel.push(Animated.timing(
state.countDown,
{
easing: Easing.bezier(.07,.42,.85,.5),
toValue: 1,
duration: state.duration
}
)
);
result = [...result, Animated.parallel(parallel)];
}
return result;
}
componentDidMount() {
Animated.sequence(this.animations()).start();
}
shouldComponentUpdate(nextProps, nextState) {
//I don't to run lifecycle in any case by mistake here
return false;
}
createImagesAnimated() {
let transitionY = this.state.countDown.interpolate({
inputRange: [0, 1],
outputRange: [0, this.props.transition.y]
});
let result = [];
//What to be Animated
for(let i = 0; i < this.state.quantity; i++) {
let image = this.state.animatedValues[i].interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0, 1, 0]
});
result.push(
<Animated.Image
key={util.uniqueId()}
source={timeLapse[i]}
style={{
//How to animate it
position: 'absolute',
opacity: image,
transform: [{
translateY: transitionY
}]
}}
/>
);
}
return result;
}
render() {
return (
<View style={styles.container}>
{this.createImagesAnimated()}
</View>
);
}
}
This would be the implementation of the component:
<CountDownComponent
transition={{y:200}}
duration={5000}
/>
This solves my problem I hope to find in another moment a better solution in the future.
Here is the repo:
https://github.com/jotaoncode/CountDown
Related
I am developing an app, which provides the design of different shapes like Square , I have used the following Reanimated 2 API . I want to get its coordinates (pageX and PageY) every time I move the object.
my research
So i read this article about measure and i decided to try with that.
Then i created ref to my animated view like that const aref = useAnimatedRef();.
After that i read again in Reanimated docs that i should use useDerivedValue combined with measure. But unfortunately I didn't get what I needed in the end. My app crashed with following error -
Tried to synchronously call usederivedvalue from a different thread
My code so far i tried to use runOnJs - no luck again! And my question is -
import React from "react";
import { StyleSheet, View } from "react-native";
import {
GestureHandlerRootView,
PanGestureHandler,
} from "react-native-gesture-handler";
import Animated, {
measure,
runOnJS,
useAnimatedGestureHandler,
useAnimatedRef,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
} from "react-native-reanimated";
const SIZE = 100.0;
const CIRCLE_RADIUS = SIZE * 2;
export default function App() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const aref = useAnimatedRef();
const panGestureEvent = useAnimatedGestureHandler({
onStart: (event, context) => {
context.translateX = translateX.value;
context.translateY = translateY.value;
},
onActive: (event, context) => {
translateX.value = event.translationX + context.translateX;
translateY.value = event.translationY + context.translateY;
const wrap = () => {
try {
const pageX = measure(aref).pageX;
console.log({ pageX });
} catch {}
};
useDerivedValue(() => {
runOnJS(wrap)(true);
});
},
onEnd: () => {
const distance = Math.sqrt(translateX.value ** 2 + translateY.value ** 2);
if (distance < CIRCLE_RADIUS + SIZE / 2) {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
}
},
});
const rStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translateX.value,
},
{
translateY: translateY.value,
},
],
};
});
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<View style={styles.container}>
<View style={styles.circle}>
<PanGestureHandler onGestureEvent={panGestureEvent}>
<Animated.View style={[styles.square, rStyle]} ref={aref} />
</PanGestureHandler>
</View>
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
square: {
width: SIZE,
height: SIZE,
backgroundColor: "rgba(0, 0, 256, 0.5)",
borderRadius: 20,
},
circle: {
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
alignItems: "center",
justifyContent: "center",
borderRadius: CIRCLE_RADIUS,
borderWidth: 5,
borderColor: "rgba(0, 0, 256, 0.5)",
},
});
My error - every time when i move my square my app crash with error above.
How can i get coordinates (pageX and PageY) every time I move the object.
What is causing the problem?
Of course this is the use of
const wrap = () => {
try {
const pageX = measure(aref).pageX;
console.log({ pageX });
} catch {}
};
useDerivedValue(() => {
runOnJS(wrap)(true);
});
in onActive event but i don't know how to fix that ?
Sorry for my bad English.
I mapped an object array to create a tag element with the details being mapped onto the element. And then I created an animation so on render, the tags zoom in to full scale. However, I was wanting to take it to the next step and wanted to animate each tag individually, so that each tag is animated in order one after the other. To me, this seems like a common use of animations, so how could I do it from my example? Is there any common way to do this that I am missing?
import {LeftIconsRightText} from '#atoms/LeftIconsRightText';
import {LeftTextRightCircle} from '#atoms/LeftTextRightCircle';
import {Text, TextTypes} from '#atoms/Text';
import VectorIcon, {vectorIconTypes} from '#atoms/VectorIcon';
import styled from '#styled-components';
import * as React from 'react';
import {useEffect, useRef} from 'react';
import {Animated, ScrollView} from 'react-native';
export interface ICustomerFeedbackCard {
title: string;
titleIconName: string[];
tagInfo?: {feedback: string; rating: number}[];
}
export const CustomerFeedbackCard: React.FC<ICustomerFeedbackCard> = ({
title,
titleIconName,
tagInfo,
...props
}) => {
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
const zoomAnim = useRef(new Animated.Value(START_ZOOM_SCALE)).current;
/**
* Creates an animation with a
* set duration and scales the
* size by a set factor to create
* a small zoom effect
*/
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: FINAL_ZOOM_SCALE,
duration: FAST_ZOOM,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
/**
* Sorts all tags from highest
* to lowest rating numbers
* #returns void
*/
const sortTags = () => {
tagInfo?.sort((a, b) => b.rating - a.rating);
};
/**
* Displays the all the created tags with
* the feedback text and rating number
* #returns JSX.Element
*/
const displayTags = () =>
tagInfo?.map((tag) => (
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
return (
<CardContainer {...props}>
<HeaderContainer>
<LeftIconsRightText icons={titleIconName} textDescription={title} />
<Icon name="chevron-right" type={vectorIconTypes.SMALL} />
</HeaderContainer>
<ScrollOutline>
<ScrollContainer>
{sortTags()}
{displayTags()}
</ScrollContainer>
</ScrollOutline>
<FooterContainer>
<TextFooter>Most recent customer compliments</TextFooter>
</FooterContainer>
</CardContainer>
);
};
And here is the object array for reference:
export const FEEDBACKS = [
{feedback: 'Good Service', rating: 5},
{feedback: 'Friendly', rating: 2},
{feedback: 'Very Polite', rating: 2},
{feedback: 'Above & Beyond', rating: 1},
{feedback: 'Followed Instructions', rating: 1},
{feedback: 'Speedy Service', rating: 3},
{feedback: 'Clean', rating: 4},
{feedback: 'Accommodating', rating: 0},
{feedback: 'Enjoyable Experience', rating: 10},
{feedback: 'Great', rating: 8},
];
Edit: I solved it by replacing React-Native-Animated and using an Animated View and instead using Animatable and using an Animatable which has built in delay. Final solution:
const displayTags = () =>
tagInfo?.map((tag, index) => (
<TagContainer animation="zoomIn" duration={1000} delay={index * 1000}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
));
Here is a gif of the animation
This is an interesting problem. A clean way you could approach this problem is to develop a wrapper component, DelayedZoom that will render its child component with a delayed zoom. This component would take a delay prop that you can control to add a delay for when the component should begin animation.
function DelayedZoom({delay, speed, endScale, startScale, children}) {
const zoomAnim = useRef(new Animated.Value(startScale)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
delay: delay,
toValue: endScale,
duration: speed,
useNativeDriver: true,
}).start();
};
zoomIn();
}, [zoomAnim]);
return (
<Animated.View
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
{children}
</Animated.View>
);
}
After this, you can use this component as follows:
function OtherScreen() {
const tags = FEEDBACKS;
const FAST_ZOOM = 800;
const START_ZOOM_SCALE = 0.25;
const FINAL_ZOOM_SCALE = 1;
function renderTags() {
return tags.map((tag, idx) => {
const delay = idx * 10; // play around with this. Main thing is that you get a sense for when something should start to animate based on its index, idx.
return (
<DelayedZoom
delay={delay}
endScale={FINAL_ZOOM_SCALE}
startScale={START_ZOOM_SCALE}
speed={FAST_ZOOM}>
{/** whatever you want to render with a delayed zoom would go here. In your case it may be TagContainer */}
<TagContainer>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</DelayedZoom>
);
});
}
return <View>{renderTags()}</View>;
}
I hope this helps to point you in the right direction!
Also some helpful resources:
Animation delays: https://animationbook.codedaily.io/animated-delay/
Demo
It is a bit of work to implement this, I didn't have your components to try it out so I have created a basic implementation, I hope this will help
import React, { useEffect, useRef, useState } from "react";
import { StyleSheet, Text, View, Animated } from "react-native";
const OBJ = [{ id: 1 }, { id: 2 }, { id: 3 }];
const Item = ({ data, addValue }) => {
const zoomAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
const zoomIn = () => {
Animated.timing(zoomAnim, {
toValue: 1,
duration: 500,
useNativeDriver: true
}).start(() => {
addValue();
});
};
zoomIn();
}, [zoomAnim]);
return (
<View>
<Animated.View
ref={zoomAnim}
style={[
{
transform: [{ scale: zoomAnim }]
}
]}
>
<Text style={styles.text}>{data}</Text>
</Animated.View>
</View>
);
};
function App() {
const [state, setState] = useState([OBJ[0]]);
const addValue = () => {
const currentId = state[state.length - 1].id;
if (OBJ[currentId]) {
const temp = [...state];
temp.push(OBJ[currentId]);
setState(temp);
}
};
return (
<View style={styles.app}>
{state.map((item) => {
return <Item data={item.id} key={item.id} addValue={addValue} />;
})}
</View>
);
}
const styles = StyleSheet.create({
text: {
fontSize: 20
}
});
export default App;
Basically, I am adding an element to the state at the end of the previous animation, one thing to note is that the key is very important, and don't use the index as a key. instead of Ids you might want to add any other value that is sorted or maybe link an item by passing the id of the previous item.
ADDING A SOLUTION USING REANIMATED AND MOTI
There is this library which you can use moti(https://moti.fyi/) it will work with reanimated, so you need to add reanimated too. Before using Reanimated you must consider that your normal chrome dev tools for that particular application will stop working with reanimated 2.0 and above you can use flipper though.
coming to the solution.
import { View as MotiView } from 'moti';
...
const displayTags = () =>
tagInfo?.map((tag, index) => (
<MotiView
key = {tag.id}
from={{ translateY: 20, opacity: 0 }}
animate={{ translateY: 0, opacity: 1 }}
transition={{ type: 'timing' }}
duration={500}
delay={index * 150}>
<TagContainer
style={[
{
transform: [{scale: zoomAnim}],
},
]}>
<LeftTextRightCircle feedback={tag.feedback} rating={tag.rating} />
</TagContainer>
</MotiView>
));
...
That's it, make sure to use a proper key, don't use index as key.
Side Note: If you are doubtful that sould you use reanimated or not, just go through https://docs.swmansion.com/react-native-reanimated/docs/ this page. Using Moti you can have really cool animation easily also if you reanimated version 2.3.0-alpha.1 then you need not to use Moti but as it is alpha version so it is not advisable to use in production you can wait for its stable release too.
I was using the reanimated v2 for creating animations for an app. I was creating a splash (loading) screen with three dots that jump up and down. The 3 dots are supposed to have certain constant delay (interval) between them. Like the animation when the other person is typing in facebook's messenger.
The animation looks fine in the beginning but after a while the 2 dots or even 3 dots depending on the delays and duration sync up and I am left with 2 or 3 dots absolutely in sync with each other. Here is the video of the problem animation video
I am very new to react-native and reanimated. So I am assuming the problem is in my code. I have no idea if this is the correct way to do this. The code examples I see in reanimated v1 have "startClock" and custom "runTiming" functions but I couldn't find them in the docs for v2. reanimated docs
import React, { useEffect } from "react";
import { View, StyleSheet } from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
withDelay,
} from "react-native-reanimated";
import { themeColor } from "../../assets/ThemeColor";
const Loading = () => {
const y1 = useSharedValue(0);
const y2 = useSharedValue(0);
const y3 = useSharedValue(0);
const animatedStyles1 = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withDelay(
0,
withRepeat(withTiming(y1.value, { duration: 200 }), -1, true)
),
},
],
};
});
const animatedStyles2 = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withDelay(
100,
withRepeat(withTiming(y2.value, { duration: 200 }), -1, true)
),
},
],
};
});
const animatedStyles3 = useAnimatedStyle(() => {
return {
transform: [
{
translateY: withDelay(
200,
withRepeat(withTiming(y3.value, { duration: 200 }), -1, true)
),
},
],
};
});
/**
*
*
*
*
*/
useEffect(() => {
y1.value = -10;
y2.value = -10;
y3.value = -10;
}, []);
/**
*
*
*
*
*
*/
return (
<View style={styles.loadingContainer}>
<Animated.View
style={[styles.ballStyle, animatedStyles1]}
></Animated.View>
<Animated.View
style={[styles.ballStyle, animatedStyles2]}
></Animated.View>
<Animated.View
style={[styles.ballStyle, animatedStyles3]}
></Animated.View>
</View>
);
};
const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
},
ballStyle: {
width: 13,
height: 13,
backgroundColor: themeColor,
borderRadius: 13,
margin: 10,
},
});
export default Loading;
Can someone please tell me why the animations sync up eventually and what is the correct way to animate three elements with the same animation but some constant delay. Thank you.
Every element of the array should be displayed for some time and the time for which each element is displayed should be determined by a value in each element.
let array=[{display:"a",time:10},{display:"b",time:15},{display:"c",time:22}]
class App extends React.Component{
state={stateDisplay:"",
stateTime:""
}
componentWillMount(){
var i=0;
let handle=setInterval(()=>{
var element= array[i]
this.setState({
stateDisplay:element.display,
stateTime:element.time,
})
i=i+1;
if(i===array.length){
clearInterval(handle)
}
},10000)
}
render(){
return(
<div> {this.state.stateDisplay} </div>
)}}
i have done something like this but using setinterval the delay can only be set for a constant time,here 10s.
I want the first element to display for 10s and then the next element for 15s, third for 22s which is the time value for each element of the array.
I know i cant do that using setinterval is there a way to do this using Settimeout?
This was almost like a little challenge, heres what i managed to come up with, its in typescript, if you need js, just remove interfaces and type annotations
/* eslint-disable #typescript-eslint/no-explicit-any */
/* eslint-disable prettier/prettier */
/* eslint-disable no-shadow */
/* eslint-disable no-console */
import React, { FC, useState, useEffect, useCallback } from 'react';
import { View, Button, Text } from 'react-native';
interface Data {
duration: number;
bgColor: string;
}
const dataArr: Data[] = [
{ duration: 3, bgColor: 'tomato' },
{ duration: 6, bgColor: 'skyblue' },
{ duration: 9, bgColor: 'gray' },
];
const Parent = () => {
const [currentIdx, setCurrentIdx] = useState<number>(0);
const [elementData, setElementData] = useState<Data>(dataArr[currentIdx]);
useEffect(() => {
console.log('idx', currentIdx);
if (currentIdx > dataArr.length) return;
setElementData({ ...dataArr[currentIdx] });
}, [currentIdx]);
const pushNext = () => {
setCurrentIdx(currentIdx + 1);
};
const handleRestart = () => {
setCurrentIdx(0);
setElementData({ ...dataArr[0] });
};
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Timer
data={elementData}
onCountDownComplete={pushNext}
restart={handleRestart}
/>
</View>
);
};
interface Props {
data: Data;
onCountDownComplete: () => void;
restart: () => void;
}
const Timer: FC<Props> = ({ data, onCountDownComplete, restart }) => {
const [seconds, setSeconds] = useState<number>(data.duration);
// update on data change
useEffect(() => {
setSeconds(data.duration);
}, [data]);
const callback = useCallback(() => {
onCountDownComplete();
}, [onCountDownComplete]);
useEffect(() => {
let interval: any = null;
if (seconds > -1) {
interval = setInterval(() => {
if (seconds - 1 === -1) {
callback();
} else {
setSeconds(seconds - 1);
}
}, 1000);
} else {
return;
}
return () => {
clearInterval(interval);
};
}, [seconds, callback]);
return (
<View
style={{ backgroundColor: data.bgColor, padding: 16, borderRadius: 10 }}
>
<Text style={{ marginBottom: 24 }}>{seconds}</Text>
<Button title="restart" onPress={restart} />
</View>
);
};
I have a SectionList with a RN elements ListItem, I have hidden the chevron and set the switch state to a state variable. the onSwitch method is not working, or at least i cant get it to work.
I have tried a few different ways of implementing it to no avail.
<SectionList
sections={[
{title: 'Company Focus', data: this.state.focusNodes},
]}
renderSectionHeader={({section}) => <Text style=
{styles.sectionHeader}>{section.title}</Text>}
renderItem={({item}) => <ListItem style={styles.subTitle} key=
{item.id} title={item.name}
switchButton hideChevron switched=
{this.state.isNodeSelected} switchOnTintColor={'#00BCD4'}
onSwitch={(value) => {
this._handleSwitch(value);
}}/>} />
_handleSwitch = (item) => {
this.state.isNodeSelected = true;
console.log(item);
console.log(this.state.isNodeSelected);
}
The other way:
onSwitch={(value) => {
this.setState(previousState => {
return{previousState, isNodeSelected: value}})
}}/>} />
The switch in the first example just moves then moves back. In the second example it moves all the switches in the list to on or off regardless of which one i switch on/off
EDIT - THE WHOLE COMPONENT :)
'use strict';
import React from 'react';
import { StyleSheet,
Text,
View,
NavigatorIOS,
ScrollView,
Button,
PickerIOS,
Dimensions,
SectionList,
TouchableHighlight,
} from 'react-native';
import {Header, ListItem, List} from 'react-native-elements';
import StepIndicator from 'react-native-step-indicator';
export default class FocusPage extends React.Component {
constructor(props){
super(props);
this.state ={
data : [],
roleNodes: [],
skillNodes: [],
focusNodes: [],
// roleSelection: '',
// childSelection: '',
isVisible: false,
currentPosition: 0,
selectedNodes: [],
isFocusNodeSelected: false
}
}
_createFocusNodesArray = (array) => {
var nodeArray =[];
for(var i = 0; i < array.length; i++){
if(array[i].orderNumber === 2)
nodeArray.push(array[i]);
}
return nodeArray;
}
_getFocusData = () => {
fetch('http://10.0.0.58:8082/api/getfocusnodes', {
method: 'GET'
})
.then((response) => response.json())
.then((responseJson) => {
// this.setState({
// data: responseJson
// })
var simpleArray =
this._simplifyJsonArray(responseJson);
// console.log(simpleArray);
this.setState({
focusNodes:
this._createFocusNodesArray(simpleArray),
})
})
.catch((error) => {
console.log(error);
// this.setState({
// isLoading: false,
// message: 'Woops! ' + error
// })
})
}
//'http://10.0.0.58:8082/api/gettreenodes'
//'http://192.168.6.217:8082/api/gettreenodes'
componentDidMount = () => {
this._getFocusData();
}
_simplifyJsonArray = (array) => {
var tempArray = [];
for(var i = 0; i < array.length; i++)
{
var tempNode = {
id: array[i].id,
name: array[i].name,
orderNumber: array[i].orderNumber,
isSelected: false
}
tempArray.push(tempNode);
}
return tempArray;
}
getSelection = (selectedItem) => {
console.log(selectedItem);
// selectedItem.isSelected = true;
// this.state.isNodeSelected = true;
for (let index = 0; index < this.state.focusNodes.length;
index++) {
const element = this.state.focusNodes[index];
if(element.name === selectedItem){
console.log(element.name);
this.state.isFocusNodeSelected = true;
}
}
if (selectedItem.isSelected) {
this.state.isFocusNodeSelected = true;
}
// console.log(this.state.isNodeSelected);
// for (let i = 0; i < this.state.focusNodes.length; i++) {
// const element = this.state.focusNodes[i];
// if (element.name === selectedItem) {
// element.isSelected = true;
// this.state.isNodeSelected = element.isSelected;
// this.state.selectedNodes.push(element);
// console.log(this.state.isNodeSelected);
// }
// else{
// this.state.isNodeSelected = false;
// }
// }
// console.log('The selected item: ' + selectedItem);
// console.log('The selected Array: ' + this.state.selectedNodes);
}
_handleSwitch = (item) => {
this.state.isFocusNodeSelected = true;
console.log(item);
console.log(this.state.isFocusNodeSelected);
}
render() {
return (
<View style={styles.container}>
<SectionList
sections={[
{title: 'Company Focus', data: this.state.focusNodes},
]}
renderSectionHeader={({section}) => <Text style={styles.sectionHeader}>{section.title}</Text>}
renderItem={({item}) => <ListItem style={styles.subTitle} key={item.id} title={item.name}
switchButton hideChevron switched={item.isSelected} switchOnTintColor={'#00BCD4'}
onSwitch={(value) => {
this.setState(previousState => {
return{previousState, isFocusNodeSelected: value}
})
}}/>} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
pageTitle:{
fontSize: 27,
textAlign: 'center',
//margin: 20
},
sectionHeader:{
fontSize: 20,
backgroundColor: '#673AB7',
height: 40,
textAlign: 'center',
padding: 10,
color: 'white'
},
subTitle: {
fontSize: 16,
// textAlign: 'center',
// marginTop: 20,
backgroundColor: '#00BCD4'
},
popButton: {
borderRadius: 10,
padding: 5,
// marginLeft: 5,
marginRight: 5,
backgroundColor: '#00BCD4',
borderColor: 'black',
borderWidth: 1,
},
subtitleView: {
flexDirection: 'row',
paddingLeft: 10,
paddingTop: 5,
textAlign: 'right'
},
});
In your first example, the reason why your switches get flipped back is because you are always setting your switches to true, so they can never get set to false:
this.state.isNodeSelected = true;
That's not the only problem though. As you see in your second example, you fix this by setting the value to what get's passed into onSwitch as value. That's good. Now the issue of having all switches flip is because you are using the same state value for all switches.
switched={this.state.isNodeSelected}
So what is happening is that when you update isNodeSelected, you are updating it for every switch.
To fix this, you need to hold the switched value for each ListItem somewhere; probably the most straightforward would be in what you pass to sections.
sections={[
{title: 'Company Focus', data: this.state.focusNodes},
]}
The data you pass into your sections prop should be kept in your state so you can update the specific item that whose switch is flipped. However, without seeing your state code, it's hard to tell what you're doing and how exactly to fix it. The above explanation should be enough to get you to a solution though. Just remember that renderItem also gets an index argument which are shown in the simple examples from the docs and explained further in the prop docs.
Edit: With the edited in info and changes, we now have a renderItem where each ListItem has its own switched value stored in item.isSelected. So given that, our goal is to have a onSwitch that updates just that value for just that item. So what do we update?
Well, the SectionList's sections prop is getting that data from this.state.focusNodes (what data is set to). So updating the correct value in focusNodes is what needs to happen. As I alluded to above, one way to do this is to leverage renderItem's index argument:
renderItem={({item, index}) =>
<ListItem
style={styles.subTitle}
key={item.id}
title={item.name}
switchButton
hideChevron
switched={item.isSelected}
switchOnTintColor={'#00BCD4'}
onSwitch={(value) => {
let focusNodes = [...this.state.focusNodes];
focusNodes[index].isSelected = value;
this.setState({ focusNodes, isFocusNodeSelected: value });
}}
/>
}
Notes:
I used index to figure out which of the focusNodes needs to get updated. Since you are using SectionList, read the docs to understand how this index value gets determined. Be careful and don't make assumptions once you start using multiple sections. I say this because...
I noticed that data in your state is unused. If you eventually refactor and change your sections prop to use this instead or you move focusNodes into that, you'll have to refactor what is being updated. Take care and understand how your data is structured and not make a bad assumption about index.
In case the missing previousState threw you off, I used a shorthand in setState to make it cleaner.
I'm making the assumption that the value being passed to onSwitch is the correct boolean value that isSelected needs to be updated to. I don't have a test project setup to run this to confirm.