React Native: How to animate a particular component? - reactjs

I am making a quiz. And all options will render in for loop.
Expected Behaviour:
When I click on an option, if it is the wrong answer then it should change the background color to red and it should shake.
Below is the code I am trying.
import React, { Component } from "react";
import {
View,
Text,
TouchableWithoutFeedback,
Animated,
Easing
} from "react-native";
class MCQOptions extends Component {
state = {
optionSelectedStatus: 0 // 0: unselected, 1: correct, -1: wrong
};
constructor() {
super();
this.animatedValue = new Animated.Value(0);
this.shakeAnimValue = new Animated.Value(0);
}
onOptionSelected(i) {
// this.props.showNextQuestion();
var answer = this.props.answer;
if (answer == i) {
this.setState({ optionSelectedStatus: 1 });
this.showCorrectAnimation();
} else {
this.setState({ optionSelectedStatus: -1 });
this.showErrorAnimation();
}
}
showErrorAnimation() {
this.shakeAnimValue.setValue(0);
Animated.timing(this.shakeAnimValue, {
toValue: 1,
duration: 300,
easing: Easing.linear
}).start();
}
showCorrectAnimation() {}
getOptions() {
var options = [];
var optionSelectedStyle = styles.optionUnselected;
var optionShadowStyle = styles.optionShadow;
if (this.state.optionSelectedStatus == 1) {
optionSelectedStyle = styles.optionCorrect;
optionShadowStyle = null;
} else if (this.state.optionSelectedStatus == -1) {
optionSelectedStyle = styles.optionWrong;
optionShadowStyle = null;
}
const marginLeft = this.shakeAnimValue.interpolate({
inputRange: [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1],
outputRange: [0, -10, 10, -10, 10, -10, 0]
});
for (var i = 0; i < this.props.options.length; i++) {
options.push(
<TouchableWithoutFeedback
onPress={this.onOptionSelected.bind(this, this.props.indexes[i])}
key={"options_" + i}
>
<View style={styles.optionBox}>
<View style={optionShadowStyle} />
<Animated.Text
style={[
styles.option,
optionSelectedStyle,
{ marginLeft: marginLeft }
]}
key={"option" + i}
>
{this.props.options[i]}
</Animated.Text>
</View>
</TouchableWithoutFeedback>
);
}
return options;
}
render() {
const marginTop = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [100, 0]
});
const opacity = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 1]
});
return (
<Animated.View style={{ marginTop: marginTop, opacity: opacity }}>
{this.getOptions()}
</Animated.View>
);
}
// Animations
componentDidMount() {
this.slideUpOptionsContainer();
}
componentWillReceiveProps() {
this.slideUpOptionsContainer();
this.setState({ optionSelectedStatus: 0 });
}
slideUpOptionsContainer() {
this.animatedValue.setValue(0);
Animated.timing(this.animatedValue, {
toValue: 1,
duration: 300,
easing: Easing.linear
}).start();
}
}
const styles = {
optionBox: {
margin: 5
},
optionsContainer: {
marginTop: 100
},
option: {
padding: 10,
textAlign: "center",
borderRadius: 10,
overflow: "hidden",
width: "100%"
},
optionUnselected: {
backgroundColor: "#FFF"
},
optionWrong: {
backgroundColor: "red"
},
optionCorrect: {
backgroundColor: "green"
},
optionShadow: {
backgroundColor: "rgba(255,255,255,0.85)",
position: "absolute",
width: "100%",
height: "100%",
left: -5,
top: 5,
borderRadius: 10
}
};
export default MCQOptions;
The above code animating(shake) all the options (Which is proper according to the login written), and I am stuck how to make only the clicked option get animated instead all?
Edited:
Parent component with props feed:
class MCQ extends Component<{}> {
render() {
var options = ["yes", "no", "can't define"];
var indexes = [1,2,3];
var answer = 1;
optionsObj = <MCQOptions
options={options}
indexes={indexes}
answer={answer}/>;
return (
<View style={styles.container} >
<View style={styles.optionsContainer}>
{optionsObj}
</View>
</View>
);
}
}
const styles = {
container: {
flex: 1,
backgroundColor: "blue",
paddingTop: 20,
justifyContent: 'flex-start',
padding: 20
},
};
export default MCQ;
Second EDIT:
Trying to simplify problem.
Below is the simplified code with zero props. I want to animate clicked element only.
import React, { Component } from "react";
import {
View,
Text,
TouchableWithoutFeedback,
Animated,
Easing
} from "react-native";
class MCQOptions extends Component {
constructor() {
super();
this.shakeAnimValue = new Animated.Value(0);
}
showErrorAnimation() {
this.shakeAnimValue.setValue(0);
Animated.timing(this.shakeAnimValue, {
toValue: 1,
duration: 300,
easing: Easing.linear
}).start();
}
getOptions() {
const marginLeft = this.shakeAnimValue.interpolate({
inputRange: [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1],
outputRange: [0, -10, 10, -10, 10, -10, 0]
});
var options = [];
for (var i = 0; i < 4; i++) {
options.push(
<TouchableWithoutFeedback
onPress={this.showErrorAnimation.bind(this)}
key={"options_" + i}
>
<View style={styles.optionBox}>
<Animated.Text style={[
styles.option,
{ marginLeft: marginLeft }
]}
key={"option" + i}
>
{"Option "+i}
</Animated.Text>
</View>
</TouchableWithoutFeedback>
);
}
return options;
}
render() {
return (
<View style={{ marginTop: 100}}>
{this.getOptions()}
</View>
);
}
}
const styles = {
optionBox: {
margin: 5
},
optionsContainer: {
marginTop: 100
},
option: {
padding: 10,
textAlign: "center",
borderRadius: 10,
overflow: "hidden",
width: "100%"
},
optionUnselected: {
backgroundColor: "#FFF"
},
optionWrong: {
backgroundColor: "red"
},
};
export default MCQOptions;

