react-native-speech-bubble
Version:
A speech bubble dialog component for React Native.
256 lines (230 loc) • 6.82 kB
JavaScript
import PropTypes from 'prop-types';
import React from 'react';
import {
Animated,
Image,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
import TypeWriter from './TypeWriter';
const propTypes = {
allowSkip: PropTypes.bool,
allowSpeechReplay: PropTypes.bool,
onSpeechEnd: PropTypes.func,
onSpeechNext: PropTypes.func,
onSpeechReplay: PropTypes.func,
speaker: PropTypes.string,
speakerStyle: View.propTypes.style,
speakerTextStyle: Text.propTypes.style,
speechBubbleActiveOpacity: PropTypes.number,
speechBubbleStyle: View.propTypes.style,
speechBubbleTextStyle: View.propTypes.style,
speeches: PropTypes.array.isRequired,
typeWriterStyle: Text.propTypes.style,
nextStyle: View.propTypes.style,
style: View.propTypes.style,
hideIcons: PropTypes.bool,
};
const styles = StyleSheet.create({
dialog: {
flexDirection: 'row',
borderRadius: 50,
backgroundColor: 'white',
marginLeft: 20,
marginRight: 20,
marginBottom: 20,
paddingLeft: 20,
paddingRight: 10,
paddingTop: 20,
paddingBottom: 20,
overflow: 'hidden',
},
dialogText: {
flexShrink: 1,
},
dialogNext: {
padding: 5,
justifyContent: 'center',
backgroundColor: 'transparent',
},
typeWriter: {
backgroundColor: 'transparent',
},
});
class SpeechBubble extends React.Component {
constructor(props) {
super(props);
this.state = {
speechIndex: 0,
speechBubbleScale: new Animated.Value(1),
nextDialogAnimation: new Animated.ValueXY(),
replaySpeechBubbleAnimation: new Animated.Value(0),
typeEnd: false,
lastSpeech: false,
};
this.onSpeechBubblePress = this.onSpeechBubblePress.bind(this);
this.onSpeechBubblePressIn = this.onSpeechBubblePressIn.bind(this);
this.onSpeechBubblePressOut = this.onSpeechBubblePressOut.bind(this);
this.nextSpeechBubbleAnimation = this.nextSpeechBubbleAnimation.bind(this);
this.replaySpeechBubbleAnimation = this.replaySpeechBubbleAnimation.bind(this);
}
componentDidMount() {
this.nextSpeechBubbleAnimation();
this.replaySpeechBubbleAnimation();
}
onSpeechBubblePress() {
const { allowSkip, speechIndex, typeEnd } = this.state;
const { onSpeechNext, onSpeechReplay, speeches } = this.props;
if (allowSkip || typeEnd) {
if (speechIndex + 1 < speeches.length) {
const lastSpeech = (speechIndex + 1) + 1 === speeches.length;
// Next speech
if (onSpeechNext) {
onSpeechNext();
}
this.setState({
speechIndex: speechIndex + 1,
typeEnd: false,
lastSpeech,
});
} else {
// Replay speech
if (onSpeechReplay) {
onSpeechReplay();
}
if (this.props.allowSpeechReplay) {
this.setState({
speechIndex: 0,
typeEnd: false,
lastSpeech: speeches.length === 1,
});
}
}
}
}
onSpeechBubblePressIn() {
Animated.spring(this.state.speechBubbleScale, {
toValue: 0.9,
tension: 40,
friction: 3,
useNativeDriver: true,
}).start();
}
onSpeechBubblePressOut() {
Animated.spring(this.state.speechBubbleScale, {
toValue: 1,
tension: 40,
friction: 3,
useNativeDriver: true,
}).start();
}
nextSpeechBubbleAnimation() {
const anim = this.state.nextDialogAnimation;
const translateDuration = 200;
Animated.sequence([
Animated.timing(anim.y, {
toValue: -5,
duration: translateDuration,
useNativeDriver: true,
}),
Animated.timing(anim.y, {
toValue: 5,
duration: translateDuration,
useNativeDriver: true,
}),
]).start(() => {
this.nextSpeechBubbleAnimation();
});
}
get NextSpeechBubble() {
return !this.state.lastSpeech ? (
<Animated.View
style={[
styles.dialogNext,
this.props.nextStyle,
{ transform: [{ translateY: this.state.nextDialogAnimation.y }] },
{ opacity: this.state.typeEnd && !this.state.lastSpeech ? 1 : 0 },
]}
>
<Image source={require('./assets/ic_touch_app.png')} />
</Animated.View>
) : null;
}
replaySpeechBubbleAnimation() {
const anim = this.state.replaySpeechBubbleAnimation;
const rotateDuration = 750;
Animated.sequence([
Animated.timing(anim, {
toValue: 100,
duration: rotateDuration,
useNativeDriver: true,
}),
Animated.delay(1000),
Animated.timing(anim, {
toValue: 0,
duration: rotateDuration,
useNativeDriver: true,
}),
]).start(() => {
this.replaySpeechBubbleAnimation();
});
}
get ReplaySpeechBubble() {
const interpolatedRotateAnimation = this.state.replaySpeechBubbleAnimation.interpolate({
inputRange: [0, 100],
outputRange: ['0deg', '360deg'],
});
return this.state.lastSpeech ? (
<Animated.View
style={[
styles.dialogNext,
this.props.nextStyle,
{ transform: [{ rotate: interpolatedRotateAnimation }] },
{ opacity: this.state.typeEnd && this.state.lastSpeech ? 1 : 0 },
]}
>
<Image
source={require('./assets/ic_replay.png')}
/>
</Animated.View>
) : null;
}
render() {
return (
<View style={this.props.style}>
<View style={this.props.speakerStyle}>
<Text style={this.props.speakerTextStyle}> {this.props.speaker} </Text>
</View>
<TouchableOpacity
style={{ transform: [{ scale: this.state.speechBubbleScale }] }}
activeOpacity={this.props.speechBubbleActiveOpacity || 0.6}
onPress={this.onSpeechBubblePress}
onPressIn={this.onSpeechBubblePressIn}
onPressOut={this.onSpeechBubblePressOut}
>
<View style={this.props.speechBubbleStyle || styles.dialog}>
<View style={[styles.dialogText, this.props.speechBubbleTextStyle]}>
<TypeWriter
text={this.props.speeches[this.state.speechIndex]}
typing={1}
onTypingEnd={() => {
if (this.props.onSpeechEnd) {
this.props.onSpeechEnd();
}
this.setState({ typeEnd: true });
}}
typeWriterStyle={this.props.typeWriterStyle || styles.typeWriter}
/>
</View>
{!this.props.hideIcons && this.NextSpeechBubble}
{!this.props.hideIcons && this.ReplaySpeechBubble}
</View>
</TouchableOpacity>
</View>
);
}
}
SpeechBubble.propTypes = propTypes;
export default SpeechBubble;