How does Animated.Event work in React Native? - react-native

I'm I understanding it correctly ?
Does this two set of code meant the same thing ?Does it have any difference in performance or reliability ?
<ScrollView
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: this.state.scrollY}}}]
)}
>
</ScrollView>
AND
handleScroll(e){
this.setState({ scrollY : e.nativeEvent.contentOffset.y });
}
<ScrollView
onScroll={(e) => this.handleScroll(e)}
>
</ScrollView>
Thanks

it's not the same. Animated.event is used to map gestures like scrolling, panning or other events directly to Animated values. so in your first example this.state.scrollY is an Animated.Value. you would probably have code somewhere that initialized it, maybe your constructor would looks something like this:
constructor(props) {
super(props);
this.state = {
scrollY: new Animated.Value(0)
};
}
in your second example this.state.scrollY is the y value (just the number) that was triggered in the scroll event, but completely unrelated to animation. so you couldn't use that value as you could use Animated.Value in animation.
it's explained here in the documentation

If you want to handle the scroll you can use it this way:
handleScroll = (event) => {
//custom actions
}
<ScrollView
onScroll={Animated.event(
[{ nativeEvent: {
contentOffset: {
x: this.state.scrollY
}
}
}],{
listener: event => {
this.handleScroll(event);
}})
}>
</ScrollView>

According to this source code Animated.event traverses the objects passed as arguments of it until finds an instance of AnimatedValue.
Then this key (where the AnimatedValue has been found) is applied to the callback (onScroll) and the value at the key of the event passed to the scroll is assigned to this AnimatedValue.
In the code:
const animatedValue = useRef(new Animated.Value(0)).current;
...
onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: animatedValue}}}]
)}
is the same as
const animatedValue = useRef(new Animated.Value(0)).current;
...
onScroll={({nativeEvent: { contentOffset: {y} }}) => {
animatedValue.setValue(y);
}}
If your callback accepts more than one event (argument), then just put the mapping object at the needed index (thus the array as the argument of Animated.value.
onScroll={Animated.event(
[
{}, // <- disregard first argument of the event callback
{nativeEvent: {contentOffset: {y: animatedValue}}} // <- apply mapping to the second
]
)}

Yes there is a difference in semantic
<ScrollView onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: this.state.scrollY}}}]
)}></ScrollView>
The first one i.e the above Animated.event returns a function that sets the scrollview's nativeEvent.contentOffset.y to your current scrollY state which I assume is animated.
The other code just sets scrollY to your scrollView's e.nativeEvent.contentOffset.y and causes a rerender to your component

Related

Conditionally trigger Animated.timing from ScrollY position of ScrollView

I've created a template for screens throughout my app. The template utilises Animated Header components that respond to the scrollY position of the ScrollView content.
I'm able to use this scrollY variable to interpolate items, but I can't figure out how to use the variable to trigger Animated.timing events.
In simple terms:
if (ScrollY > 30) do Animated.timing one
if (ScrollY < 30) do Animated.timing two
For now I've created an inadequate workaround using the Animated.event() listener:
<ScrollView
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollListener } } }],
{
listener: (event) => {
handleScroll(event);
},
}
)}
...
The handleScroll(event) controls an array of Animated.timing events, each of which operates conditionally from a setScrollPosition() useState variable:
const [scrollPosition, setScrollPosition] = useState(0);
const fixedHeaderTranslatePosition = useRef(new Animated.Value(4)).current;
const handleScroll = (event) => {
setScrollPosition(
event.nativeEvent.contentOffset.y +
(Platform.OS === 'ios' ? HEADER_MAX_HEIGHT : 0)
);
if (scrollPosition > 30 && headerType === 'Animated') {
Animated.timing(fixedHeaderTranslatePosition, {
toValue: 0,
useNativeDriver: true,
}).start();
...
}
Whilst my efforts do create a functional component, there are some minor problems.
Firstly, the event.nativeEvent.contentOffset.y seems to cause a lag on Android devices. This is most noticeable with the fixedHeaderTranslatePosition.
Also logically it would make much more sense to utilise the already declared scrollListener from the Animated.event. So my question is: How can I utilise the ScrollY variable (scrollListener) inside my handleScroll()? 🤔
The ScrollY variable is not noted above since it makes more sense in context. Here's a working snack: https://snack.expo.dev/#dazzerr/animated-header-example . You can search for "TODO" to find the areas that I believe require attention.
Thanks in advance!

useState function seems to block Animated.timing event?

I've created a "twitter style" button that when pressed opens up a sub-menu of items that can be selected/"tweeted" about.
The button is simple in that when pressed, it triggers a function with Animated events:
const toggleOpen = () => {
if (this._open) {
Animated.timing(animState.animation, {
toValue: 0,
duration: 300,
}).start();
} else {
Animated.timing(animState.animation, {
toValue: 1,
duration: 300,
}).start(); // putting '() => setFirstInteraction(true)' here causes RenderItems to disappear after the animation duration, until next onPress event.
}
this._open = !this._open;
};
and here's the button that calls this function:
<TouchableWithoutFeedback
onPress={() => {
toggleOpen();
// setFirstInteraction(true); // this works here, but the button doesn't toggleOpen until the 3rd + attempt.
}}>
<Animated.View style={[
styles.button,
styles.buttonActiveBg,
]}>
<Image
style={styles.icon}
source={require('./assets/snack-icon.png')}
/>
</Animated.View>
</TouchableWithoutFeedback>
I need to add a second useState function that is called at the same time as toggleOpen();. You can see my notes above regarding the problems I'm facing when using the setFirstInteraction(true) useState function I'm referring to.
Logically this should work, but for some reason when I add the setFirstInteraction(true) it seems to block the toggleOpen() function. If you persist and press the button a few times, eventually the toggleOpen() will work exactly as expected. My question is, why does this blocking type of action happen?
You can reproduce the issue in my snack: https://snack.expo.dev/#dazzerr/topicactionbutton-demo . Please use a device. The web preview presents no issues, but on both iOS and Android the issue is present. Line 191 is where you'll see the setFirstInteraction(true) instance.
Your animatedValue isn't stable. This causes it to be recreated on each state change. It is advised to useRef instead (though, useMemo would do the trick here as well).
const animState = useRef(new Animated.Value(0)).current;
Your toggleOpen function can also be simplified. In fact, you only need a single state to handle what you want and react on it in a useEffect to trigger the animations that you have implemented.
I have called this state isOpen and I have removed all other states. The toggleOpen function just toggles this state.
const [isOpen, setIsOpen] = useState(false)
const toggleOpen = () => {
setIsOpen(prev => !prev)
}
In the useEffect we react on state changes and trigger the correct animations.
const animState = useRef(new Animated.Value(0)).current;
useEffect(() => {
Axios.get('https://www.getfretwise.com/wp-json/buddyboss/v1/forums')
.then(({ data }) => setData(data))
.catch((error) => console.error(error));
}, []);
useEffect(() => {
Animated.timing(animState, {
toValue: isOpen ? 1 : 0,
duration: 300,
useNativeDriver: true,
}).start();
}, [isOpen, animState])
I have adapted your snack. Here is a working version.
Remarks: Of course, you still need for your data to be fetched from your API. The opacity change of the button is still the same and it remains disabled until the data has been fetched.

Animation and state issue: using Animated.spring inside useEffect

I'm new to React-Native app development.
https://snack.expo.io/RjmfLhFYg
I'm trying to understand why the animation stops working if I un-comment line 11 setTest(false) in the linked expo snack.
Thank you!
Relevant code copy:
export default function App() {
const element1 = useRef(new Animated.ValueXY()).current;
const element2 = new Animated.ValueXY();
const [test, setTest] = useState(true);
useEffect(() => {
// setTest(false);
setTimeout(() => {
Animated.spring(
element2,
{
toValue: {x: -10, y: -100},
useNativeDriver: true
}
).start();
}, 2000);
}, []);
return (
<View style={styles.container}>
<Animated.View
style={[styles.element, {backgroundColor: "blue"}, {
transform: [
{translateX: element2.x}, {translateY: element2.y}
]
}]}>
</Animated.View>
<Animated.View
style={[styles.element, {backgroundColor: "red"}, {
transform: [{translateX: element1.x}, {translateY: element1.y}]
}]}
>
</Animated.View>
</View>
);
}
useState docs
The setState function is used to update the state. It accepts a new state value and enqueues a re-render of the component.
Remember that when you change the state of a component it causes a "re-render" so your element2 variable gets back to its initial value. To solve it use the "useRef" hook on the element2 variable just like you did with element1.
const element2 = useRef(new Animated.ValueXY()).current;
"useRef" hook will make the variable persistent through the component life cycle so it won't be affected if the component gets re-rendered
useRef docs
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.

React Native - changing screen content while animating

I have an animated screen using gesture handler, and reanimated libraries. My problem is I am trying to change the screen content while the animations fired. I have made it work, but honestly I am 100% sure this is not the best solution (or it might be, but need some more configurations)
Code:
const onStateChangeLogin = event([
{
nativeEvent: ({state}) => block([
cond(eq(state, State.END), set(buttonOpacity, runTiming(new Clock(), 1, 0))), call([], () => handleTapLogin())
])
}
]);
In the call function, I am trying to change the content:
const handleTapLogin = (e) => {
setContent(<Login />)}
This is the Animated.View:
<TapGestureHandler onHandlerStateChange={onStateChangeLogin}>
<Animated.View style={{...styles.button, opacity: buttonOpacity, transform:[{translateY: buttonY}]}}>
<Text style={{...styles.buttonText, color: colors.white}}>{strings.signin}</Text>
</Animated.View>
</TapGestureHandler>
And the screen content is located in the bottom of my 'return' method:
....</Animated.View>
</TapGestureHandler>
{content}
</Animated.View>
</View>
</Container>
So it's working now, but too slow. I think because the call function in the event is async. So what happens now, the content is changing, but sometimes I need to wait like 2-3 secs to see the right screen.
I have already tried to use 'listener' in the event function:
const onStateChangeLogin = event([
{
nativeEvent: ({state}) => block([
cond(eq(state, State.END), set(buttonOpacity, runTiming(new Clock(), 1, 0))), call([], () => handleTapLogin())
], {
listener: () => handleTapLogin
}
)
}
]);
And I'm also tried to use onGestureEvent and onHandlerStateChange together like:
<TapGestureHandler onGestureEvent={onStateChangeLogin} onHandlerStateChange={handleTapLogin}>
...
</TapGestureHandler>

Make react-native component blink at regular time interval

I am trying to make a component "blink" on my page. I was thinking about setting a visible: true state in my componentWillMount method and then put a timeout of 1s in componentDidUpdate to set state to the "opposite" of the previous state. As I see it the component lifecycle looks like this :
sets state to visible to true (componentWillMount that runs only once and is not triggering a rerender)
enters componentdidUpdate
waits 1s
hides component (setstate to visible false)
enters componentDidUpdate
waits 1s
shows component (setstate to visible true)
However my component is blinking but the intervals of hide and show are not regular, they change and dont seem to follow the 1s logic
Here's my component code :
class ResumeChronoButton extends Component {
componentWillMount(){
console.log('in componentWillMount')
this.setState({visible: true})
}
componentDidUpdate(){
console.log('in componentDidUpdate')
setTimeout(() =>this.setState({visible: !this.state.visible}), 1000)
}
// componentWillUnmount(){
// clearInterval(this.interval)
// }
render(){
const { textStyle } = styles;
if (this.state.visible){
return (
<TouchableOpacity onPress={this.props.onPress}>
<Pause style={{height: 50, width: 50}}/>
</TouchableOpacity>
);
}
else {
return (
<View style={{height: 50, width: 50}}>
</View>
)
}
}
};
How can I make my component blink at regular time interval.
The following works for me
componentDidMount = () => {
this.interval = setInterval(() => {
this.setState((state, props) => {
return {
visible: !state.visible,
};
});
}, 1000);
};
componentWillUnmount = () => {
clearInterval(this.interval);
};
and then your render can just check this.state.visible to determine if it needs to show or not.
alternatively you could change the setState to
this.setState({visible: !this.state.visible})
Most likely because you are using the state and timeouts. State is set asynchronously and, for this reason, it may take different amounts of time to change the value depending on how many resources you are using.
To achieve the effect you want I would recommendo you to use the Animation framework from React Native. Check the docs.
just use
setInterval(()=>{//setstate here},time_in_ms)