Since you want to animate them separately, they cannot bind to the same Animated object. You have to make them multiple, for example:
Example:
export class App extends Component {
constructor() {
super();
this.getOptions = this.getOptions.bind(this);
this.originalOptions = [0,1,2,3];
this.shakeAnimations = this.originalOptions.map( (i) => new Animated.Value(0) );
}
showErrorAnimation(i) {
this.shakeAnimations[i].setValue(0);
Animated.timing(this.shakeAnimations[i], {
toValue: 1,
duration: 300,
easing: Easing.linear
}).start();
}
getOptions() {
var options = this.originalOptions.map( (i) => {
const marginLeft = this.shakeAnimations[i].interpolate({
inputRange: [0, 0.2, 0.4, 0.6, 0.8, 0.9, 1],
outputRange: [0, -10, 10, -10, 10, -10, 0]
});
return (
<TouchableWithoutFeedback
onPress={() => this.showErrorAnimation(i)}
key={"options_" + i}
>
<View style={styles.optionBox}>
<Animated.Text style={[
styles.option,
{ marginLeft: marginLeft }
]}
key={"option" + i}
>
{"Option "+i}
</Animated.Text>
</View>
</TouchableWithoutFeedback>
)
});
return options;
}
render() {
return (
<View style={{ marginTop: 100}}>
{this.getOptions()}
</View>
);
}
}
const styles = {
optionBox: {
margin: 5
},
optionsContainer: {
marginTop: 100
},
option: {
padding: 10,
textAlign: "center",
borderRadius: 10,
overflow: "hidden",
width: "100%"
},
optionUnselected: {
backgroundColor: "#FFF"
},
optionWrong: {
backgroundColor: "red"
},
};
Result:

Related

Range Slider transform translateX working as not expected React Native iOS

