How to draw a line with animation by React Native - react-native

I have some graphs and need to show the drawing path.
In the iOS, i can stroke the graph in CALayer with CABasicAnimation
How to do animation for stroking in React Native

To render an animated Line in a chart, you can use a charts library. One of the simplest and best-documented charts libraries in react native is - react-native-gifted-charts
It will not only make the animations easy to implement, but also let you easily customise and beautify your chart. Here is an animated Line chart made using this library. And below is the code for the same-
import { LineChart } from "react-native-gifted-charts"
const App = () => {
const lineData = [
{value: 0, dataPointText: '0'},
{value: 20, dataPointText: '20'},
{value: 18, dataPointText: '18'},
{value: 40, dataPointText: '40'},
{value: 36, dataPointText: '36'},
{value: 60, dataPointText: '60'},
{value: 54, dataPointText: '54'},
{value: 85, dataPointText: '85'}
];
return (
<View style={{backgroundColor: '#1A3461'}}>
<LineChart
initialSpacing={0}
data={lineData}
spacing={30}
textColor1="yellow"
textShiftY={-8}
textShiftX={-10}
textFontSize={13}
thickness={5}
hideRules
hideYAxisText
yAxisColor="#0BA5A4"
showVerticalLines
verticalLinesColor="rgba(14,164,164,0.5)"
xAxisColor="#0BA5A4"
color="#0BA5A4"
/>
</View>
);
};

it is an old question but I post the answer for the reference. You can use Animated from "react-native". you have to consider three directions: Horizontal, Vertical and diagonal. I added the comments to clarify the code
import React, { ReactElement, useRef, useEffect } from "react";
import { StyleSheet, Animated } from "react-native";
// size prop is to determine the size of graph
export default function Line({ size }): ReactElement {
// Pythagorean theorem to calculate the diagonal height
const diagonalHeight = Math.sqrt(Math.pow(size, 2) + Math.pow(size, 2));
// if we dont use ref, animation will be reinitialized everytime we rerender the component
// we want animation to be rendered when component reredners so we use useeffect
const animationRef = useRef<Animated.Value>(new Animated.Value(0));
useEffect(() => {
// timing is a linear animation
Animated.timing(animationRef.current, {
toValue: 1,
duration: 700,
// true might not work with the all properties that you need to animate. true might improve animation performance
useNativeDriver: false
}).start();
}, []);
return (
<>
{conditionForVerticalDisplay && (
// if we are using Animated, it has to be inside Animated.View not View
<Animated.View
style={[
// you can pass multiple style objects
{
// interpolate maps the animated value to another value
height: animationRef.current.interpolate({
inputRange: [0, 1],
outputRange: ["0%", "100%"]
})
}
]}
></Animated.View>
)}
{conditionForHorizontalDisplay && (
<Animated.View
style={[
{
height: animationRef.current.interpolate({
inputRange: [0, 1],
outputRange: ["0%", "100%"]
})
}
]}
></Animated.View>
)}
{ conditionForDiagonalDisplay && (
<Animated.View
style={[
{
height: diagonalHeight,
transform: [
{
// negative will shift it upward.
// translateY: -(diagonalHeight - size) / 2
translateY: animationRef.current.interpolate({
inputRange: [0, 1],
outputRange: [size / 2, -(diagonalHeight - size) / 2]
})
},
{
rotateZ: conditionToPosOrNegDiagonal === "MAIN" ? "-45deg" : "45deg"
}
]
}
]}
></Animated.View>
)}
</>
);
}

Related

How to press a component and it rotates 180 degrees in react native?

I have a Pressable component and inside it I have an icon. I want to press it and rotate it 180 degrees, how can I do that?
So to do this you must take use of the Animated library from react-native.
In which you make animated values and make functions to update them. Here's a full sample of you want (https://snack.expo.dev/#heytony01/grumpy-pretzel) and below is the explantion.
First import the library and make an Animated value
import { Text, View, StyleSheet,Animated,TouchableWithoutFeedback } from 'react-native';
const spinValue = React.useState(new Animated.Value(0))[0]; // Makes animated value
Next define functions to change the value
// When button is pressed in, make spinValue go through and up to 1
const onPressIn = () => {
Animated.spring(spinValue, {
toValue: 1,
useNativeDriver: true,
}).start();
};
// When button is pressed out, make spinValue go through and down to 0
const onPressOut = () => {
Animated.spring(spinValue, {
toValue: 0,
useNativeDriver: true,
}).start();
};
The tricky part is that in order to rotate in react-native you need to pass "0deg" or "12deg" etc..
<View style={{
transform: [
{ rotate: "45deg" },
]
}}>
</View>
So what you do is you interpolate the animated value to "0deg" to "360deg"
// spinDeg will be between '0deg' and '360deg' based on what spinValue is
const spinDeg = spinValue.interpolate({
useNativeDriver: true,
inputRange: [0, 1],
outputRange: ['0deg', '360deg']
})
Lastly you pass in the spinDeg into your button and your done
// The animated style for scaling the button within the Animated.View
const animatedScaleStyle = {
transform: [{rotate: spinDeg}]
};
return (
<View style={{flex:1,justifyContent:"center",alignItems:"center"}}>
<TouchableWithoutFeedback
onPress={()=>{}}
onPressIn={onPressIn}
onPressOut={onPressOut}
>
<View style={{justifyContent:"center",alignItems:"center",backgroundColor:"lightgray",padding:15,borderRadius:20}}>
<Text>PRESS ME</Text>
<Animated.View style={[styles.iconContainer, animatedScaleStyle]}>
<Ionicons name="md-checkmark-circle" size={32} color="green" />
</Animated.View>
</View>
</TouchableWithoutFeedback>
</View>
);

In React Native, how can I adjust translation values when scaling an animated view, so that scaling appears to have an origin at an arbitrary point?

I am using PanGestureHandler and PinchGestureHandler to allow a large image to be panned and zoomed in/out on the screen. However, once I introduce the scaling transformation, panning behaves differently.
I have three goals with this implementation:
I am trying to scale down the image to fit a specific height when the view is first loaded. This is so the user can see as much of the image as possible. If you only apply a scale transformation to the image, translation values of 0 will not put the image in the upper left corner (as a result of the centered scale origin).
I am trying to make it so that when someone uses the pinch gesture to zoom, the translation values are adjusted as well to make it seem as if the zoom origin is at the point where the user initiated the gesture (React Native only currently supports a centered origin for scale transformations). To accomplish this, I assume I will need to adjust the translation values when a user zooms (if the scale is anything other than 1).
When panning after a zoom, I want the pan to track the user's finger (rather than moving faster/slower) by adjusting the translation values as they relate to the scale from the zoom.
Here is what I have so far:
import React, { useRef, useCallback } from 'react';
import { StyleSheet, Animated, View, LayoutChangeEvent } from 'react-native';
import {
PanGestureHandler,
PinchGestureHandler,
PinchGestureHandlerStateChangeEvent,
State,
PanGestureHandlerStateChangeEvent,
} from 'react-native-gesture-handler';
const IMAGE_DIMENSIONS = {
width: 2350,
height: 1767,
} as const;
export default function App() {
const scale = useRef(new Animated.Value(1)).current;
const translateX = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(0)).current;
const setInitialPanZoom = useCallback((event: LayoutChangeEvent) => {
const { height: usableHeight } = event.nativeEvent.layout;
const scaleRatio = usableHeight / IMAGE_DIMENSIONS.height;
scale.setValue(scaleRatio);
// TODO: should these translation values be set based on the scale?
translateY.setValue(0);
translateX.setValue(0);
}, []);
// Zoom
const onZoomEvent = Animated.event(
[
{
nativeEvent: { scale },
},
],
{
useNativeDriver: true,
}
);
const onZoomStateChange = (event: PinchGestureHandlerStateChangeEvent) => {
if (event.nativeEvent.oldState === State.ACTIVE) {
// Do something
}
};
// Pan
const handlePanGesture = Animated.event([{ nativeEvent: { translationX: translateX, translationY: translateY } }], {
useNativeDriver: true,
});
const onPanStateChange = (_event: PanGestureHandlerStateChangeEvent) => {
// Extract offset so that panning resumes from previous location, rather than resetting
translateX.extractOffset();
translateY.extractOffset();
};
return (
<View style={styles.container}>
<PanGestureHandler
minPointers={1}
maxPointers={1}
onGestureEvent={handlePanGesture}
onHandlerStateChange={onPanStateChange}
>
<Animated.View style={styles.imageContainer} onLayout={setInitialPanZoom}>
<PinchGestureHandler onGestureEvent={onZoomEvent} onHandlerStateChange={onZoomStateChange}>
<Animated.View style={{ transform: [{ scale }, { translateY }, { translateX }] }}>
<Animated.Image
source={require('./assets/my-image.png')}
style={{
width: IMAGE_DIMENSIONS.width,
height: IMAGE_DIMENSIONS.height,
}}
resizeMode="contain"
/>
</Animated.View>
</PinchGestureHandler>
</Animated.View>
</PanGestureHandler>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
imageContainer: {
flex: 1,
backgroundColor: 'orange',
width: '100%',
},
});
I have tried something along the lines of subtracting the difference in dimensions from the translation values:
translateX.setValue(0 - (IMAGE_DIMENSIONS.width / 2) - (IMAGE_DIMENSIONS.width * scaleRatio / 2))
The numbers don't quite work with this implementation, so I'm probably not doing this right. Also, this would only account for my first goal, and I am guessing that I will need to do something like interpolate the translation values based on the scale value to accomplish the other two.

React Native - Is there a way to implement a scrolling view inside a PanGestureHandler? (Android and iOS)

I'm trying to build a cross platform scrollable bottom sheet component with react native similar to ones implemented by Apple (Apple Maps), Yelp (Map Search), etc.
I've tried using react-native reanimated-bottom-sheet which allows the scrolling behavior within the bottom sheet, but the main issue is that the bottom sheet does not properly respond to the velocity of the swipe leading to some sub par UX.
Instead I've built a component using PanGestureHandler as seen below. I'd like to implement a scrollable list within the component. Currently a scrollable view will scroll on iOS. Scrolling does not work at all on android for me.
Furthermore, I'd like the ScrollView to pass the swipe to the PanGestureHandler when the list has reached the top. Currently, on iOS, when there is a ScrollView within the PanGestureHandler, the user must use a handle above the ScrollView to drag/swipe the bottom sheet.
I'm sure there's a way to do it, but I've reached a dead end. Any direction or help is appreciated!
import React from 'react';
import { StyleSheet, Dimensions } from 'react-native';
import Animated from 'react-native-reanimated';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import { onGestureEvent, withSpring, clamp } from 'react-native-redash';
const { Value } = Animated;
const screenHeight = Dimensions.get('screen').height;
const BottomSheet = ({ snapTop, snapCenter, snapBottom, renderContent }) => {
const SNAP_TOP = screenHeight * snapTop;
const SNAP_BOTTOM = screenHeight * snapBottom;
const SNAP_CENTER = screenHeight * snapCenter;
const translationY = new Value(0);
const velocityY = new Value(0);
const state = new Value(State.UNDETERMINED);
const offset = new Value(SNAP_TOP);
const gestureHandler = onGestureEvent({
translationY,
state,
velocityY
});
const springConfig = {
damping: 20,
mass: 1,
stiffness: 150,
overshootClamping: false,
restSpeedThreshold: 0.1,
restDisplacementThreshold: 0.1
};
const translateY = clamp(
withSpring({
state,
offset,
value: translationY,
velocity: velocityY,
snapPoints: [SNAP_TOP, SNAP_CENTER, SNAP_BOTTOM],
config: springConfig
}),
SNAP_BOTTOM,
SNAP_TOP
);
return (
<PanGestureHandler {...gestureHandler}>
<Animated.View
style={[
styles.bottomSheet,
{
height: screenHeight,
transform: [{ translateY }]
}
]}
>
{renderContent}
</Animated.View>
</PanGestureHandler>
);
};
const styles = StyleSheet.create({
bottomSheet: {
position: 'absolute',
zIndex: 1,
left: 0,
right: 0,
borderTopRightRadius: 12,
borderTopLeftRadius: 12,
elevation: 3,
shadowColor: 'black',
shadowOpacity: 0.1,
shadowRadius: 5,
shadowOffset: { width: 0, height: -1 },
backgroundColor: '#ffffff'
}
});
export default BottomSheet;

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>
);
}

React Native: Transform with key of "rotate" must be a string: {"rotate":"0deg"}

Seems like I did everything in the documentation and referenced other examples.
Trying to animate a text component rotation:
this.state = {
rotateAnimation: new Animated.Value(0),
};
spin = () => {
this.state.rotateAnimation.setValue(0);
Animated.timing(this.state.rotateAnimation, {
toValue: 1,
duration: 3000,
easing: Easing.linear
}).start((animation) => {
if (animation.finished) {
this.spin();
}
});
};
render() {
return (
<Content>
<View>
<Text style={{
transform: [
{
rotate:
this.state.rotateAnimation.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"]
})
}
]
} ]}>{this.FAIcons.circleONotch}</Text>
</View>
</Content>
);
}
This works fine if I manually enter in any degree i.e rotate: "90deg"
However, when I use interpolate(), I get this error: Transform with key of "rotate" must be a string: {"rotate":"0deg"}
Seems like Interpolate is not returning a string. I tried to typecast it using "toString()" but then I get this error: Rotate transform must be expressed in degrees (deg) or radians (rad): {"rotate":"[object Object]"}
I followed this documentation: https://facebook.github.io/react-native/docs/animations.html
And referenced this example: https://gist.github.com/levynir/5962de39879a0b8eb1a2fd77ccedb2d8
What am I doing wrong here?
**** EDIT ****
Thanks to #Guilherme Cronemberger for pointing me in the right direction, you need to create the component like this.
render() {
const StyledAnimatedText = Animated.createAnimatedComponent(Text);
}
Then utilize it like this:
return (
<StyledAnimatedText
style={{
fontFamily: 'FontAwesome',
backgroundColor: 'transparent',
transform: [{
rotate: this.state.rotateAnimation.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"]
})
},
{ perspective: 1000 }]
}}>
{this.FAIcons.circleONotch}
</StyledAnimatedText>
)
Interpolate is a function which results are used only in declared "Animated" classes, so you'd add "Animated." to your Text class.
render() {
var rotateProp = this.state.rotateAnimation.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"]
})
console.log(rotateProp) //just to check if it's returning what you want
return (
<Content>
<View>
<Animated.Text style={{
transform: [
{
rotate: rotateProp
}
]
} ]}>{this.FAIcons.circleONotch}</Animated.Text>
</View>
</Content>
);
}
I had the same problem
Just use <Animated.Text> instead of <Text />
I had the same issue
I fixed it by using <Animated.SomeComponent> instead of <SomeComponent>
As an example if you want to animate View component :
import { Animated, View } from 'react-native';
<Animated.View>
<ChildComponents/>
</Animated.View>
Thanks.
I had the same problem and I fix it by doing this.Convert PI in degree = (PI = 180deg) & (2 * PI= 360geg),
The answer is :
{ rotate: progress.interpolate(
{
inputRange: [0.5, 1],
outputRange: ['180deg', '360deg'],
}
),
},