I want to rotate a picture using Animated.timing but assigning a variable containing a number doesnt animate anything. Putting in int like 50, works.
toValue: degree does nothing, toValue:50 does what I need. I print out degree inside Text block like it should.
I put animated.timing inside render() so that I can access degree const directly, I tried putting it in componentDidMount() but that just generated errors.
This code prints out x y z and calculated degree in xxx.xx format, I set up xyz in state and later use it to pass it to compassHeading function to calculate degree:
render() {
const movementX = this.state.x;
const movementY = this.state.y;
const movementZ = this.state.z;
const degree= compassHeading(movementX, movementY, movementZ).toFixed(2);
Animated.timing(
this.state.spinValue,
{
toValue: degree,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true
}
)
.start();
return (
<View >
<Animated.Image
style={{
width: 350, height: 350,
transform: [{
rotate: this.state.spinValue.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
}) }] }}
source={require('./compass.png')}
/>
<Text> {movementX}</Text>
<Text> {movementY}</Text>
<Text> {movementZ}</Text>
<Text> {degree}</Text>
</View>
);
}
}
You start a new animation in every render and each animation will renders loads of time to vary this.state.spinValue for your animation and each render you start the animation again ...
You need to start the Animated.timing outside of the render function and depends on how often the movementXYZ change.(let say they change every 100ms) you may need to wait the animation finish, before you start another one. While it is animating, if any new movement coming in, store the latest value for the next animation. (I dont' suggest interrupting the on going animation too frequent, particularly using nativeDriver....so I let it finish one first.)
It look something like:
onMovementChange(){
const degree= compassHeading(movementX, movementY, movementZ).toFixed(2);
if(!this.state.isAnimating){
this.setState({isAnimating:true,nextDegree:degree});
Animated.timing(
this.state.spinValue,
{
toValue: degree,
duration: 3000,
easing: Easing.linear,
useNativeDriver: true
}
).start(()=>{
if(this.state.nextDegree!=degree){
onMovementChange()
}
this.setState({isAnimating:false})
});
}else{
if(this.state.nextDegree!=degree){
this.setState({nextDegree:degree});
}
}
}
render() {
const movementX = this.state.x;
const movementY = this.state.y;
const movementZ = this.state.z;
return (
<View >
<Animated.Image
style={{
width: 350, height: 350,
transform: [{
rotate: this.state.spinValue.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
}) }] }}
source={require('./compass.png')}
/>
<Text> {movementX}</Text>
<Text> {movementY}</Text>
<Text> {movementZ}</Text>
<Text> {this.state.nextDegree}</Text>
</View>
);
}
Related
(Using React obviously + Gatsby)
I have a hamburger button that gonna open a nav-menu in my website.
I wanted to know how to make the menu open with an animation using Framer Motion.
you could use this method that is given in the examples section of the framer motion documentation.
Framer Motion API Documentation
import { motion } from "framer-motion"
const variants = {
open: { opacity: 1, x: 0 },
closed: { opacity: 0, x: "-100%" },
}
export const MyComponent = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.nav
animate={isOpen ? "open" : "closed"}
variants={variants}
>
'Menu Content'
</motion.nav>
)
}
<MonkeyPic rotate={showFistBump} />
// ...
// Then switch animation variant depending on rotate prop
const variants = {
rotate: { rotate: [0, -30, 0], transition: { duration: 0.5 } },
// You can do whatever you want here, if you just want it to stop completely use `rotate: 0`
stop: { y: [0, -10, 0], transition: { repeat: Infinity, repeatDelay: 3 } }
};
const MonkeyPic = ({ rotate }) => {
return (
<div>
<motion.img
variants={variants}
animate={rotate ? 'rotate' : 'stop'}
id="monkeyFace"
src="/images/Monkey.png"
/>
</div>
);
};
Is there any built-in way to make the animation start when the element is in-view (for example, when we scroll to the element)?
Framer Motion has mount animations section which says:
When a component mounts, it'll automatically animate to the values in animate if they're different from those defined in style or initial
So I couldn't really find a straight forward way to make the animation start when it comes into view.
However, I reached the only option I see for now is using Animation Controls which means I'll have to implement a listener on the scroll manually and trigger the control.start(), any easier ways are much appreciated.
framer-motion has built-in support for this use case since version 5.3.
Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-5-3-94j13
Relevant code:
function FadeInWhenVisible({ children }) {
return (
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ duration: 0.3 }}
variants={{
visible: { opacity: 1, scale: 1 },
hidden: { opacity: 0, scale: 0 }
}}
>
{children}
</motion.div>
);
}
Usage:
<FadeInWhenVisible>
<Box />
</FadeInWhenVisible>
Previous versions:
You can currently use the imperative animation controls to achieve this effect. Intersection observers are useful to detect if an element is currently visible.
Here's a CodeSandbox demonstrating the pattern: https://codesandbox.io/s/framer-motion-animate-in-view-gqcc8.
Relevant code:
function FadeInWhenVisible({ children }) {
const controls = useAnimation();
const [ref, inView] = useInView();
useEffect(() => {
if (inView) {
controls.start("visible");
}
}, [controls, inView]);
return (
<motion.div
ref={ref}
animate={controls}
initial="hidden"
transition={{ duration: 0.3 }}
variants={{
visible: { opacity: 1, scale: 1 },
hidden: { opacity: 0, scale: 0 }
}}
>
{children}
</motion.div>
);
}
I am trying to run a few simple animations using react-native-animatable library. (But I believe the question should be generic to any react animations so adding other tags as well.)
The problem is, in the first time, the image animates just as expected. But when aimed to start second animation animation with the gesture, the image translation starts from its original coordinates.
A search yielt, in Android development (which is obviously not my case) there seems a method, setFillAfter which sets the coordinate after the animation.
My question is, how to set the location (left / top values for example) to the final translated point so that consecutive animation starts from the point the previous translation left.
The expo snack for below code block is here.
import * as React from 'react';
import { Image, StyleSheet, ImageBackground } from 'react-native';
import * as Animatable from 'react-native-animatable';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import testImg from './test.png';
import backImg from './back.png';
export default class App extends React.Component {
onTestMove(event) {
this.testAnimRef.transitionTo({
translateX: event.nativeEvent.translationX,
translateY: event.nativeEvent.translationY,
}, 0);
}
render() {
return (
<ImageBackground source={backImg} style={{ flex: 1 }} >
<PanGestureHandler
key={`test`}
onGestureEvent={(e) => { this.onTestMove(e) }}
onHandlerStateChange={e => { }}
>
<Animatable.View style={styles._animatable_view}
ref={((ref) => { this.testAnimRef = ref }).bind(this)}
useNativeDriver={true}
>
<Image source={testImg} style={styles._image} />
</Animatable.View>
</PanGestureHandler>
</ImageBackground>
);
}
}
const styles = StyleSheet.create({
_image: {
width: 50,
height: 25,
resizeMode: 'contain',
backgroundColor: 'black',
borderColor: 'gainsboro',
borderWidth: 2,
},
_animatable_view: {
position: "absolute",
top: 200,
left: 100,
},
});
I had the same problem trying to move around some cards in a view, and upon further dragging, they would reset to their origin.
My theory is/was that while the translated view would have its x / y coordinates translated, this would not apply to the parent of that view, and so the animated event passed from that component would initially have the original coordinates (nuke me if I'm wrong here)
So my solution was to keep an initial offset value in state, and maintain this every time the user releases the dragged motion
_onHandleGesture: any
constructor(props: OwnProps) {
super(props)
this.state = {
animationValue: new Animated.ValueXY({ x: 0, y: 0 }),
initialOffset: { x: 0, y: 0 },
}
this._onHandleGesture = (e: PanGestureHandlerGestureEvent) => {
this.state.animationValue.setValue({
x: e.nativeEvent.translationX + this.state.initialOffset.x, <- add initial offset to coordinates passed
y: e.nativeEvent.translationY + this.state.initialOffset.y,
})
}
}
_acceptCard = (cardValue: number) => {
const { targetLocation, onAccept } = this.props
const { x, y } = targetLocation
onAccept(cardValue)
Animated.spring(this.state.animationValue, {
// Some animation here
}).start(() => {
this.setState({ initialOffset: targetLocation }) // <- callback to set state value for next animation start
})
}
and the render method
<PanGestureHandler
onHandlerStateChange={this.onPanHandlerStateChange}
onGestureEvent={this._onHandleGesture}
failOffsetX={[-xThreshold, xThreshold]}
>
<Animated.View
style={{
position: "absolute",
left: 0,
top: 0,
transform: [{ translateX: this.state.animationValue.x }, { translateY: this.state.animationValue.y }],
}}
>
<CardTile size={size} content={content} layout={layout} backgroundImage={backgroundImage} shadow={shadow} />
</Animated.View>
</PanGestureHandler>
This example is based on the react-native-gesture-handler library, but the concept should apply to other solutions.
Dont know if this way is advisable, though it is functional.
Hope this helps!
I have a FlatList with an onScroll function that looks like this:
<Animated.View style={{ transform: [{translateX: this.state.scrollX}] }}>
<FlatList
data={ reactionGroup.reactions }
keyExtractor={ (i) => i.id + uuid.v4() }
renderItem={ this._renderItem }
horizontal={ true }
scrollEventThrottle={1}
onScroll={ reactionGroup.reactions.length > 1 ? this.onScroll : null }
showsHorizontalScrollIndicator={ false } />
</Animated.View>
onScroll(event) {
const { timing } = Animated
if (event.nativeEvent.contentOffset.x > 0) {
timing(
this.state.scrollX,
{ toValue: -60, duration: 100, useNativeDriver: true }
).start()
} else {
timing(
this.state.scrollX,
{ toValue: 0, duration: 100, useNativeDriver: true }
).start()
}
},
This works great on iOS, but for Android the animation won't start until the FlatList has stopped scrolling.
I'm basically just kicking off an animation when the user scrolls and setting it back when they go back to the beginning of the horizontal scroll.
Is there a better way to handle this so it works on Android?
I also tried doing Animation.event inside onScroll, but I don't want to tie the animation directly to the scroll position. This approach allowed Android to animate while scrolling, but it was pretty jittery.
RN: 0.43.3
You should use the Animated.event approach. As seen in the example in the docs, it maps the event.nativeEvent for you.
Here's a blogpost with an example of how to animate the nav header on scroll by a RN contributor:
https://medium.com/appandflow/react-native-collapsible-navbar-e51a049b560a
Hope it helps :-)
I'm unable to animate the borderRadius property in a ReactNative Image, it seems to only re-render the image as the animation completes. It fades out on animation start, and fades back in on animation completion. This only happens on Android; on iOS the animation plays properly.
I am trying to animate a circle expanding into a square by animating the borderRadius:
constructor(props) {
super(props);
this.state = {
borderRadius: new Animated.Value(ALBUM_CIRCLE_DIAMETER /2)
};
}
_zoomIn = () => {
Animated.timing(
this.state.borderRadius,
{
toValue: 0,
duration: ZOOM_ANIMATION_DURATION_MS,
easing: Easing.linear
}
).start()
}
And the markup:
<Animated.Image
style={[
styles.albumArtCircle,
{ width: this.state.albumArtWidth },
{ height: this.state.albumArtHeight },
{ borderRadius: this.state.borderRadius },
]}
resizeMode='contain'
source={require('../images/sampleAlbum.jpg')}>
</Animated.Image>
Right! Remove resizeMode property. This will solve your problem
Removing resizeMode='contain' made this work.