Hello i have issue with range slider it goes out of bounds when I move the thumbs.
I spend a lot time to find what is going wrong but i can't find solution.
unexpected result:
issue result
const RangeSlider = ({ sliderWidth, min, max, step }: RangeSliderProps) => {
const position = useSharedValue(0);
const position2 = useSharedValue(sliderWidth);
const zIndex = useSharedValue(0);
const zIndex2 = useSharedValue(0);
const gestureHandler = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{ x: number; y: number }
>({
onStart: (_, ctx) => {
ctx.x = position.value;
},
onActive: (e, ctx) => {
if (ctx.x + e.translationX < 0) {
position.value = 0;
} else if (ctx.x + e.translationX > position2.value) {
position.value = position2.value;
zIndex.value = 1;
zIndex2.value = 0;
} else {
position.value = ctx.x + e.translationX
}
},
onEnd: () => {
},
});
const gestureHandler2 = useAnimatedGestureHandler<
PanGestureHandlerGestureEvent,
{ x: number; y: number }
>({
onStart: (_, ctx) => {
ctx.x = position2.value;
},
onActive: (e, ctx) => {
if (ctx.x + e.translationX > sliderWidth) {
position2.value = sliderWidth;
} else if (ctx.x + e.translationX < position.value) {
position2.value = position.value;
zIndex.value = 0;
zIndex2.value = 1;
} else {
position2.value = ctx.x + e.translationX;
}
},
onEnd: () => {
},
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: position.value }],
zIndex: zIndex.value,
}));
const animatedStyle2 = useAnimatedStyle(() => ({
transform: [{ translateX: position2.value }],
zIndex: zIndex2.value,
}));
const minLabelText: any = useAnimatedProps(() => {
return {
text: `${min +
Math.floor(position.value / (sliderWidth / ((max - min) / step))) * step
}`,
};
});
const maxLabelText: any = useAnimatedProps(() => ({
text: `${min +
Math.floor(position2.value / (sliderWidth / ((max - min) / step))) *
step
}`,
}));
const sliderStyle2 = useAnimatedStyle(() => {
return {
width: position2.value - position.value,
transform: [{ translateX: position.value }]
}
});
return (
<View style={[styles.sliderContainer, { width: sliderWidth }]} >
<Animated.View style={[styles.label]}>
<AnimatedTextInput
style={styles.labelText}
editable={false}
animatedProps={minLabelText}
/>
</Animated.View>
<View style={[styles.sliderBack, { width: sliderWidth }]} />
<Animated.View style={[styles.sliderFront, sliderStyle2]} />
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View style={[animatedStyle, styles.thumb]} />
</PanGestureHandler>
<PanGestureHandler onGestureEvent={gestureHandler2} >
<Animated.View style={[animatedStyle2, styles.thumb]} >
</Animated.View>
</PanGestureHandler>
<Animated.View style={[styles.label]}>
<AnimatedTextInput
style={styles.labelText}
editable={false}
animatedProps={maxLabelText}
/>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
sliderContainer: {
justifyContent: 'center',
alignSelf: 'center',
},
sliderBack: {
height: 4,
backgroundColor: '#444444',
borderRadius: 20,
},
sliderFront: {
height: 4,
backgroundColor: '#0088D1',
borderRadius: 20,
position: 'absolute',
},
thumb: {
left: -10,
width: 13,
height: 13,
position: 'absolute',
backgroundColor: '#0088D1',
borderColor: 'white',
borderWidth: 2,
borderRadius: 10,
},
label: {
width: 35,
backgroundColor: "#444444",
borderRadius: 5,
alignSelf: 'center',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 4,
marginRight: 4
},
labelText: {
textAlign: "right",
color: '#EEEEEE',
padding: 5,
fontWeight: '500',
lineHeight: 14,
fontSize: 12,
width: '100%',
},
});
export default RangeSlider;
possible issue
const sliderStyle2 = useAnimatedStyle(() => {
return {
transform: [{ translateX: position.value }],
width: position2.value - position.value,
}
}, []);
expected result: expected image
Slider not goes out of bounds when I move the slider
overflow: 'hidden' not working as expected.

React Native: Animation plays only on first render

I am making an animation. There is a boxes array, that will contain seven boxes of info to render. Each box will appear at different times. Now the problem is that when the first box is being displayed, the animation plays as expected, but never runs again and all upcoming boxes is being displayed without animation. Where could be the problem? Here is my current code:
interface Styles {
wrap: ViewStyle;
front: ViewStyle;
back: ViewStyle;
}
const styles: Styles = {
wrap: {
position: 'relative',
},
front: {
width: '100%',
height: '100%',
position: 'absolute',
},
back: {
width: '100%',
height: '100%',
backfaceVisibility: 'hidden',
},
};
type Props = {
boxes: Box;
}
export const Component: React.FC<Props> = ({ boxes }) => {
const animation = useRef(new Animated.Value(0));
useEffect(() => {
const anim = Animated.timing(animation.current, {
toValue: 180,
duration: 2000,
useNativeDriver: false,
});
if (anim) {
anim.start();
}
return () => {
anim.stop();
};
}, [ animation ]);
return (
<View>
{boxes.map((box) => (
<View key={box}>
<View style={styles.wrap}>
<Animated.View
style={[ styles.front, {
transform: [ { rotateY: animation.current.interpolate({
inputRange: [ 0, 180 ],
outputRange: [ '180deg', '360deg' ],
}) } ],
}]}
>
<FrontBox box={box} />
</Animated.View>
<Animated.View
style={[ styles.back, {
transform: [ { rotateY: animation.current.interpolate({
inputRange: [ 0, 180 ],
outputRange: [ '360deg', '180deg' ],
}) } ],
}]}
>
<BackBox />
</Animated.View>
</View>
</View>
))}
</View>
);
};

Issue with messages rendering a view specific to onLongPress

I am trying to render a View through renderCustomView={this.displayEmojis} for an individual message once a user has activated the onLongPress={} function. However, the view im trying to display repeats for every single message on the screen, rather than the one pressed. I have attached what happens when I try to press on a message with multiple messages showing:
//EventPost
import React from 'react'
import { Platform, View, StatusBar, TouchableOpacity, Text, Image, StyleSheet } from 'react-native'
import { getStatusBarHeight } from 'react-native-status-bar-height';
import { GiftedChat } from 'react-native-gifted-chat'
import emojiUtils from 'emoji-utils'
import KeyboardSpacer from 'react-native-keyboard-spacer';
import SlackMessage from './SlackMessage'
import Keyboard from 'react-native';
export default class EventPosts extends React.Component {
constructor(props) {
super(props);
this.state = {
messages: [],
toggle: false,
reactedMessage: 0,
reacted: false,
images: [
require("../assets/emojis/beer_photo.png"),
require("../assets/emojis/party_photo.png"),
require("../assets/emojis/laughing-emoji_photo.png"),
require("../assets/emojis/happy_photo.png"),
require("../assets/emojis/crying_photo.png"),
],
id: 0,
}
this.displayEmojis = this.displayEmojis.bind(this);
}
componentDidMount() {
this.setState({
messages: [
{
_id: 1,
text: 'I cant wait for this birthday party',
createdAt: new Date(),
user: {
_id: 2,
name: 'Andrew Garrett',
avatar: 'https://placeimg.com/140/140/any',
},
},
],
});
}
onSend(messages = []) {
this.setState(previousState => ({
messages: GiftedChat.append(previousState.messages, messages),
}))
}
renderMessage(props) {
const {
currentMessage: { text: currText },
} = props
let messageTextStyle
if (currText && emojiUtils.isPureEmojiString(currText)) {
messageTextStyle = {
fontSize: 28,
lineHeight: Platform.OS === 'android' ? 34 : 30,
}
}
return (<SlackMessage {...props} messageTextStyle={messageTextStyle} />)
}
dismiss = () =>
Keyboard.dismiss();
updateEmojiCount(number) {
switch (number) {
case 1:
this.setState({ reactedMessage: 0 });
break;
case 2:
this.setState({ reactedMessage: 1 });
break;
case 3:
this.setState({ reactedMessage: 2 });
break;
case 4:
this.setState({ reactedMessage: 3 });
break;
case 5:
this.setState({ reactedMessage: 4 });
break;
default:
this.forceUpdate();
}
this.setState({ reacted: true });
this.setState({ toggle: false })
}
beerEmoji() {
return (
<View>
<TouchableOpacity style={styles.emojiContainer}
onPress={() => this.updateEmojiCount(1)}>
<Image style={styles.emoji} source={require("../assets/emojis/beer.gif")} />
</TouchableOpacity>
</View>
)
}
partyEmoji() {
return (
<View>
<TouchableOpacity style={styles.emojiContainer}
onPress={() => this.updateEmojiCount(2)}>
<Image style={styles.emoji} source={require("../assets/emojis/party.gif")} />
</TouchableOpacity>
</View>
)
}
laughingEmoji() {
return (
<View>
<TouchableOpacity onPress={() => this.updateEmojiCount(3)} style={styles.emojiContainer}>
<Image style={[styles.emoji, styles.emojiResize]} source={require("../assets/emojis/laughing-emoji.gif")} />
</TouchableOpacity>
</View>
)
}
happyEmoji() {
return (
<View>
<TouchableOpacity onPress={() => this.updateEmojiCount(4)} style={styles.emojiContainer}>
<Image style={[styles.emoji, styles.emojiResize]} source={require("../assets/emojis/happy.gif")} />
</TouchableOpacity>
</View>
)
}
cryingEmoji() {
return (
<View>
<TouchableOpacity onPress={() => this.updateEmojiCount(5)} style={styles.emojiContainer}>
<Image style={[styles.emoji, styles.emojiResize]} source={require("../assets/emojis/crying.gif")} />
</TouchableOpacity>
</View>
)
}
displayEmojis() {
if (this.state.toggle) {
return (
<View
style={{
flexDirection: 'row',
right: '35%',
backgroundColor: 'white',
zIndex: 3,
overlayColor: 'white',
borderRadius: 30,
paddingHorizontal: '10%',
bottom: '2%'
}}>
{this.beerEmoji()}
{this.partyEmoji()}
{this.laughingEmoji()}
{this.happyEmoji()}
{this.cryingEmoji()}
</View>
)
}
}
toggleEmojis = (context, message) => {
let temp = this.state.messages.filter(temp => message._id == temp._id);
let id = temp[0]._id;
this.setState({ id: temp[0]._id })
if (this.state.toggle) { this.setState({ toggle: false }) }
else { this.setState({ toggle: true }) }
}
render() {
return (
<View style={{ flex: 1 }}>
<View
style={{ paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : getStatusBarHeight() }}>
<View style={{ backgroundColor: '#2c3e50', paddingVertical: '4%' }}>
<TouchableOpacity style={{ left: "89%", top: '0%' }} onPress={() => this.props.navigation.goBack()}>
<Text style={{ color: 'white' }}>Done</Text>
</TouchableOpacity>
</View>
</View>
<GiftedChat
keyboardShouldPersistTaps={'handled'}
messages={this.state.messages}
onSend={messages => this.onSend(messages)}
placeholder={"Post something..."}
isKeyboardInternallyHandled={false}
multiline={true}
extraData={this.state}
alwaysShowSend={true}
onLongPress={this.toggleEmojis}
user={{ _id: 1 }}
renderCustomView={this.displayEmojis}
renderMessage={this.renderMessage} />
<KeyboardSpacer />
</View>
)
}
}
const styles = StyleSheet.create({
emojiContainer: {
width: 55,
height: 55,
paddingHorizontal: '13%',
right: '15%'
},
emoji: {
width: 55,
height: 55,
borderRadius: 20,
},
emojiResize: {
transform: [{ scale: 0.75 }],
},
emojiReacted: {
transform: [{ scale: 0.4 }],
},
})
//SlackMessage
import PropTypes from 'prop-types';
import React from 'react';
import {
View,
ViewPropTypes,
StyleSheet,
Image
} from 'react-native';
import { Avatar, Day, utils } from 'react-native-gifted-chat';
import Bubble from './SlackBubble';
const { isSameUser, isSameDay } = utils;
export default class Message extends React.Component {
state = {
images: [
require("../assets/emojis/beer_photo.png"),
require("../assets/emojis/party_photo.png"),
require("../assets/emojis/laughing-emoji_photo.png"),
require("../assets/emojis/happy_photo.png"),
require("../assets/emojis/crying_photo.png"),
]
}
getInnerComponentProps() {
const { containerStyle, ...props } = this.props;
return {
...props,
position: 'left',
isSameUser,
isSameDay,
};
}
renderDay() {
if (this.props.currentMessage.createdAt) {
const dayProps = this.getInnerComponentProps();
if (this.props.renderDay) {
return this.props.renderDay(dayProps);
}
return <Day {...dayProps} />;
}
return null;
}
renderBubble() {
const bubbleProps = this.getInnerComponentProps();
if (this.props.renderBubble) {
return this.props.renderBubble(bubbleProps);
}
return <Bubble {...bubbleProps} />;
}
renderAvatar() {
let extraStyle;
if (
isSameUser(this.props.currentMessage, this.props.previousMessage)
&& isSameDay(this.props.currentMessage, this.props.previousMessage)
) {
extraStyle = { height: 0 };
}
const avatarProps = this.getInnerComponentProps();
return (
<Avatar
{...avatarProps}
imageStyle={{ left: [styles.slackAvatar, avatarProps.imageStyle, extraStyle] }}
/>
);
}
render() {
const marginBottom = isSameUser(this.props.currentMessage, this.props.nextMessage) ? 2 : 10;
const { images } = this.state
const index = (this.props.extraData.reactedMessage);
return (
<View>
{this.renderDay()}
<View
style={[
styles.container,
{ marginBottom },
this.props.containerStyle,
]}>
{this.renderAvatar()}
{this.renderBubble()}
{<Image source={images[index]} style={{ width: 20, height: 20, right: '8%' }} />}
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'flex-end',
justifyContent: 'flex-start',
marginLeft: 8,
marginRight: 8,
backgroundColor: 'white',
borderWidth: 2,
borderColor: '#BEBEBE',
paddingBottom: '3%'
},
slackAvatar: {
height: 40,
width: 40,
borderRadius: 3,
left: '10%',
bottom: '12%'
},
});
Message.defaultProps = {
renderAvatar: undefined,
renderBubble: null,
renderDay: null,
currentMessage: {},
nextMessage: {},
previousMessage: {},
user: {},
containerStyle: {},
};
Message.propTypes = {
renderAvatar: PropTypes.func,
renderBubble: PropTypes.func,
renderDay: PropTypes.func,
currentMessage: PropTypes.object,
nextMessage: PropTypes.object,
previousMessage: PropTypes.object,
user: PropTypes.object,
containerStyle: PropTypes.shape({
left: ViewPropTypes.style,
right: ViewPropTypes.style,
}),
};
//Bubble
import PropTypes from 'prop-types';
import React from 'react';
import {
Text,
Clipboard,
StyleSheet,
TouchableOpacity,
View,
ViewPropTypes,
Platform,
} from 'react-native';
import { MessageText, MessageImage, Time, utils } from 'react-native-gifted-chat';
const { isSameUser, isSameDay } = utils;
export default class Bubble extends React.Component {
constructor(props) {
super(props);
this.onLongPress = this.onLongPress.bind(this);
}
onLongPress() {
if (this.props.onLongPress) {
this.props.onLongPress(this.context, this.props.currentMessage);
} else {
if (this.props.currentMessage.text) {
const options = [
'Copy Text',
];
const cancelButtonIndex = options.length - 1;
this.context.actionSheet().showActionSheetWithOptions({
options,
cancelButtonIndex,
},
(buttonIndex) => {
switch (buttonIndex) {
case 0:
Clipboard.setString(this.props.currentMessage.text);
break;
}
});
}
}
}
renderMessageText() {
if (this.props.currentMessage.text) {
const { containerStyle, wrapperStyle, messageTextStyle, ...messageTextProps } = this.props;
if (this.props.renderMessageText) {
return this.props.renderMessageText(messageTextProps);
}
return (
<MessageText
{...messageTextProps}
textStyle={{
left: [styles.standardFont, styles.slackMessageText, messageTextProps.textStyle, messageTextStyle],
}}
/>
);
}
return null;
}
renderMessageImage() {
if (this.props.currentMessage.image) {
const { containerStyle, wrapperStyle, ...messageImageProps } = this.props;
if (this.props.renderMessageImage) {
return this.props.renderMessageImage(messageImageProps);
}
return <MessageImage {...messageImageProps} imageStyle={[styles.slackImage, messageImageProps.imageStyle]} />;
}
return null;
}
renderTicks() {
const { currentMessage } = this.props;
if (this.props.renderTicks) {
return this.props.renderTicks(currentMessage);
}
if (currentMessage.user._id !== this.props.user._id) {
return null;
}
if (currentMessage.sent || currentMessage.received) {
return (
<View style={[styles.headerItem, styles.tickView]}>
{currentMessage.sent && <Text style={[styles.standardFont, styles.tick, this.props.tickStyle]}>✓</Text>}
{currentMessage.received && <Text style={[styles.standardFont, styles.tick, this.props.tickStyle]}>✓</Text>}
</View>
);
}
return null;
}
renderUsername() {
const username = this.props.currentMessage.user.name;
if (username) {
const { containerStyle, wrapperStyle, ...usernameProps } = this.props;
if (this.props.renderUsername) {
return this.props.renderUsername(usernameProps);
}
return (
<Text style={[styles.standardFont, styles.headerItem, styles.username, this.props.usernameStyle]}>
{username}
</Text>
);
}
return null;
}
renderTime() {
if (this.props.currentMessage.createdAt) {
const { containerStyle, wrapperStyle, ...timeProps } = this.props;
if (this.props.renderTime) {
return this.props.renderTime(timeProps);
}
return (
<Time
{...timeProps}
containerStyle={{ left: [styles.timeContainer] }}
textStyle={{ left: [styles.standardFont, styles.headerItem, styles.time, timeProps.textStyle] }}
/>
);
}
return null;
}
renderCustomView() {
if (this.props.renderCustomView) {
return this.props.renderCustomView(this.props);
}
return null;
}
render() {
const isSameThread = isSameUser(this.props.currentMessage, this.props.previousMessage)
&& isSameDay(this.props.currentMessage, this.props.previousMessage);
const messageHeader = isSameThread ? null : (
<View style={styles.headerView}>
{this.renderUsername()}
{this.renderTime()}
{this.renderTicks()}
</View>
);
return (
<View style={[styles.container, this.props.containerStyle]}>
<TouchableOpacity
onLongPress={this.onLongPress}
accessibilityTraits="text"
{...this.props.touchableProps}
>
<View
style={[
styles.wrapper,
this.props.wrapperStyle,
]}
>
<View>
{this.renderCustomView()}
{messageHeader}
{this.renderMessageImage()}
{this.renderMessageText()}
</View>
</View>
</TouchableOpacity>
</View>
);
}
}
// Note: Everything is forced to be "left" positioned with this component.
// The "right" position is only used in the default Bubble.
const styles = StyleSheet.create({
standardFont: {
fontSize: 15,
},
slackMessageText: {
marginLeft: 0,
marginRight: 0,
},
container: {
flex: 1,
alignItems: 'flex-start',
},
wrapper: {
marginRight: 60,
minHeight: 20,
justifyContent: 'flex-end',
},
username: {
fontWeight: 'bold',
},
time: {
textAlign: 'left',
fontSize: 12,
},
timeContainer: {
marginLeft: 0,
marginRight: 0,
marginBottom: 0,
},
headerItem: {
marginRight: 10,
},
headerView: {
// Try to align it better with the avatar on Android.
marginTop: Platform.OS === 'android' ? -2 : 0,
flexDirection: 'row',
alignItems: 'baseline',
},
/* eslint-disable react-native/no-color-literals */
tick: {
backgroundColor: 'transparent',
color: 'white',
},
/* eslint-enable react-native/no-color-literals */
tickView: {
flexDirection: 'row',
},
slackImage: {
borderRadius: 3,
marginLeft: 0,
marginRight: 0,
},
});
Bubble.contextTypes = {
actionSheet: PropTypes.func,
};
Bubble.defaultProps = {
touchableProps: {},
onLongPress: null,
renderMessageImage: null,
renderMessageText: null,
renderCustomView: null,
renderTime: null,
currentMessage: {
text: null,
createdAt: null,
image: null,
},
nextMessage: {},
previousMessage: {},
containerStyle: {},
wrapperStyle: {},
tickStyle: {},
containerToNextStyle: {},
containerToPreviousStyle: {},
};
Bubble.propTypes = {
touchableProps: PropTypes.object,
onLongPress: PropTypes.func,
renderMessageImage: PropTypes.func,
renderMessageText: PropTypes.func,
renderCustomView: PropTypes.func,
renderUsername: PropTypes.func,
renderTime: PropTypes.func,
renderTicks: PropTypes.func,
currentMessage: PropTypes.object,
nextMessage: PropTypes.object,
previousMessage: PropTypes.object,
user: PropTypes.object,
containerStyle: PropTypes.shape({
left: ViewPropTypes.style,
right: ViewPropTypes.style,
}),
wrapperStyle: PropTypes.shape({
left: ViewPropTypes.style,
right: ViewPropTypes.style,
}),
messageTextStyle: Text.propTypes.style,
usernameStyle: Text.propTypes.style,
tickStyle: Text.propTypes.style,
containerToNextStyle: PropTypes.shape({
left: ViewPropTypes.style,
right: ViewPropTypes.style,
}),
containerToPreviousStyle: PropTypes.shape({
left: ViewPropTypes.style,
right: ViewPropTypes.style,
}),
};

