Slide to record animations in react-native like whatsapp / viber
Asked Answered
K

0

8

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:

enter image description here

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:

enter image description here

viber demo:

enter image description here

Krimmer answered 3/2, 2021 at 7:31 Comment(2)
try this one 1, 2Bridging
@RsD thanks, the first example is for native android, the second one is really good!Krimmer

© 2022 - 2024 — McMap. All rights reserved.