Reanimated 2 reusable animation in custom hook - react-native

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

Related

Reusing custom TextInput component in react native?

I am using react native 0.68.5
When I call the renderReusableTextInput method in Main.js I am sending all the styles including the needed and unneeded styles. This can lead performance penalty.
So, is there a way that I can send the only needed styles to the renderReusableTextInput method.
I also want to know is the code reusing approach of mine correct or there are better ways.
I have the following code-logic structure:
(i) A Reusable component; It has only structure no style or state
(ii) A file that includes reusable methods
(iii) The Main component, which contains components, styles and states
ReusableTextInput.js
// import ...
const ReusableTextInput = forwardRef(({
inputName,
inputLabel,
inputValue,
secureTextEntry,
textInputContainerWarperStyle,
textInputContainerStyle,
textInputLabelStyle,
textInputStyle,
textInputHelperStyle,
textInputErrorStyle,
helperText,
inputError,
onFocus,
onChangeText,
onBlur,
}, inputRef) => {
return (
<View style={[textInputContainerWarperStyle]}>
<View style={[textInputContainerStyle]}>
<Text style={[textInputLabelStyle]}>
{inputLabel}
</Text>
<TextInput
ref={(elm) => inputRef[inputName] = elm}
style={[textInputStyle]}
value={inputValue}
secureTextEntry={secureTextEntry}
onChangeText={onChangeText}
onFocus={onFocus}
onBlur={onBlur}
/>
</View>
{
((inputRef[inputName]) && (inputRef[inputName].isFocused()) && (!inputValue))
?
<Text style={[textInputHelperStyle]}>
{helperText}
</Text>
:
null
}
{
((inputError) && (inputValue))
?
<Text style={[textInputErrorStyle]}>
{inputError}
</Text>
:
null
}
</View>
);
});
export default memo(ReusableTextInput);
reusableMethods.js
// import ...
const handleFocus = (state, setState, styles) => {
const stateData = { ...state };
stateData.styleNames.textInputContainer = {
...styles.textInputContainer,
...styles[`${stateData.name}ExtraTextInputContainer`],
...styles.textInputContainerFocus,
};
stateData.styleNames.textInputLabel = {
...styles.textInputLabel,
...styles[`${stateData.name}ExtraTextInputLabel`],
...styles.textInputLabelFocus,
};
stateData.styleNames.textInput = {
...styles.textInput,
...styles[`${stateData.name}ExtraTextInput`],
...styles.textInputFocus,
};
// other logics...
setState(stateData);
};
const handleChangeText = (state, setState, text) => {
const stateData = { ...state };
// individual validation
const schemaData = Joi.object().keys(stateData.validationObj); // I used Joi for validation
const inputData = { [stateData.name]: text };
const options = { abortEarly: false, errors: { label: false } };
const result = schemaData.validate(inputData, options);
// -----
stateData.error = (result.error) ? result.error.details[0].message : '';
stateData.value = text;
// other logics...
setState(stateData);
};
const handleBlur = (state, setState, styles) => {
const stateData = { ...state };
if (stateData.value) {
stateData.styleNames.textInputContainer = {
...styles.textInputContainer,
...styles[`${stateData.name}ExtraTextInputContainer`],
...styles.textInputContainerFocus,
...styles[`${stateData.name}ExtraTextInputContainerFocus`],
...styles.textInputContainerBlurText,
...styles[`${stateData.name}ExtraTextInputContainerBlurText`],
};
stateData.styleNames.textInputLabel = {
...styles.textInputLabel,
...styles[`${stateData.name}ExtraTextInputLabel`],
...styles.textInputLabelFocus,
...styles[`${stateData.name}ExtraTextInputLabelFocus`],
...styles.textInputLabelBlurText,
...styles[`${stateData.name}ExtraTextInputLabelBlurText`],
};
stateData.styleNames.textInput = {
...styles.textInput,
...styles[`${stateData.name}ExtraTextInput`],
...styles.textInputFocus,
...styles[`${stateData.name}ExtraTextInputFocus`],
...styles.textInputBlurText,
...styles[`${stateData.name}ExtraTextInputBlurText`],
};
}
else {
stateData.styleNames.textInputContainer = { ...styles.textInputContainer, ...styles[`${stateData.name}ExtraTextInputContainer`] };
stateData.styleNames.textInputLabel = { ...styles.textInputLabel, ...styles[`${stateData.name}ExtraTextInputLabel`] };
stateData.styleNames.textInput = { ...styles.textInput, ...styles[`${stateData.name}ExtraTextInput`] };
}
// other logics...
setState(stateData);
};
// other methods...
export const renderReusableTextInput = (
state,
setState,
inputRef,
styles,
// contains all the styles from Main component and here I am sending all the styles including the needed and unneeded styles. I want improvement here
) => {
return (
<ReusableTextInput
inputName={state.name}
inputLabel={state.label}
inputValue={state.value}
inputRef={inputRef}
secureTextEntry={state.secureTextEntry}
textInputContainerWarperStyle={{...styles.textInputContainerWarper, ...styles[`${state.name}ExtraTextInputContainerWarper`]}}
textInputContainerStyle={state.styleNames.textInputContainer}
textInputLabelStyle={state.styleNames.textInputLabel}
textInputStyle={state.styleNames.textInput}
textInputHelperStyle={{...styles.textInputHelper, ...styles[`${state.name}ExtraTextInputHelper`]}}
textInputErrorStyle={{...styles.textInputError, ...styles[`${state.name}ExtraTextInputError`]}}
helperText={state.helperText}
inputError={state.error}
onFocus={() => handleFocus(state, setState, styles)}
onChangeText={(text) => handleChangeText(state, setState, text)}
onBlur={() => handleBlur(state, setState, styles)}
/>
);
};
Main.js
// import Joi from 'joi';
// import { joiPasswordExtendCore } from 'joi-password';
// import { renderReusableTextInput } from ''; ...
const schema =
{
email: Joi.string().strict()
.case("lower")
.min(5)
.max(30)
.email({ minDomainSegments: 2, tlds: { allow: ["com", "net", "org"] } })
.required(),
countryCode: // Joi.string()...,
phoneNumber: // Joi.string()...,
password: // Joi.string()...,
// ...
};
const Main = () => {
const { width: windowWidth, height: windowHeight, scale, fontScale } = useWindowDimensions();
const minimumWidth = (windowWidth <= windowHeight) ? windowWidth : windowHeight;
const styles = useMemo(() => currentStyles(minimumWidth), [minimumWidth]);
const [email, setEmail] = useState({
name: 'email', // unchangeable
label: 'Email', // unchangeable
value: '',
error: '',
validationObj: { email: schema.email }, // unchangeable
trailingIcons: [require('../../file/image/clear_trailing_icon.png')], // unchangeable
helperText: 'only .com, .net and .org allowed', // unchangeable
styleNames: {
textInputContainer: { ...styles.textInputContainer, ...styles.emailExtraTextInputContainer },
textInputLabel: { ...styles.textInputLabel, ...styles.emailExtraTextInputLabel },
textInput: { ...styles.textInput, ...styles.emailExtraTextInput },
},
});
const [phoneNumber, setPhoneNumber] = useState({
// ...
});
const [countryCode, setCountryCode] = useState({
});
const [password, setPassword] = useState({
});
// ...
const references = useRef({});
return (
<View style={[styles.mainContainer]}>
{
useMemo(() => renderReusableTextInput(email, setEmail, references.current, styles), [email, minimumWidth])
}
</View>
);
}
export default memo(Main);
const styles__575 = StyleSheet.create({
// 320 to 575
mainContainer: {
},
textInputContainerWarper: {
},
emailExtraTextInputContainerWarper: {
},
countryCodeExtraTextInputContainerWarper: {
},
phoneNumberExtraTextInputContainerWarper: {
},
passwordExtraTextInputContainerWarper: {
},
textInputContainer: {
},
emailExtraTextInputContainer: {
},
textInputContainerFocus: {
},
textInputContainerBlurText: {
},
textInputLabel: {
},
emailExtraTextInputLabel: {
},
textInputLabelFocus: {
},
textInputLabelBlurText: {
},
textInput: {
},
emailExtraTextInput: {
},
textInputFocus: {
},
textInputBlurText: {
},
textInputHelper: {
},
emailExtraTextInputHelper: {
},
textInputError: {
},
emailExtraTextInputError: {
},
// other styles...
});
const styles_576_767 = StyleSheet.create({
// 576 to 767
});
const styles_768_ = StyleSheet.create({
// 768; goes to 1024;
});
const currentStyles = (width, stylesInitial = { ...styles__575 }, styles576 = { ...styles_576_767 }, styles768 = { ...styles_768_ }) => {
let styles = {};
if (width < 576) {
// ...
}
else if ((width >= 576) && (width < 768)) {
// ...
}
else if (width >= 768) {
// ...
}
return styles;
};
I tried mentioned approach and want a better answer.
First, I think you want global styles, so that you can access it from anywhere and you can also be able to execute needed code.
Make sure you use useMemo, useCallback in right manner, for better performance.
Move all Schema and Styles and Methods of your screens inside the reusableMethods.js file (at most case). It will act like the controller of all screens of your app, also make a demo method which return a tiny component and this method execute another method, so that you can get styles for different dimentions(see below code)
Store only changeable and needed properties in state variables.
Is code reusing approach of yours, correct? I can't say about that. I will say that it depends on developer choice.
you can try like below:
reusableMethods.js
// import all schema, style variants, utility methods and other
let screenStyles = {};
const executeDimensionBasedMethods = (width, screenName) => {
if (screenName === 'a_screen_name') screenStyles[screenName] = currentStyles(width, otherParameter);
// else if()
// ...
};
export const renderDimension = (width, screenName) => {
executeDimensionBasedMethods(width, screenName);
return (
<Text style={{ width: 0, height: 0 }}></Text>
);
};
// all method definitions and logics
export const renderReusableTextInput = (
state,
setState,
inputRef,
screenName
) => {
return (
<ReusableTextInput
inputName={state.name}
inputLabel={state.label}
inputValue={state.value}
inputRef={inputRef}
secureTextEntry={state.secureTextEntry}
textInputContainerWarperStyle={{ ...screenStyles[screenName].textInputContainerWarper, ...screenStyles[screenName][`${state.name}ExtraTextInputContainerWarper`] }}
textInputContainerStyle={state.styleNames.textInputContainer || { ...screenStyles[screenName].textInputContainer }
textInputLabelStyle={state.styleNames.textInputLabel || { ...screenStyles[screenName].textInputLabel }
textInputStyle={state.styleNames.textInput || { ...screenStyles[screenName].textInput }
textInputHelperStyle={{ ...screenStyles[screenName].textInputHelper, ...screenStyles[screenName][`${state.name}ExtraTextInputHelper`] }}
textInputErrorStyle={{ ...screenStyles[screenName].textInputError, ...screenStyles[screenName][`${state.name}ExtraTextInputError`] }}
helperText={state.helperText}
inputError={state.error}
onFocus={() => handleFocus(state, setState, screenStyles[screenName])}
onChangeText={(text) => handleChangeText(state, setState, text)}
onBlur={() => handleBlur(state, setState, screenStyles[screenName])}
/>
);
};
Main.js
const Main = () => {
const { width: windowWidth, height: windowHeight} = useWindowDimensions();
const minimumWidth = (windowWidth <= windowHeight) ? windowWidth : windowHeight;
const [email, setEmail] = useState({
name: 'email', // unchangeable
label: 'Email', // unchangeable
value: '',
error: '',
trailingIcons: [require('../../file/image/clear_trailing_icon.png')], // unchangeable
helperText: 'only .com, .net and .org allowed', // unchangeable
styleNames: {
textInputContainer: undefined,
textInputLabel: undefined,
textInput: undefined,
},
});
// other states
const references = useRef({});
return (
<>
{
useMemo(() => renderDimension(minimumWidth), [minimumWidth])
}
<View style={{ // inline style}}>
{
useMemo(() => renderReusableTextInput(email, setEmail, references.current, 'nameofscreen'), [email, minimumWidth])
}
</View>
</>
}
export default memo(Main);

Implement smooth text transition in React Native

I try to implement component that takes takes text property and depend on previous value shows smooth transition:
import React, { useEffect, useRef } from 'react'
import { Animated, StyleSheet } from 'react-native'
const usePrevious = (value) => {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
export default function AnimatedText({ style, children }) {
const fadeInValue = new Animated.Value(0)
const fadeOutValue = new Animated.Value(1)
const prevChildren = usePrevious(children)
useEffect(() => {
if (children != prevChildren) {
animate()
}
}, [children])
const animate = () => {
Animated.parallel([
Animated.timing(fadeInValue, {
toValue: 1,
duration: 1000,
useNativeDriver: true
}),
Animated.timing(fadeOutValue, {
toValue: 0,
duration: 1000,
useNativeDriver: true
})
]).start()
}
return (
<>
<Animated.Text style={[ style, { opacity: fadeInValue }]}>{children}</Animated.Text>
{
prevChildren &&
<Animated.Text style={[ style, styles.animatedText, { opacity: fadeOutValue }]}>{prevChildren}</Animated.Text>
}
</>
)
}
const styles = StyleSheet.create({
animatedText: {
position: 'absolute',
top: 0,
left: 0,
}
})
As the result I got smooth transition between component rendering with different children arguments. But there is a flickering due to some reasons related to animated value updates. Is there any way to avoid this problem or better solution to implement such component?
Found the solution with react-native-reanimated. It's not elegant implementation but it seems to work correctly without flickering:
import React, { useEffect, useRef } from 'react'
import { StyleSheet } from 'react-native'
import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated'
const usePrevious = (value) => {
const ref = useRef()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
export default function AnimatedText({ style, children }) {
const fadeValue1 = useSharedValue(0)
const fadeValue2 = useSharedValue(1)
const toggleFlagRef = useRef(false)
const animatedTextStyle1 = useAnimatedStyle(() => {
return {
opacity: withTiming(fadeValue1.value, { duration: 1000 })
}
})
const animatedTextStyle2 = useAnimatedStyle(() => {
return {
opacity: withTiming(fadeValue2.value, { duration: 1000 })
}
})
const prevChildren = usePrevious(children)
useEffect(() => {
if (children != prevChildren) {
animate()
}
}, [children])
const animate = () => {
if (toggleFlagRef.current) {
fadeValue1.value = 0
fadeValue2.value = 1
} else {
fadeValue1.value = 1
fadeValue2.value = 0
}
toggleFlagRef.current = !toggleFlagRef.current
}
return (
<>
<Animated.Text style={[ style, animatedTextStyle1 ]}>{toggleFlagRef.current ? prevChildren : children}</Animated.Text>
{
prevChildren &&
<Animated.Text style={[ style, styles.animatedText, animatedTextStyle2 ]}>{toggleFlagRef.current ? children : prevChildren}</Animated.Text>
}
</>
)
}
const styles = StyleSheet.create({
animatedText: {
position: 'absolute',
top: 0,
left: 0,
}
})

Question on switching threads in react native reanimated

So I have this simple animation where if you drag an element it will return back to (0, 0) on animation end,
import React from "react"
import { SafeAreaView } from "react-native"
import { PanGestureHandler } from "react-native-gesture-handler"
import Animated, {
Easing,
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
withTiming
} from "react-native-reanimated"
const Comp: React.FC = () => {
const x = useSharedValue(0)
const y = useSharedValue(0)
const translateAnim = useAnimatedStyle(() => {
return {
transform: [{ translateX: x.value }, { translateY: y.value }]
}
})
const drag = useAnimatedGestureHandler({
onStart: (e, ctx: { startX: number; startY: number }) => {
ctx.startX = x.value
ctx.startY = y.value
},
onActive: (e, ctx) => {
runOnJS(move)(
ctx.startX,
ctx.startY,
e.translationX,
e.translationY
)
},
onEnd: (e, ctx) => {
runOnJS(end)()
}
})
function move(
startX: number,
startY: number,
translateX: number,
translateY: number
) {
x.value = withTiming(startX + translateX, {
duration: 0,
easing: Easing.linear
})
y.value = withTiming(startY + translateY, {
duration: 0,
easing: Easing.linear
})
}
function end() {
x.value = withSpring(0)
y.value = withSpring(0)
}
return (
<SafeAreaView>
<PanGestureHandler onGestureEvent={drag}>
<Animated.View
style={[
{ backgroundColor: "red", height: 100, width: 100 },
translateAnim
]}></Animated.View>
</PanGestureHandler>
</SafeAreaView>
)
}
export default Comp
So as you can see I have move and end run in JS thread using runOnJs() function.
Does that mean withTiming will also run on JS thread or withTiming always runs on UI thread?
Also as you can see x.value = withTiming(...). Should I wrap this in runOnUI() function? Or to be precise when we set the animation value does it have to be run on UI thread?
I'm a bit late to your question, but for the sake of other answer-seekers:
When you assign an animation to your shared value, reanimated will automatically run that animation on the UI thread. So the answer to both of your questions is no, you don't have to do anything.
Note that the optional callback you pass to the animation will also be run on the UI thread.
See the documentation for this:
When using one of the hooks listed in the Reanimated API Reference, we automatically detect that the provided method is a worklet and do not require the directive to be specified. The method provided to the hook will be turned into a worklet and executed on the UI thread automatically.

React Native Pan Responder to Animate single dynamic view

I'm creating a series of cubes within a grid and I want to only move the cube that is pressed upon. Currently in my code it is moving all the cubes as one. I tried finding the individual key of the cube to try and target it for animation, but I'm not having any luck so far in figuring this out.
The myPanel component is just to layout the cubes that are defined in the renderCube function. It may be an issue with laying out all of the cubes with Animated.View, even though I added the key, but I can't access or figure out how to specifically animate only the selected cube.
import React, { Component } from 'react';
import { TouchableWithoutFeedback, View, ImageBackground, Dimensions, Animated, PanResponder } from 'react-native';
import myPanel from './myPanel';
const DATA = [
{ id: 1, color: '#ff8080' },
{ id: 2, color: '#80ff80' },
{ id: 3, color: '#ffff80' }
];
const winWidth = Dimensions.get('window').width;
const widthCalc = winWidth/400;
const cubeWidth = widthCalc*74;
const winHeight = Dimensions.get('window').height;
const heightCalc = ((winHeight-winWidth)/2)-5;
const xPush = 5*widthCalc;
const borRadius = 7.4*widthCalc;
let cnt = 0;
let xMove = 0;
let yMove = 0;
let currTarget = '033';
class Board extends Component {
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
};
}
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onStartShouldSetPanResponder:(e, gestureState) => true,
onPanResponderGrant: (e, gestureState) => {
console.log(this.currTarget);
},
onPanResponderMove: Animated.event([
null,
{
dx: this.state.pan.x,
dy: this.state.pan.y,
},
]),
onPanResponderRelease: () => {
Animated.spring(
this.state.pan,
{toValue: {x: 0, y: 0}},
).start();
}
});
}
renderCube(item) {
return (
<Animated.View {...this._panResponder.panHandlers}
style={this.state.pan.getLayout()} key={item.id}>
<TouchableWithoutFeedback
onPressIn={()=>{this.currTarget = item.id; console.log('Set '+item.id);}}>
<View
style={{
backgroundColor:myColor,
width:cubeWidth,
height:cubeWidth,
position: 'absolute',
top: yMove,
left: xMove,
bottom: 0,
right: 0,
borderRadius:borRadius
}}
></View>
</TouchableWithoutFeedback>
</Animated.View>
);
}
render() {
return (
<myPanel
data={DATA}
renderCube={this.renderCube.bind(this)}
/>
);
}
}
export default Board;

How to add a function call to Animated.sequence in react native

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