Why is only the last component in array animating?

Goal: create an OptionFan button that when pressed, rotates on its Z axis, and FanItems release from behind the main button and travel along their own respective vectors.
OptionFan.js:
import React, { useState, useEffect } from 'react';
import { Image, View, Animated, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
import FanItem from './FanItem';
const { height, width } = Dimensions.get('window');
export default class OptionFan extends React.Component {
constructor (props) {
super(props);
this.state = {
animatedRotate: new Animated.Value(0),
expanded: false
};
}
handlePress = () => {
if (this.state.expanded) {
// button is opened
Animated.spring(this.state.animatedRotate, {
toValue: 0
}).start();
this.refs.option.collapse();
this.setState({ expanded: !this.state.expanded });
} else {
// button is collapsed
Animated.spring(this.state.animatedRotate, {
toValue: 1
}).start();
this.refs.option.expand();
this.setState({ expanded: !this.state.expanded });
}
};
render () {
const animatedRotation = this.state.animatedRotate.interpolate({
inputRange: [ 0, 0.5, 1 ],
outputRange: [ '0deg', '90deg', '180deg' ]
});
return (
<View>
<View style={{ position: 'absolute', left: 2, top: 2 }}>
{this.props.options.map((item, index) => (
<FanItem ref={'option'} icon={item.icon} onPress={item.onPress} index={index} />
))}
</View>
<TouchableOpacity style={styles.container} onPress={() => this.handlePress()}>
<Animated.Image
resizeMode={'contain'}
source={require('./src/assets/img/arrow-up.png')}
style={{ transform: [ { rotateZ: animatedRotation } ], ...styles.icon }}
/>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
borderRadius: 30,
backgroundColor: '#E06363',
elevation: 15,
shadowOffset: {
height: 3,
width: 3
},
shadowColor: '#333',
shadowOpacity: 0.5,
shadowRadius: 5,
height: width * 0.155,
width: width * 0.155
},
icon: {
height: width * 0.06,
width: width * 0.06
},
optContainer: {
justifyContent: 'center',
alignItems: 'center',
borderRadius: 30,
backgroundColor: '#219F75',
elevation: 5,
shadowOffset: {
height: 3,
width: 3
},
shadowColor: '#333',
shadowOpacity: 0.5,
shadowRadius: 5,
height: width * 0.13,
width: width * 0.13,
position: 'absolute'
}
});
FanItem.js:
import React, { useState } from 'react';
import { Image, Animated, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import EStyleSheet from 'react-native-extended-stylesheet';
const { width } = Dimensions.get('window');
export default class FanItem extends React.Component {
constructor (props) {
super(props);
this.state = {
animatedOffset: new Animated.ValueXY(0),
animatedOpacity: new Animated.Value(0)
};
}
expand () {
let offset = { x: 0, y: 0 };
switch (this.props.index) {
case 0:
offset = { x: -50, y: 20 };
break;
case 1:
offset = { x: -20, y: 50 };
break;
case 2:
offset = { x: 20, y: 50 };
break;
case 3:
offset = { x: 75, y: -20 };
break;
}
Animated.parallel([
Animated.spring(this.state.animatedOffset, { toValue: offset }),
Animated.timing(this.state.animatedOpacity, { toValue: 1, duration: 600 })
]).start();
}
collapse () {
Animated.parallel([
Animated.spring(this.state.animatedOffset, { toValue: 0 }),
Animated.timing(this.state.animatedOpacity, { toValue: 0, duration: 600 })
]).start();
}
render () {
return (
<Animated.View
style={
(this.props.style,
{
left: this.state.animatedOffset.x,
top: this.state.animatedOffset.y,
opacity: this.state.animatedOpacity
})
}
>
<TouchableOpacity style={styles.container} onPress={this.props.onPress}>
<Image resizeMode={'contain'} source={this.props.icon} style={styles.icon} />
</TouchableOpacity>
</Animated.View>
);
}
}
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
borderRadius: 30,
backgroundColor: '#219F75',
elevation: 5,
shadowOffset: {
height: 3,
width: 3
},
shadowColor: '#333',
shadowOpacity: 0.5,
shadowRadius: 5,
height: width * 0.13,
width: width * 0.13,
position: 'absolute'
},
icon: {
height: width * 0.08,
width: width * 0.08
}
});
Implementation:
import React from 'react';
import { StyleSheet, View, Dimensions } from 'react-native';
import Component from './Component';
const { height, width } = Dimensions.get('window');
const testArr = [
{
icon: require('./src/assets/img/chat.png'),
onPress: () => alert('start chat')
},
{
icon: require('./src/assets/img/white_video.png'),
onPress: () => alert('video chat')
},
{
icon: require('./src/assets/img/white_voice.png'),
onPress: () => alert('voice chat')
},
{
icon: require('./src/assets/img/camera.png'),
onPress: () => alert('request selfie')
}
];
const App = () => {
return (
<View style={styles.screen}>
<Component options={testArr} />
</View>
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#E6E6E6'
}
});
export default App;
Problem: The issue is, only the last FanItem item runs its animation. (opacity, and vector translation). before implementing the opacity animation I could tell the first three FanItems did in fact render behind the main button, because I could see them when pressing the main button, as the opacity temporarily changes for the duration of the button click.
My question is 1) why are the first three mapped items not animating? and 2) how to resolve this?
You are storing ref of FanItem in option. but, ref gets overridden in each iteration of map. so, at the end it only stores ref of last FanItem in option. So, first declare one array in constructor to store ref of each FanItem:
constructor(props) {
super(props);
// your other code
this.refOptions = [];
}
Store ref of each FanItem separately like this:
{this.props.options.map((item, index) => (
<FanItem ref={(ref) => this.refOptions[index] = ref} icon={item.icon} onPress={item.onPress} index={index} />
))}
and then to animate each FanItem:
for(var i = 0; i < this.refOptions.length; i++){
this.refOptions[i].expand(); //call 'expand' or 'collapse' as required
}
This is expo snack link for your reference:
https://snack.expo.io/BygobuL3JL

