I have a button at the middle of my screen. onScroll I want the button to scale down to 0 to disappear and then scale back up to 1 to reappear in a new position at the bottom of the screen. I want to be able call setState (which controls the position of the button) in between the scale down and scale up animations. Something like the code below. Any idea of the best way to add a function call in between these two animations? Or an even better way of doing this?
animateScale = () => {
return (
Animated.sequence([
Animated.timing(
this.state.scale,
{
toValue: 0,
duration: 300
}
),
this.setState({ positionBottom: true }),
Animated.timing(
this.state.scale,
{
toValue: 1,
duration: 300
}
)
]).start()
)
}
After more research I found the answer.start() takes a callback function as shown here:
Calling function after Animate.spring has finished
Here was my final solution:
export default class MyAnimatedScreen extends PureComponent {
state = {
scale: new Animated.Value(1),
positionUp: true,
animating: false,
};
animationStep = (toValue, callback) => () =>
Animated.timing(this.state.scale, {
toValue,
duration: 200,
}).start(callback);
beginAnimation = (value) => {
if (this.state.animating) return;
this.setState(
{ animating: true },
this.animationStep(0, () => {
this.setState(
{ positionUp: value, animating: false },
this.animationStep(1)
);
})
);
};
handleScrollAnim = ({ nativeEvent }) => {
const { y } = nativeEvent.contentOffset;
if (y < 10) {
if (!this.state.positionUp) {
this.beginAnimation(true);
}
} else if (this.state.positionUp) {
this.beginAnimation(false);
}
};
render() {
return (
<View>
<Animated.View
style={[
styles.buttonWrapper,
{ transform: [{ scale: this.state.scale }] },
this.state.positionUp
? styles.buttonAlignTop
: styles.buttonAlignBottom,
]}
>
<ButtonCircle />
</Animated.View>
<ScrollView onScroll={this.handleScrollAnim}>
// scroll stuff here
</ScrollView>
</View>
);
}
}
That is correct answer.
Tested on Android react-native#0.63.2
Animated.sequence([
Animated.timing(someParam, {...}),
{
start: cb => {
//Do something
console.log(`I'm wored!!!`)
cb({ finished: true })
}
},
Animated.timing(someOtherParam, {...}),
]).start();
Related
How can I create a reusable React hook with animation style with Reanimated 2? I have an animation that is working on one element, but if I try to use the same animation on multiple elements on same screen only the first one registered is animating. It is too much animation code to duplicate it everywhere I need this animation, so how can I share this between multiple components on the same screen? And tips for making the animation simpler is also much appreciated.
import {useEffect} from 'react';
import {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withSequence,
withTiming,
} from 'react-native-reanimated';
const usePulseAnimation = ({shouldAnimate}: {shouldAnimate: boolean}) => {
const titleOpacity = useSharedValue(1);
const isAnimating = useSharedValue(false);
useEffect(() => {
if (shouldAnimate && !isAnimating.value) {
isAnimating.value = true;
titleOpacity.value = withRepeat(
withSequence(
withTiming(0.2, {duration: 700, easing: Easing.inOut(Easing.ease)}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
if (!shouldAnimate) {
cancelAnimation(titleOpacity);
}
},
),
),
-1,
false,
() => {
if (titleOpacity.value < 1) {
titleOpacity.value = withSequence(
withTiming(0.2, {
duration: 700,
easing: Easing.inOut(Easing.ease),
}),
withTiming(
1,
{duration: 700, easing: Easing.inOut(Easing.ease)},
() => {
isAnimating.value = false;
},
),
);
} else {
titleOpacity.value = withTiming(
1,
{
duration: 700,
easing: Easing.inOut(Easing.ease),
},
() => {
isAnimating.value = false;
},
);
}
},
);
} else {
isAnimating.value = false;
cancelAnimation(titleOpacity);
}
}, [shouldAnimate, isAnimating, titleOpacity]);
const pulseAnimationStyle = useAnimatedStyle(() => {
return {
opacity: titleOpacity.value,
};
});
return {pulseAnimationStyle, isAnimating: isAnimating.value};
};
export default usePulseAnimation;
And I am using it like this inside a component:
const {pulseAnimationStyle} = usePulseAnimation({
shouldAnimate: true,
});
return (
<Animated.View
style={[
{backgroundColor: 'white', height: 100, width: 100},
pulseAnimationStyle,
]}
/>
);
The approach that I've taken is to write my Animations as wrapper components.
This way you can build up a library of these animation components and then simply wrap whatever needs to be animated.
e.g.
//Wrapper component type:
export type ShakeProps = {
// Animation:
children: React.ReactNode;
repeat?: boolean;
repeatEvery?: number;
}
// Wrapper component:
const Shake: FC<ShakeProps> = ({
children,
repeat = false,
repeatEvery = 5000,
}) => {
const shiftY = useSharedValue(0);
const animatedStyles = useAnimatedStyle(() => ({
//Animation properties...
}));
const shake = () => {
//Update shared values...
}
// Loop every X seconds:
const repeatAnimation = () => {
shake();
setTimeout(() => {
repeatAnimation();
}, repeatEvery);
}
// Start Animations on component Init:
useEffect(() => {
// Run animation continously:
if(repeat){
repeatAnimation();
}
// OR ~ call once:
else{
shake();
}
}, []);
return (
<Animated.View style={[animatedStyles]}>
{children}
</Animated.View>
)
}
export default Shake;
Wrapper Component Usage:
import Shake from "../../util/animated-components/shake";
const Screen: FC = () => {
return (
<Shake repeat={true} repeatEvery={5000}>
{/* Whatever needs to be animated!...e.g. */}
<Text>Hello World!</Text>
</Shake>
)
}
From their docs:
CAUTION
Animated styles cannot be shared between views.
To work around this you can generate multiple useAnimatedStyle in top-level loop (number of iterations must be static, see React's Rules of Hooks for more information).
https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
I am trying to animate the changing of data in my Victory Native Pie Chart. I fetch the data from an API on a recurring timeout set up in the ComponentDidMount method. Once the data is retrieved I set it to a state variable which is passed to the data prop in the VictoryPie component with the animate props enabled as the docs show.
I am following this article and the Victory Chart docs for animations but mine does not behave in the same way as these examples.
Currently it sets the data correctly but without any smooth animation. It instantly jumps from initial state value to the fetched data value. Only time I see an animation is when the fetched data returns a zero value after the previous fetch had a value that wasn't zero.
export default class HaloXPChart extends Component {
constructor(props) {
super(props);
this.state = {
gamertag: this.props.gamertag ? this.props.gamertag : '',
dateRetrievalInterval: 1,
totalXp: 0,
startXp: 0,
spartanRank: 0,
xpChartData: [
{x: 'xp earned', y: 0},
{x: 'xp remaining', y: 1}
],
loading: false
};
}
componentDidMount() {
this.setState({loading: true});
this.recursiveGetData();
}
async recursiveGetData(){
this.setState({indicator: true}, async () => {
await this.getData()
this.setState({indicator: false});
let timeout = setTimeout(() => {
this.recursiveGetData();
}, this.state.dateRetrievalInterval * 60 * 1000);
});
}
async getData(){
await Promise.all([
fetch('https://www.haloapi.com/stats/h5/servicerecords/arena?players=' + this.state.gamertag, fetchInit),
fetch('https://www.haloapi.com/metadata/h5/metadata/spartan-ranks', fetchInit)
])
.then(([res1, res2]) => {
return Promise.all([res1, res2.json()])
}).then(([res1, res2) => {
const xp = res1.Results[0].Result.Xp;
const spartanRank = res1.Results[0].Result.SpartanRank;
this.setState({totalXp: xp});
const currentRank = res2.filter(r => r.id == this.state.spartanRank);
this.setState({startXp: currentRank[0].startXp});
this.setState({loading: false}, () => {
this.setState({xpChartData: [
{x: 'xp earned', y: this.state.totalXp - this.state.startXp},
{x: 'xp remaining', y: 15000000 - (this.state.totalXp - this.state.startXp)}
]});
});
})
.catch((error) => {
console.log(JSON.stringify(error));
})
}
render() {
return(
<View>
<Svg viewBox="0 0 400 400" >
<VictoryPie
standalone={false}
width={400} height={400}
data={this.state.xpChartData}
animate={{
easing: 'exp',
duration: 2000
}}
innerRadius={120} labelRadius={100}
style={{
labels: { display: 'none'},
data: {
fill: ({ datum }) =>
datum.x === 'xp earned' ? '#00f2fe' : 'black'
}
}}
/>
</Svg>
</View>
);
}
}
Try setting endAngle to 0 initially and set it to 360 when you load the data as described in this GitHub issue:
Note: Pie chart animates when it's data is changed. So set the data initially empty and set the actual data after a delay. If your data is coming from a service call it will do it already.
const PieChart = (props: Props) => {
const [data, setData] = useState<Data[]>([]);
const [endAngle, setEndAngle] = useState(0);
useEffect(() => {
setTimeout(() => {
setData(props.pieData);
setEndAngle(360);
}, 100);
}, []);
return (
<VictoryPie
animate={{
duration: 2000,
easing: "bounce"
}}
endAngle={endAngle}
colorScale={props.pieColors}
data={data}
height={height}
innerRadius={100}
labels={() => ""}
name={"pie"}
padding={0}
radius={({ datum, index }) => index === selectedIndex ? 130 : 120}
width={width}
/>
)
I have a Wave component that unmounts as soon as it goes out of bounds.
This is my result so far: https://streamable.com/hmjo6k
I would like to have multiple Waves spawn every 300ms, I've tried implementing this using setInterval inside useEffect, but nothing behaves like expected and app crashes
const Wave = ({ initialDiameter, onOutOfBounds }) => {
const scaleFactor = useRef(new Animated.Value(1)).current,
opacity = useRef(new Animated.Value(1)).current
useEffect(() => {
Animated.parallel(
[
Animated.timing(scaleFactor, {
toValue: 8,
duration: DURATION,
useNativeDriver: true
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: DURATION,
useNativeDriver: true
})
]
).start(onOutOfBounds)
})
return (
<Animated.View
style={
{
opacity,
backgroundColor: 'white',
width: initialDiameter,
height: initialDiameter,
borderRadius: initialDiameter/2,
transform: [{scale: scaleFactor}]
}
}
/>
)
}
export default Wave
const INITIAL_WAVES_STATE = [
{ active: true },
]
const Waves = () => {
const [waves, setWaves] = useState(INITIAL_WAVES_STATE),
/* When out of bounds, a Wave will set itself to inactive
and a new one is registered in the state.
Instead, I'd like to spawn a new Wave every 500ms */
onWaveOutOfBounds = index => () => {
let newState = waves.slice()
newState[index].active = false
newState.push({ active: true })
setWaves(newState)
}
return (
<View style={style}>
{
waves.map(({ active }, index) => {
if (active) return (
<Wave
key={index}
initialDiameter={BUTTON_DIAMETER}
onOutOfBounds={onWaveOutOfBounds(index)}
/>
)
else return null
})
}
</View>
)
}
export default Waves
I think the useEffect cause the app crashed. Try to add dependencies for the useEffect
useEffect(() => {
Animated.parallel(
[
Animated.timing(scaleFactor, {
toValue: 8,
duration: DURATION,
useNativeDriver: true
}),
Animated.timing(opacity, {
toValue: 0.3,
duration: DURATION,
useNativeDriver: true
})
]
).start(onOutOfBounds)
}, [])
In my React Native app, I have buttons that you can drag into a drop zone. If a certain condition is met by the drop zone, the data in the drop zone will change, but if it's not met, I would like the draggable return to its original position. Maybe I'm missing it, but there doesn't seem to be a property that retains the original position. Is there a simple way of doing this? I'm sure I can find a roundabout way of doing this, but it seems like I'm missing something.
Can anyone help? Below is the component I'm working with.
class AddTile extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
showDraggable : true,
dropZoneValues : null,
moveValues: null,
showView: true,
};
}
_onPressButton() {
Alert.alert('You tapped the button!')
}
removeButton() {
this.setState({
showView: false,
});
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder : () => true,
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 3 }
).start();
},
// When we drag/pan the object, set the delate to the states pan position
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: ({ identifier, target, pageX, pageY }, {moveX, moveY, x0, y0, dx, dy, vx, vy}) => {
// Flatten the offset to avoid erratic behavior
this.state.pan.flattenOffset();
this.props.movedTile( { title: this.props.tileProps, moveX, moveY } );
**//I want to add a check here that will return to original position, but not sure best way to get or retain coordinates for original position**
this.removeButton();
}
});
}
render() {
// Destructure the value of pan from the state
let { pan, scale } = this.state;
// Calculate the x and y transform from the pan value
let [translateX, translateY] = [pan.x, pan.y];
let rotate = '0deg';
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let tileStyle = {
flex: 0,
borderRadius: 4,
flexDirection: 'row',
justifyContent: 'center',
flexWrap: 'wrap',
transform: [ {translateX}, {translateY}, {rotate}, {scale} ]
};
return (
<Animated.View style={ tileStyle } { ...this._panResponder.panHandlers } >
{this.state.showView && (
<Button
id={this.props.key}
onPress={this._onPressButton}
title={this.props.tileProps}
/>
)}
</Animated.View>
);
}
}
Im trying to make two circles that can drag and drop with react-native.
I could have created one circle that can drag and drop, but dont know how with two circles individually.
here is the code for one circle that can drag and drop,
constructor(props){
super(props);
this.state = {
pan : new Animated.ValueXY() //Step 1
};
this.panResponder = PanResponder.create({ //Step 2
onStartShouldSetPanResponder : () => true,
onPanResponderMove : Animated.event([null,{ //Step 3
dx : this.state.pan.x,
dy : this.state.pan.y
}]),
onPanResponderRelease : (e, gesture) => {} //Step 4
});
}
and this is for image
renderDraggable(){
return (
<View style={styles.draggableContainer}>
<Animated.View
{...this.panResponder.panHandlers}
style={[this.state.pan.getLayout(), styles.circle]}>
<Text style={styles.text}>Drag me!</Text>
</Animated.View>
</View>
);
}
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Image, // we want to use an image
PanResponder, // we want to bring in the PanResponder system
Animated // we wil be using animated value
} from 'react-native';
export default class MovingCircle extends React.Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1)
};
}
_handleStartShouldSetPanResponder(e, gestureState) {
return true;
}
_handleMoveShouldSetPanResponder(e, gestureState) {
return true;
}
componentWillMount() {
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder:
this._handleStartShouldSetPanResponder.bind(this),
onMoveShouldSetPanResponder:
this._handleMoveShouldSetPanResponder.bind(this),
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 30*Math.random(), y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 1 }
).start();
},
// When we drag/pan the object, set the delate to the states pan position
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, {vx, vy}) => {
// Flatten the offset to avoid erratic behavior
this.state.pan.flattenOffset();
Animated.spring(
this.state.scale,
{ toValue: 1, friction: 1 }
).start();
}
});
}
render() {
// Destructure the value of pan from the state
let { pan, scale } = this.state;
// Calculate the x and y transform from the pan value
let [translateX, translateY] = [pan.x, pan.y];
let rotate = '0deg';
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let imageStyle = {transform: [{translateX}, {translateY}, {rotate}, {scale}]};
return (
<Animated.View style={[imageStyle, styles.container]} {...this._panResponder.panHandlers} >
<View style={styles.rect}>
<Text style={styles.txt} >tgyyHH</Text>
</View>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
container: {
width:50,
height:50,
position: 'absolute'
},
rect: {
borderRadius:4,
borderWidth: 1,
borderColor: '#fff',
width:50,
height:50,
backgroundColor:'#68a0cf',
},
txt: {
color:'#fff',
textAlign:'center'
}
});
Here is how made items independent of each other. This example is in typescript, but should be clear enough to convert to pure javascript. The main idea here is that each animated item needs its own PanResponderInstance and once you update the items, you need to also refresh the PanResponderInstance
interface State {
model: Array<MyAnimatedItem>,
pans: Array<Animated.ValueXY>,
dropZone1: LayoutRectangle,
dropZone2: LayoutRectangle,
}
public render(): JSX.Element {
const myAnimatedItems = new Array<JSX.Element>()
for (let i = 0; i < this.state.model.length; i++) {
const item = this.state.model[i]
const inst = this.createResponder(this.state.pans[i], item)
myAnimatedItems.push(
<Animated.View
key={'item_' + i}
{...inst.panHandlers}
style={this.state.pans[i].getLayout()}>
<Text>{item.description}</Text>
</Animated.View>
)
}
return (
<View>
<View onLayout={this.setDropZone1} style={styles.dropZone}>
<View style={styles.draggableContainer}>
{myAnimatedItems}
</View>
</View>
<View onLayout={this.setDropZone2} style={styles.dropZone}>
<View style={styles.draggableContainer}>
...
</View>
</View>
</View>
)
}
private setDropZone1 = (event: LayoutChangeEvent): void => {
this.setState({
dropZone1: event.nativeEvent.layout
})
}
private setDropZone2 = (event: LayoutChangeEvent): void => {
this.setState({
dropZone2: event.nativeEvent.layout
})
}
private isDropZone(gesture: PanResponderGestureState, dropZone: LayoutRectangle): boolean {
const toolBarHeight = variables.toolbarHeight + 15 // padding
return gesture.moveY > dropZone.y + toolBarHeight
&& gesture.moveY < dropZone.y + dropZone.height + toolBarHeight
&& gesture.moveX > dropZone.x
&& gesture.moveX < dropZone.x + dropZone.width
}
private createResponder(pan: Animated.ValueXY, item: MyAnimatedItem): PanResponderInstance {
return PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, {
dx: pan.x,
dy: pan.y
}]),
onPanResponderRelease: (_e, gesture: PanResponderGestureState) => {
const model = this.state.model
const pans = this.state.pans
const idx = model.findIndex(x => x.id === item.id)
if (this.isDropZone(gesture, this.state.dropZone1)) {
... // do something with the item if needed
// reset each PanResponderInstance
for (let i = 0; i < model.length; i++) {
pans[i] = new Animated.ValueXY()
}
this.setState({ model: model, pans: pans })
return
}
} else if (this.isDropZone(gesture, this.state.dropZone2)) {
... // do something with the item if needed
// reset each PanResponderInstance
for (let i = 0; i < model.length; i++) {
pans[i] = new Animated.ValueXY()
}
this.setState({ model: model, pans: pans })
return
}
Animated.spring(pan, { toValue: { x: 0, y: 0 } }).start()
this.setState({ scrollEnabled: true })
}
})
}