滑动以像whatsapp / viber一样在react-native中录制动画

Sta*_*nas 8 react-native react-animated react-native-reanimated

我想复制长按来录制并向左滑动来取消whatsapp/viber Messenger。


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;


Run Code Online (Sandbox Code Playgroud)

上面的代码片段产生以下结果:

在此输入图像描述

该片段的问题是:

  • 即使我不按下按钮,麦克风按钮的平移响应器也允许它水平移动(不会在视频上发生,但在真实设备中发生)
  • 平移手势允许向左/向右移动,而它应该只向左移动
  • 当麦克风按钮到达屏幕中间时,按钮应该被“释放”并返回到初始位置。
  • 拖动按钮时,文本“滑动取消”应沿着按钮移动,而不是保持静止。

WhatsApp 演示:

在此输入图像描述

振动演示:

在此输入图像描述