I want to replicate the long press to record and slide left to cancel of whatsapp/viber messengers.
import React, {useRef, useState} from 'react';
import {
Dimensions,
TextInput,
TouchableWithoutFeedback,
View,
PanResponder,
Animated as NativeAnimated,
} from 'react-native';
import Animated, {Easing} from 'react-native-reanimated';
import styled from 'styled-components';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
const {Value, timing} = Animated;
let isMoving = false;
const width = Dimensions.get('window').width;
const height = Dimensions.get('window').height;
const RecordButton = ({onPress, onPressIn, onPressOut}) => (
<RecordButton.Container
accessibilityLabel="send message"
accessibilityRole="button"
accessibilityHint="tap to send message">
<TouchableWithoutFeedback
delayPressOut={900}
pressRetentionOffset={300}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}>
<RecordButton.Icon />
</TouchableWithoutFeedback>
</RecordButton.Container>
);
RecordButton.Container = styled(View)`
height: 46px;
justify-content: center;
`;
RecordButton.Icon = styled(MaterialCommunityIcons).attrs({
size: 26,
name: 'microphone',
color: 'red',
})``;
const Input = styled(TextInput).attrs((props) => ({}))`
background-color: grey;
border-radius: 10px;
color: black;
flex: 1;
font-size: 17px;
max-height: 180px;
padding: 12px 18px;
text-align-vertical: top;
`;
const App = () => {
const [isFocused, setIsFocused] = useState(false);
const inputBoxTranslateX = useRef(new Value(0)).current;
const contentTranslateY = useRef(new Value(0)).current;
const contentOpacity = useRef(new Value(0)).current;
const textTranslateX = useRef(new Value(-10)).current;
const position = useRef(new NativeAnimated.ValueXY()).current;
const handlePressIn = () => {
setIsFocused(true);
const input_box_translate_x_config = {
duration: 200,
toValue: -width,
easing: Easing.inOut(Easing.ease),
};
const text_translate_x_config = {
duration: 200,
toValue: -50,
easing: Easing.inOut(Easing.ease),
};
const content_translate_y_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
const content_opacity_config = {
duration: 200,
toValue: 1,
easing: Easing.inOut(Easing.ease),
};
timing(inputBoxTranslateX, input_box_translate_x_config).start();
timing(contentTranslateY, content_translate_y_config).start();
timing(contentOpacity, content_opacity_config).start();
timing(textTranslateX, text_translate_x_config).start();
};
const handlePressOut = ({isFromPan, pos}) => {
// console.log(position._value);
if (!isFromPan) {
return;
}
if (isMoving && !isFromPan) {
return;
}
console.log(isMoving);
setIsFocused(false);
const input_box_translate_x_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
const text_translate_x_config = {
duration: 200,
toValue: -10,
easing: Easing.inOut(Easing.ease),
};
const content_translate_y_config = {
duration: 0,
toValue: height,
easing: Easing.inOut(Easing.ease),
};
const content_opacity_config = {
duration: 200,
toValue: 0,
easing: Easing.inOut(Easing.ease),
};
timing(inputBoxTranslateX, input_box_translate_x_config).start();
timing(contentTranslateY, content_translate_y_config).start();
timing(contentOpacity, content_opacity_config).start();
timing(textTranslateX, text_translate_x_config).start();
};
const panResponder = React.useRef(
PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => {
const {dx, dy} = gestureState;
const shouldCap = dx > 2 || dx < -2;
if (shouldCap) {
isMoving = true;
}
return shouldCap;
},
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
const {dx, dy} = gestureState;
const shouldCap = dx > 2 || dx < -2;
if (shouldCap) {
isMoving = true;
}
return shouldCap;
},
onPanResponderMove: NativeAnimated.event(
[null, {dx: position.x, dy: position.y}],
{
useNativeDriver: false,
listener: (event, gestureState) => {
let {pageX, pageY} = event.nativeEvent;
isMoving = true;
console.log({pageX});
if (pageX < width / 2) {
console.log('Message cancelled');
}
},
},
),
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
let {pageX, pageY} = evt.nativeEvent;
isMoving = false;
// if (pageX > 300) {
handlePressOut({isFromPan: true});
// }
NativeAnimated.spring(position, {
toValue: {x: 0, y: 0},
friction: 10,
useNativeDriver: true,
}).start();
},
onPanResponderTerminate: (evt, gestureState) => {},
onShouldBlockNativeResponder: (evt, gestureState) => {
return true;
},
}),
).current;
return (
<View
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
flex: 1,
marginHorizontal: 12,
}}>
<Animated.View
style={{
height: 50,
transform: [{translateX: inputBoxTranslateX}],
flexGrow: 1,
}}>
<Input style={{width: '100%', height: 40}} />
</Animated.View>
<View style={{flexDirection: 'row', alignItems: 'center'}}>
<Animated.View
style={{
opacity: contentOpacity,
transform: [{translateX: textTranslateX}],
}}>
{isFocused ? (
<Animated.Text
style={{
color: 'black',
fontSize: 20,
}}>
Slide Left to cancel
</Animated.Text>
) : null}
</Animated.View>
<NativeAnimated.View
style={[
{
alignItems: 'center',
justifyContent: 'center',
width: 46,
},
{
transform: [
{
translateX: position.x.interpolate({
inputRange: [-width + 80, 0],
outputRange: [-width + 80, 0],
extrapolate: 'clamp',
}),
},
{
scale: position.x.interpolate({
inputRange: [-width - 60, 0],
outputRange: [1.8, 1],
extrapolate: 'clamp',
}),
},
],
},
isFocused
? {
backgroundColor: 'orange',
borderRadius: 10,
}
: {},
]}
{...panResponder.panHandlers}>
<RecordButton
onPressIn={handlePressIn}
onPressOut={() => handlePressOut({pos: position})}
/>
</NativeAnimated.View>
</View>
</View>
);
};
export default App;
snippet above produces the following:
The problems with this snippet are:
- the pan responder of the mic button allows it to move horizontally even if I do not press the button (not happening on video but in real device)
- pan gesture allows moving both left/right while it should be moving only to left
- when the mic button arrives at the middle of the screen, the button should be "released" and return to the initial position.
- when dragging the button, the text "slide to cancel" should move along the button and not stay static.
whatsapp demo:
viber demo: