React-Native animations dont work properly after setValue - react-native

I have a list of items rendered in flatlist and I'm animating each of them.
In constructor I assign values for animations
constructor(props){
super(props);
this.state = { ... };
this.height = new Animated.Value(SCREEN_WIDTH/1.8);
this.width = new Animated.Value(SCREEN_WIDTH/2.5);
}
So I render items by index to get different values of them like this:
renderStyle
if(index == 0){
return {
marginRight: SCREEN_WIDTH / 35,
marginLeft: SCREEN_WIDTH / 35,
margin: SCREEN_WIDTH / 50,
width: this.width,
height: this.height,
opacity: this.state.opacity,
borderRadius: SCREEN_WIDTH / 36,
}
}
if(index == 1){
this.height.setValue(SCREEN_WIDTH/1.5);
return {
marginRight: SCREEN_WIDTH / 35,
marginLeft: SCREEN_WIDTH / 35,
margin: SCREEN_WIDTH / 50,
width: this.width,
height: this.height,
opacity: this.state.opacity,
borderRadius: SCREEN_WIDTH / 36,
}
}
then I animate the items when user press on it. Index = 0 animates correctly but index = 1 which I set the new value is not animating at all, I dont understand whats wrong with it? Can you explain me please?
Animation function:
Animated.parallel([
Animated.timing(this.width,{
toValue: SCREEN_WIDTH/1.2,
duration: 750
}),
Animated.timing(this.height,{
toValue: SCREEN_HEIGHT/1.2,
duration: 750
}),
Animated.timing(this.state.opacity,{
toValue: 0,
duration: 750
}),
]).start(() => this.props.go(item));
setTimeout(() => Actions.video({item: item}), 800);
Render
return
(
<Animated.View pointerEvents={this.state.pointer} onLayout={({nativeEvent}) => {
this.setState({
measurements: nativeEvent.layout
})
}}>
<TouchableOpacity onPress={this.captureAnimation.bind(this, item, index)}>
<AnimatedImage source={{uri: 'http://' + item.info.picture_path}} style={this.renderStyle(index)}/>
</View>
</TouchableOpacity>
</Animated.View>
);
This is a different JS file, so flatlist is working in home page and renderItem method gets the values in this JS file to be able to animate each of it.
Edit: I found a alternative solution but anyways its not still explaining why after set value animation is not working.

Related

React Native Image component with "{width: 100%, height: auto}" gets dislocated when using margin

I have an Image component that has width from Dimensions.get('window').width,
and height that is calculated using this formula:
{ //not the real code, but it works exactly like this.
width: Dimensions.get('window').width,
height: actualImageHeight*(Dimensions.get('window').width/actualImageWidth)
}
It basically closely emulates how width: 100%, height: auto works in css.
and it works well until i added margins to it, which causes the image to get dislocated
like this:
Dislocated Bear
I have also tried to use PixelRatio.getPixelSizeForLayoutSize(margin*2) to try taking account the margin, which makes the new formula look like this:
{ //not the real code, but it works exactly like this.
width: Dimensions.get('window').width-PixelRatio.getPixelSizeForLayoutSize(margin*2),
height: actualImageHeight*((Dimensions.get('window').width-PixelRatio.getPixelSizeForLayoutSize(margin*2))/actualImageWidth)
}
and the result is almost, there, but still slightly dislocated: Slightly Dislocated Bear
Which makes me think that Dimensions isn't a good reference for width.
So how do I emulate width: 100%, height: auto that doesn't use Dimensions?
Is it possible to use width: 100% as a reference to use in the formula?
import React from 'react';
import {SafeAreaView,View,Dimensions,Image,} from 'react-native';
const {width, height} = Dimensions.get('window');
const actualImageHeight = 200;
const actualImageWidth = 300;
const Test = props => {
return (
<SafeAreaView style={{flex: 1}}>
<View
style={{
width: width,
height: actualImageHeight * (width / actualImageWidth),
borderRadius: 6,
borderWidth: 1,
borderColor: '#f1f1f1',
overflow: 'hidden',
backgroundColor: 'red',
padding: 12,
}}>
<Image
style={{flex: 1, width: null, height: null}}
source={{uri: 'https://picsum.photos/200/300'}}
/>
</View>
</SafeAreaView>
);
};
when you place calculated width and height to any view if you place margin in styles it will dislocated the view because while rendering the view margin also considered. Better to wrap image with view.
I still continued the idea of subtracting the width with the margins, and I managed to make it work. Simply by doing Dimensions.get('window').width-(margin*2) will make it work. So final code is:
{
width: Dimensions.get('window').width-(margin*2),
height: actualImageHeight*(Dimensions.get('window').width-(margin*2)/actualImageWidth)
}
The result: Bears
Full code of the component:
import React, { useState, useEffect } from "react";
import { Dimensions, Image } from "react-native";
interface AnotherCardProps {
thumbnail: string;
margin?: number;
column?: number;
maxHeight?: number;
minHeight?: number;
}
const AnotherCard: React.FC<AnotherCardProps> = (props) => {
const [imageSize, setImageSize] = useState({ width: 0, height: 0, ratio: 0 });
const margin = props.margin || 0;
const column = props.column || 1;
const newWidth = Dimensions.get("window").width / column - margin * 2;
const maxHeight = props.maxHeight || newWidth * 1.5;
const minHeight = props.minHeight || newWidth;
useEffect(() => {
Image.getSize(props.thumbnail, (w, h) =>
setImageSize({
width: w,
height: h,
ratio: newWidth / w,
})
);
});
return (
<Image
style={{
borderRadius: 20,
margin: margin,
flex: 1,
width: newWidth,
height:
imageSize.height * imageSize.ratio < minHeight
? minHeight
: imageSize.height * imageSize.ratio > maxHeight
? maxHeight
: imageSize.height * imageSize.ratio,
resizeMode: "cover",
}}
source={{
uri: props.thumbnail,
}}
/>
);
};
export default AnotherCard;

PanGestureHandler with functional component react native

I am trying to use a Gesture handler with a functional component. The problem is when I drag for the second time it's dragging from initial position again
This is my code below
let translateXRef = useRef(new Animated.Value(0)).current;
const onGestureEvent = useCallback(
Animated.event(
[
{
nativeEvent: {
translationX: translateXRef,
},
},
],
{ useNativeDriver: true }
),
[]
);
<View
style={{
backgroundColor: '#FFFFFF80',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
height: 100,
}}
>
<PanGestureHandler
onGestureEvent={onGestureEvent}
onHandlerStateChange={onHandlerStateChange}
>
<Animated.View
// eslint-disable-next-line react-native/no-inline-styles
style={{
height: '100%',
width: 10,
backgroundColor: AppColors.buttonColor,
transform: [{ translateX: translateXRef }],
}}
/>
</PanGestureHandler>
</View>
You need to use the context in addition to the event in your callback.
I'm not sure why you're using the Animated.event. You should generate your callbacks using the useAnimatedGestureHandler.
Each of those callbacks onStart, onActive, onEnd, etc... take an event and context argument.
The context argument is an object that would let you set your previous position so that then next click would not reset the position.
Here's more info:
https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/events/#using-context
Also, a pretty good video that explains it:
https://youtu.be/4HUreYYoE6U

How to animate header to show based on scrolling in react native?

So Ideally, When i scroll down, I want the header to disappear(slide down) and when I scroll up I want it to show (slide up). Idc where im at in the page. I just want the animation to fire when those 2 events occur. I see some apps have this but I can't think of how to replicate it. please help me set a basis for this
You can use Animated.FlatList or Animated.ScrollView to make the scroll view, and attach a callback to listen onScroll event when it is changed. Then, using interpolation to map value between y-axis and opacity.
searchBarOpacityAnim is a component's state. By using Animated.event, the state will be updated when a callback is called. Also, don't forget to set useNativeDriver to be true. I've attached the link to document below about why you have to set it.
<Animated.FlatList
...
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: searchBarOpacityAnim } } }],
{ useNativeDriver: true },
)}
...
/>
Then, use Animated.View wraps your component which you want to animate it. Use .interpolate to map value between the state and component's opacity like the example below...
<Animated.View
style={{
opacity: searchBarOpacityAnim.interpolate({
inputRange: [213, 215],
outputRange: [0, 1],
}),
}}
>
<SearchBar />
</Animated.View>
You can read more information about useNativeDriver, .interpolate, and Animated.event here.
https://facebook.github.io/react-native/docs/animated#using-the-native-driver
https://facebook.github.io/react-native/docs/animations#interpolation
https://facebook.github.io/react-native/docs/animated#handling-gestures-and-other-events
You can use Animated from 'react-native'
here an example changing the Topbar height:
import { Animated } from 'react-native';
define maxHeight and minHeight topbar
const HEADER_MAX_HEIGHT = 120;
const HEADER_MIN_HEIGHT = 48;
initialize a variable with the scrollY value
constructor(props) {
super(props);
this.state = {
scrollY: new Animated.Value(
Platform.OS === 'ios' ? -HEADER_MAX_HEIGHT : 0,
),
};
}
on render you can interpolate a value acording the scrollY Value
render() {
const { scrollY } = this.state;
// this will set a height for topbar
const headerHeight = scrollY.interpolate({
inputRange: [0, HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT],
outputRange: [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT],
extrapolate: 'clamp',
});
// obs: the inputRange is the scrollY value, (starts on 0)
// and can go until (HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT)
// outputRange is the height that will set on topbar
// obs: you must add a onScroll function on a scrollView like below:
return (
<View>
<Animated.View style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: '#2e4265',
height: headerHeight,
zIndex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
}}>
<Text>{title}</Text>
</Animated.View>
<ScrollView
style={{ flex: 1 }}
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.scrollY } } }],
)}>
<View style={{ height: 1000 }} />
</ScrollView>
</View>
);
}

How to make a looped image background in React Native with Animated

I originally used setInterval() to make a looped image background by having two images that one starts at x:0 and another starts at x: imageWidth, then update them in the following way:
_updateBackgroundImage = () => {
this.setState({
background1Left: this.state.background1Left > (-this.backgroundImageWidth) ? this.state.background1Left-3 : this.backgroundImageWidth,
background2Left: this.state.background2Left > (-this.backgroundImageWidth) ? this.state.background2Left-3 : this.backgroundImageWidth,
})
}
It worked just fine but setInterval() was causing conflicts with another component from an online library, so I switched to using Animated API and have the following code:
this.translateValue = new Animated.Value(0)
translate() {
this.translateValue.setValue(0)
Animated.timing(
this.translateValue,
{
toValue: 1,
duration: 14000,
easing: Easing.linear
}
).start(()=>this.translate())
}
const translateBackgroundImage1 = this.translateValue.interpolate({
inputRange: [0, 1],
outputRange: [0, -this.backgroundImageWidth]
})
const translateBackgroundImage2 = this.translateValue.interpolate({
inputRange: [0, 1],
outputRange: [this.backgroundImageWidth, -this.backgroundImageWidth]
})
return (
<View style={{flex:1}}>
<Animated.Image
style={{
flex: 1,
position: 'absolute',
left: translateBackgroundImage1,
}}
resizeMode={Image.resizeMode.cover}
source={this.backgroundImage}
/>
<Animated.Image
style={{
flex: 1,
position: 'absolute',
left: translateBackgroundImage2,
}}
resizeMode={Image.resizeMode.cover}
source={this.backgroundImage}
/>
To apply the same logic I used for setInterval() I would have translateBackgroundImage1 to start at x:0 in the first loop and then starts at x: ImageWidth
I'm not sure how to implement this with Animated
I eventually found a solution. It's not very clean but works.
I essentially have two Animated.image loaded from the same image. Then I have a translateValue1, which controls the left position of the first image. Then based on translateValue1, we have translateValue2 that has an offset of the imagewidth. translateValue2 controls the left position of the second image.
When the first image about to exit from the screen, it will go to the far right of the screen, and at the same time the second image will be moved to the front of the first image, so we need to change the offset to -imagewidth. Therefore there's a setState method in each of the two animation functions.
Inside the constructor I have these variables:
constructor(props) {
super(props);
this.backgroundImage = require('../assets/images/graidenttPastel.jpg');
this.backgroundImageWidth = resolveAssetSource(this.backgroundImage).width;
this.translateXValue1 = new Animated.Value(-1);
this.translateXValue2 = new Animated.Value(0);
this.animationLength = 20000;
this.state = {
translateXValue2Offset: this.backgroundImageWidth,
stopAnimation: false,
}
Then I have these two functions, each controls half of the loop:
translateXFirstHalfLoop() {
this.translateXValue1.setValue(-1);
this.setState({translateXValue2Offset: this.backgroundImageWidth});
this.firstHalfLoop = Animated.timing(
this.translateXValue1,
{
toValue: -this.backgroundImageWidth,
duration: this.animationLength/2,
easing: Easing.linear
}
).start(() => {
if(this.state.stopAnimation === false) {
this.translateXSecondHalfLoop()
}
})
}
translateXSecondHalfLoop() {
this.translateXValue1.setValue(this.backgroundImageWidth);
this.setState({translateXValue2Offset: -this.backgroundImageWidth});
this.secondHalfLoop = Animated.timing(
this.translateXValue1,
{
toValue: 0,
duration: this.animationLength/2,
easing: Easing.linear
}
).start(() => {
if(this.state.stopAnimation === false) {
this.translateXFirstHalfLoop()
}
})
}
Finally in the render() method, I have two Animated.Image like below:
render() {
this.translateXValue2 = Animated.add(this.translateXValue1, this.state.translateXValue2Offset);
return (
<SafeAreaView
style={[{backgroundColor: THEME_COLOR, flex: 1}]}
forceInset={{ bottom: 'never' }}>
<Animated.Image
style={{
position: 'absolute',
left: this.translateXValue1,
}}
resizestate={Image.resizeMode.cover}
source={this.backgroundImage}
/>
<Animated.Image
style={{
position: 'absolute',
left: this.translateXValue2,
}}
resizestate={Image.resizeMode.cover}
source={this.backgroundImage}
/>
<View style={{flex:1}}>
{this._renderScreenContent()}
</View>
</SafeAreaView>
);
}
Since each half loop function calls the other, we need to stop them before this component is unmounted, so we have this additional step below:
//clean up animation first
this.setState({stopAnimation: true}, () => {
this.props.navigation.goBack()
})
If you are looking to animate an ImageBackground, try
var AnimatedImage = Animated.createAnimatedComponent(ImageBackground)

How to set an initial offset for Animated in Reac Native

I have an image and I want to translate it horizontally from imageWidth to -imageWidth on a loop, but I want to have it starts at 0 for the first loop. I've tried to setValue(0) right after setting the timing but it still starts from imageWidth. The code I have now:
this.translateValue = new Animated.Value(0)
translate() {
this.translateValue.setValue(this.backgroundImageWidth)
Animated.timing(
this.translateValue,
{
toValue: -this.backgroundImageWidth,
duration: 20000,
easing: Easing.linear
}
).start(()=>this.translate())
}
componentDidMount() {
this.translate()
this.translateValue.setValue(0)
...
}
render() {
return(
<Animated.Image
style={{
flex: 1,
position: 'absolute',
left: this.translateValue,
}}
resizeMode={Image.resizeMode.cover}
source={this.backgroundImage}
/>
)