I'm trying to create a button that resizes (gets a little bit smaller when it' pressed). I use TouchableWithoutFeedback from react-native-gesture-handlerand I use react-native-reanimated.
This is my code so far:
import React, { useState } from 'react';
import { View } from 'react-native';
import Animated, { Easing, Extrapolate } from 'react-native-reanimated';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
const { interpolate, sub } = Animated;
const TouchableResize = (props) => {
const { onPress, children } = props;
const [scale, setScale] = useState(0);
const scaling = interpolate(scale, {
inputRange: [0, 1],
outputRange: [1, 0.90],
extrapolate: Extrapolate.CLAMP
});
return (
<TouchableWithoutFeedback onPressIn={() => setScale(1)} onPressOut={() => setScale(0)}>
<Animated.View style={{ transform: [{ scaleX: scaling }, { scaleY: scaling }] }}>
{children}
</Animated.View>
</TouchableWithoutFeedback>
);
};
export { TouchableResize };
This code works partly. The button resizes to 0.90 when it's pressed, but the animation is not smooth. It snaps directly to 0.90, and when it's released, the button directly snaps back.
How can I update my code so the animation runs smoothly? Please note I'm a complete beginner in react-native-reanimated.
You have to use timing function to change your Animated.Value over time. Here example in docs. Also, I created expo snack example. Here updated component code
import React, { useState, useMemo } from 'react';
import { View } from 'react-native';
import Animated, { Easing, Extrapolate } from 'react-native-reanimated';
import { TouchableWithoutFeedback } from 'react-native-gesture-handler';
const {
Clock,
Value,
set,
cond,
startClock,
clockRunning,
timing,
debug,
stopClock,
block,
interpolate,
useCode,
} = Animated;
function runTiming(clock, from, to) {
const state = {
finished: new Value(0),
position: new Value(from),
time: new Value(0),
frameTime: new Value(0),
};
const config = {
duration: 100,
toValue: new Value(to),
easing: Easing.inOut(Easing.ease),
};
return block([
cond(
clockRunning(clock),
[],
startClock(clock),
),
// we run the step here that is going to update position
timing(clock, state, config),
// if the animation is over we stop the clock
cond(state.finished, debug('stop clock', stopClock(clock))),
// we made the block return the updated position
state.position,
]);
}
const TouchableResize = (props) => {
const { onPress, children } = props;
const [pressed, setPressed] = useState(false);
const {clock, scale} = useMemo(() => ({
clock: new Clock(),
scale: new Value(1),
}), [])
useCode(
() => block([
pressed ? set(scale, runTiming(clock, 0, 1)) : set(scale, runTiming(clock, 1, 0))
]), [pressed]
);
const scaling = interpolate(scale, {
inputRange: [0, 1],
outputRange: [1, 0.90],
extrapolate: Extrapolate.CLAMP
});
return (
<TouchableWithoutFeedback onPressIn={() => setPressed(true)} onPressOut={() => setPressed(false)}>
<Animated.View style={{ transform: [{ scaleX: scaling }, { scaleY: scaling }] }}>
{children}
</Animated.View>
</TouchableWithoutFeedback>
);
};
export { TouchableResize };
Related
I want to implement an animated accordion list/ drawer / drop-down menu / collapsible card.
The animation should be performant and look like this:
After a lot of searching, I could find many libraries. But I wanted to implement it without any library. Also, some tutorials showed how to build one, but they were not performant.
Finally, this is how I implemented it. The complete snack code is here: https://snack.expo.dev/#vipulchandra04/a85348
I am storing isOpen (whether the menu is open or closed) in a state. Then changing that state on button press. I am using the LayoutAnimation API in React-Native to animate the opening and closing of the list. LayoutAnimation runs the animation natively, thus it is performant.
const Accordion = ({ title, children }) => {
const [isOpen, setIsOpen] = useState(false);
const toggleOpen = () => {
setIsOpen(value => !value);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
}
return (
<>
<TouchableOpacity onPress={toggleOpen} activeOpacity={0.6}>
{title}
</TouchableOpacity>
<View style={[styles.list, !isOpen ? styles.hidden : undefined]}>
{children}
</View>
</>
);
};
const styles = StyleSheet.create({
hidden: {
height: 0,
},
list: {
overflow: 'hidden'
},
});
With this, it will fix the Vipul's demo's error: if click accordion so fast, children's opacity descending to 0. and add animation for icon
import {
Animated,
LayoutAnimation,
Platform,
StyleProp,
StyleSheet,
UIManager,
View,
ViewStyle,
} from 'react-native';
import Ionicons from 'react-native-vector-icons/Ionicons;
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const toggleAnimation = duration => {
return {
duration: duration,
update: {
property: LayoutAnimation.Properties.scaleXY,
type: LayoutAnimation.Types.easeInEaseOut,
},
delete: {
property: LayoutAnimation.Properties.opacity,
type: LayoutAnimation.Types.easeInEaseOut,
},
};
};
interface IAccordion {
title?: JSX.Element | JSX.Element[];
children?: JSX.Element | JSX.Element[];
style?: StyleProp<ViewStyle> | undefined;
}
const Accordion = ({title, children, style}: IAccordion) => {
const [isOpen, setIsOpen] = useState(false);
const animationController = useRef(new Animated.Value(0)).current;
const arrowTransform = animationController.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '90deg'],
});
const onToggle = () => {
setIsOpen(prevState => !prevState);
const duration = 300;
const config = {
duration: duration,
toValue: isOpen ? 0 : 1,
useNativeDriver: true,
};
Animated.timing(animationController, config).start();
LayoutAnimation.configureNext(toggleAnimation(duration));
};
return (
<View style={style ? style : styles.accordion}>
<TouchableOpacity onPress={onToggle} style={styles.heading}>
{title}
<Animated.View style={{transform: [{rotateZ: arrowTransform}]}}>
<Ionicons name={'chevron-forward-outline'} size={18} />
</Animated.View>
</TouchableOpacity>
<View style={[styles.list, !isOpen ? styles.hidden : undefined]}>
{children}
</View>
</View>
);
};
export default Accordion;
I had difficulty using the native API, so I go to third parties. The only thing I couldn't do was make the accordion size automatic.
import { useEffect } from 'react';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
} from 'react-native-reanimated';
import styled from 'styled-components';
const Accordion = ({ children, open, height }) => {
const heightAnimation = useSharedValue(0);
useEffect(() => {
if (open === true) heightAnimation.value = height;
if (open === false) heightAnimation.value = 0;
}, [open]);
const animatedStyle = useAnimatedStyle(() => {
return {
height: withTiming(heightAnimation.value, {
duration: 500,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
}),
};
});
return (
<>
<Main style={animatedStyle}>{children}</Main>
</>
);
};
const Main = styled(Animated.View)`
width: 100%;
align-items: center;
justify-content: center;
overflow: hidden;
`;
export default Accordion;
Using:
<Accordion height={height} open={open}>
{children}
</Accordion>
As asked here for an example of what I managed to do with it, I tried to get as much out of it as possible.
You can see a deploy here: https://snack.expo.dev/#francisco.ossian/accordion
Libs used, react-native-reanimated
I am using animation for up and down in the react native, But the animation just slide from up to down and then stop at the bottom i want to move it up and down continuously. I have also used animation loop so please check and provide me solution for this
import React, { useEffect, useState } from 'react'
import { Text, View, Animated, Easing, StyleSheet } from 'react-native'
import LoaderLogo from '../../icons/commonicons/LoaderLogo'
import { Loadericon } from '../../constants/Image';
import LinearGradient from 'react-native-linear-gradient';
import { dynamicSize } from '../sizechoose';
const amimationScreen = () => {
const startValue = new Animated.Value(0);
const endValue = dynamicSize(225);
const startValue2 = new Animated.Value(225);
const endValue2 = dynamicSize(0);
const duration = 5000;
useEffect(() => {
Animated.sequence([
Animated.timing(startValue, {
toValue: endValue,
duration: duration,
useNativeDriver: true,
}),
Animated.timing(startValue2, {
toValue: endValue2,
duration: duration,
useNativeDriver: true,
})
]).start()
}, [startValue, endValue, duration]);
return (
<Animated.View style={[{ transform: [{ translateY: startValue }] }]}>
<View style={{backgroundColor:'red',height:10,width:100}}>
</View>
</Animated.View>
)
}
export default amimationScreen
I also tried with react-native-animatable package but it is not good to use for me as it starts animation from the top of the screen.
This worked for me:
const App = () => {
const animated = new Animated.Value(0);
const duration = 5000;
useEffect(() => {
Animated.loop(
Animated.sequence([
Animated.timing(animated, {
toValue: 255,
duration: duration,
useNativeDriver: true,
}),
Animated.timing(animated, {
toValue: 0,
duration: duration,
useNativeDriver: true,
}),
]),
).start();
}, []);
return (
<Animated.View style={[{transform: [{translateY: animated}]}]}>
<View style={{backgroundColor: 'red', height: 10, width: 100}}></View>
</Animated.View>
);
};
So instead of having two instances of Animated.Value for translation, create one and let it transition from 0 to 255 and from 255 back to 0 in sequence. And make it loop once the sequence has finished.
I think the main problem in your original approach is that startValue decides how the view translates since this is what you pass as the value of translateY. The downward animation therefore happens correctly in your example. The upward animation however does not happen, because startValue2 is passed to Animated.timing and startValue is not used in the translation of any views in your example.
import React, { useEffect, useRef, useState } from 'react';
import { Animated, Dimensions, Easing, StyleSheet, View } from 'react-native';
export const App = () => {
const animatedValue = useRef(new Animated.Value(0)).current;
const [isTop, setIsTop] = useState(true);
const startAnimation = toValue => {
Animated.timing(animatedValue, {
toValue,
duration: 1000,
easing: Easing.linear,
useNativeDriver: true
}).start(() => {
setIsTop(!isTop);
})
}
useEffect(() => {
startAnimation(isTop ? 1 : 0);
}, [isTop]);
const translateY = animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, Dimensions.get('window').height - 70],
extrapolate: 'clamp'
})
return (
<View style={styles.container}>
<Animated.View style={[styles.square, { transform: [{ translateY }] }]}>
</Animated.View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center'
},
square: {
width: 70,
height: 70,
backgroundColor: 'red'
}
});
in my react-native project, I'm trying to use a callback when the State of LongPressGestureHandler becomes END | FAILED | CANCELLED. The function for this callback uses a global variable reactions that I receive as props. Even after this prop gets updated. The function somehow is using the old reactions. I'll be really thankful if you could explain what I'm doing wrong.
`
import React, { useState, useEffect } from 'react';
import { useNavigation } from '#react-navigation/native';
import { LongPressGestureHandler, State } from 'react-native-gesture-handler';
import { Vibration, View, Text } from 'react-native';
import Animated from 'react-native-reanimated';
import { useGestureHandler } from 'react-native-redash';
import useReaction from '../../../../../../services/dashboard/feed/components/reaction/api';
import assets from '../../../../../../assets';
import styles from './styles';
const {
Value, useCode, block, eq, call, onChange, cond, and, or, set
} = Animated;
export default ({
reactions,
emoji,
componentRefetch,
loggedInUsersId,
itemId,
counts,
}) => {
const { navigate } = useNavigation();
const [giveReaction, giveUnReaction] = useReaction(componentRefetch);
const reacted = reactions.find(
(reaction) => (reaction.user ? reaction.user.id === loggedInUsersId : false)
&& reaction.emoji === emoji
);
const [popAnim] = useState(new Value(1));
const onEmojiPress = async () => {
console.log('Emoji Pressed');
console.log(reactions.length);
if (reacted) {
await giveUnReaction(itemId, emoji, reacted.id);
} else {
await giveReaction(itemId, emoji);
}
};
const onEmojiLongPress = () => {
console.log('Emoji long Pressed');
Vibration.vibrate(1);
navigate(assets.strings.dashboard.feeds.reactantsPopup.NAME, {
reactions,
emoji,
});
};
const longPressState = new Value(State.UNDETERMINED);
const longPressGestureHandler = useGestureHandler({ state: longPressState });
const shouldScale = new Value(-1);
useCode(
() => block([
cond(
eq(longPressState, State.BEGAN),
set(shouldScale, 1)
),
cond(
eq(longPressState, State.FAILED),
call([], onEmojiPress)
),
cond(
or(
eq(longPressState, State.CANCELLED),
eq(longPressState, State.END),
),
call([], onEmojiLongPress)
),
]),
[]
);
return (
<LongPressGestureHandler {...longPressGestureHandler} minDurationMs={100}>
<Animated.View
style={{
...styles.icon,
borderColor: reacted
? assets.colors.appDefaults.primaryColor
: '#eeeeee',
backgroundColor: reacted
? assets.colors.appDefaults.primaryColorLight
: '#eeeeee',
}}
>
<Animated.View
style={{
transform: [
{ scale: popAnim },
{
translateY: popAnim.interpolate({
inputRange: [1, 1.3],
outputRange: [0, -2],
}),
},
],
}}
>
<Text
style={{
color: reacted ? assets.colors.appDefaults.primaryColor : 'black',
}}
>
{`${emoji ?? '👍'} `}
</Text>
</Animated.View>
<Text
style={{
color: reacted ? assets.colors.appDefaults.primaryColor : 'black',
}}
>
{`${counts[emoji]}`}
</Text>
</Animated.View>
</LongPressGestureHandler>
);
};
`
I found out that(by experimenting) useCode sends a set of instructions to native side, in order to change the set of instructions to latest prop update, we need to fire the useCode on prop update.
useCode(, [dependencyThatGetsUpdated])
This is an extremely unusual problem...
I have a View that acts like a flashcard. I have used animations in order to flip over the flashcard.
After I click the flashcard, the system completely breaks down. It works fine ONCE, but then it doesn't even detect the clicks.
import React, {Component} from 'react';
import { View, Text, StyleSheet, Animated, TouchableWithoutFeedback, Easing } from 'react-native';
import Swiper from 'react-native-swiper';
import AppText from "../components/AppText";
type Props = {};
export default class FlashcardScreen extends Component<Props> {
static navigationOptions = ({ navigation }) => {
return {
title: navigation.getParam('title', 'Flashcards')
}
};
constructor(props){
super(props);
this.animatedValue = new Animated.Value(0);
this.value = 0;
this.textAnimatedValue = new Animated.Value(0);
this.animatedValue.addListener(({value}) => {
this.value = value;
if (value === 90){
Animated.timing(this.textAnimatedValue, {
toValue: 0,
duration: 250,
}).start();
}
});
}
state = {
cards: this.props.navigation.state.params.cards,
displayTexts: Object.keys(this.props.navigation.state.params.cards),
index: 0,
};
flipAnimation = (index) => {
alert("Clicked!");
let { displayTexts, cards } = this.state;
const tempDisplayTexts = [...displayTexts];
const toValue = this.value >= 90 ? 0 : 180;
const entry = Object.entries(cards)[index];
Animated.parallel([
Animated.timing(this.animatedValue, {
toValue: 90,
duration: 250,
easing: Easing.linear
}),
Animated.timing(this.textAnimatedValue, {
toValue: 90,
duration: 250
})
]).start(() => {
if(displayTexts[index] === entry[0]){
tempDisplayTexts[index] = entry[1];
} else {
tempDisplayTexts[index] = entry[0];
}
this.setState({ displayTexts: tempDisplayTexts });
Animated.parallel([
Animated.timing(this.animatedValue, {
toValue: toValue,
duration: 250,
easing: Easing.linear
}),
]).start();
});
};
render() {
let { cards, displayTexts } = this.state;
this.SetInterpolate = this.animatedValue.interpolate({
inputRange: [0, 180],
outputRange: ['0deg', '180deg'],
});
const Rotate_X_AnimatedStyle = {
transform: [{ rotateX: this.SetInterpolate }],
};
this.textSetInterpolate = this.textAnimatedValue.interpolate({
inputRange: [0, 90],
outputRange: ['0deg', '90deg'],
});
const Test_Rotate_X_AnimatedStyle = {
transform: [{ rotateX: this.textSetInterpolate }]
};
return (
<View style={styles.container}>
<Swiper showsPagination={false} loop={false} onIndexChanged={(index) => this.setState({ index })}>
{
Object.entries(cards).map((question, index) => {
return (
<View style={styles.main}>
<TouchableWithoutFeedback onPress={() => this.flipAnimation(index)}>
<Animated.View style={[Rotate_X_AnimatedStyle, styles.card]} />
</TouchableWithoutFeedback>
<Animated.Text
onPress={() => this.flipAnimation(index)}
style={[styles.text, Test_Rotate_X_AnimatedStyle]}>
{displayTexts[index]}
</Animated.Text>
</View>
)
})
}
</Swiper>
<AppText style={styles.position} text={(this.state.index + 1) + " / " + this.state.displayTexts.length} />
</View>
);
}
}
I tried to change the code so that each card has its own animatedValue and textValue, but it did not fix the problem.
Here is a link to my recordings. The first one is the emulator, while the second one is the device:
https://imgur.com/gallery/xKYs3bc
Thanks in advance for any help! I've spent hours on this :/
I am trying to animate border color in React Native, but animation doesn't work. Border color doesn't have ORIGINAL_COLOR = '#a0a0a0' neither SUCCESS_COLOR = '#008FEB', it is black. How can I make default color ORIGINAL_COLOR = '#a0a0a0' if keyboard is hidden and SUCCESS_COLOR = '#008FEB' when keyboard shows up?
const styles = StyleSheet.create({
inputContainer: {
borderBottomWidth: 1,
},
});
<Input
containerStyle={styles.inputContainer}
underlineColorAndroid="transparent"
/>;
Input.jsx
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { TextInput, Text, View, Animated, Keyboard } from 'react-native';
import styles from './styles';
const SUCCESS_COLOR = '#008FEB';
const ORIGINAL_COLOR = '#a0a0a0';
export default class Input extends Component {
constructor(props) {
super(props);
this.color = new Animated.Value(ORIGINAL_COLOR);
}
componentWillMount () {
this.keyboardWillShowSub = Keyboard.addListener('keyboardWillShow', this.keyboardWillShow);
this.keyboardWillHideSub = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide);
}
componentWillUnmount() {
this.keyboardWillShowSub.remove();
this.keyboardWillHideSub.remove();
}
keyboardWillShow = (event) => {
console.log(SUCCESS_COLOR);
Animated.timing(this.color, {
duration: event.duration,
toValue: SUCCESS_COLOR,
}).start();
};
keyboardWillHide = (event) => {
console.log(ORIGINAL_COLOR);
Animated.timing(this.color, {
duration: event.duration,
toValue: ORIGINAL_COLOR,
}).start();
};
render() {
const {
value,
} = this.props;
return (
<Animated.View style={[styles.containerStyle, { borderBottomColor: this.color }]}>
<TextInput
ref="input"
{...this.props}
value={value}
/>
</Animated.View>
);
}
}
Here you go: https://snack.expo.io/#zvona/interpolation-of-color
The key is to use interpolate to change the number value into rgb value:
let borderBottomColor = this.color.interpolate({
inputRange: [0, 1],
outputRange: [ORIGINAL_COLOR, SUCCESS_COLOR]
});