add additional style if iPhoneX

I have a helper function is.iphone('x') to check for iphone x and I want some styles to get added to the styles.icon and styles.textContainer and styles.container if true. This needs to happen inside the render method. However, when I try to run my code:
const styles = {
addToCartButton: {
borderRadius: 0,
width: windowWidth,
},
container: {
overflow: 'hidden',
},
innerContainer: {
width: 2 * windowWidth,
flexDirection: 'row',
},
checkoutButton: {
borderRadius: 0,
width: windowWidth,
},
icon: {
backgroundColor: accentColor,
},
textContainer: {},
}
export class CartButton extends Component {
checkoutButtonColor = new Animated.Value(3)
xOffset = new Animated.Value(-windowWidth)
dynamicStyles = {
transform: [ { translateX: this.xOffset } ],
}
checkoutDynamicStyles = {
backgroundColor: this.checkoutButtonColor.interpolate({
inputRange: [ 0, 3 ],
outputRange: [ color('b'), accentColor ],
}),
}
animate = () => {
Animated.sequence([
Animated.timing(this.xOffset, {
toValue: 0,
duration: 500,
useNativeDriver: true,
}),
Animated.timing(this.checkoutButtonColor, {
toValue: 0,
duration: 250,
userNativeDriver: true,
}),
]).start()
}
)
}
render () {
if (is.iphone('x')) {
styles.icon.paddingBottom = spacing
styles.textContainer.paddingBottom = spacing
styles.container.marginBottom = spacingSizes.large
}
return (
<View style={styles.container}>
<Animated.View style={[ styles.innerContainer, this.dynamicStyles ]}>
{this.renderCheckout()}
{this.renderAddToCart()}
</Animated.View>
</View>
)
}
}
I get the error "you are attempting to set Key 'paddingBottom' with value '14' (spacing =14) on an object that is meant to be immutable and has been frozen. How to do this any suggestions?
Update your code and replace style={styles.container} by style={[styles.container, is.iphone('x') ? { marginBottom: spacing } : {}]} and do the same where you use the icon style.

Resources