React Native를 사용하여 애니메이션 텍스트 필드 만들기

5년 전에 나는 react-native-textinput-effects도서관을 창설했다.그 뒤에 있는Animated 도서관은 그때 이후 큰 변화가 없었다.그래서 나는 같은 원칙을 사용하여 처음부터 새로운 React NativeTextInput 구성 요소를 구축할 것이다.
이번에 나는 material-ui 애니메이션을 실현할 것이다.이것은 간단한 예이기 때문에 나는 이것이React Native 애니메이션의 기초 지식을 이해하는 유용한 예라고 생각한다.
Github에서 이 구성 요소의 전체 버전을 찾을 수 있습니다.react-native-web 덕분에 나는 리액션 코드를 웹에 이식하는 데도 성공했다.너는 내 핸드폰에서 놀 수 있다blog.
미리 보기에서 보듯이 TextField에는 다음과 같은 네 가지 주요 UI 상태가 있습니다.
  • 비어 있고 초점이 맞지 않는 상태
  • 초점 상태
  • 채우기 및 초점 미초점 상태
  • 오류 상태
  • 기본 텍스트 필드부터 시작


    기본 TextField 부터 시작하여 react native Text Input을 확장하고 스타일을 설정합니다.
    import React from 'react';
    import { StyleSheet, TextInput } from 'react-native';
    
    // extend from native TextInput props
    type Props = React.ComponentProps<typeof TextInput>;
    
    const TextField: React.FC<Props> = (props) => {
      /*
       ** spread operator helps to extract style prop and assign
       ** any remaining props to the `restOfProps` variable.
       ** it is pretty handy here as we need to support
       ** all the props the native TextInput component has.
       */
      const { style, ...restOfProps } = props;
      return <TextInput style={[style, styles.input]} {...restOfProps} />;
    };
    
    const styles = StyleSheet.create({
      input: {
        padding: 24,
        borderColor: '#B9C4CA',
        borderWidth: 1,
        borderRadius: 4,
        fontFamily: 'Avenir-Medium',
        fontSize: 16
      }
    });
    
    export default TextField;
    
    출력은 경계선과 자리 표시자 텍스트가 있는 간단한 텍스트 입력입니다.
    <TextField placeholder="Cardholder name" />
    

    태그 만들기

    placeholder 입력이 비어 있을 때만 프롬프트가 탭 역할을 합니다.이것은 우리의 사례에 있어서 아직 부족하기 때문에 사용자 정의 탭을 만들어서 입력의 맨 위에 표시해야 합니다.
    부모View로 TextInput을 포장하고 다른 View를 탭으로 만드는 형제항을 만듭니다. 탭을 맞춤형으로 만들 수 있는 공간이 넓어집니다.우리는 라벨 position: absolute 양식을 사용하여 TextInput 의 맨 위에 있는지 확인할 것입니다.TextInput라는 새 아이템으로 원생label 모듈 아이템을 확장하였습니다. 이것은 TextField의 유일한 아이템이 될 것입니다.
    -type Props = React.ComponentProps<typeof TextInput>
    +type Props = React.ComponentProps<typeof TextInput> & {
    +  label: string
    +}
    
     const TextField: React.FC<Props> = (props) => {
    -  const { style, ...restOfProps } = props
    +  const { label, style, ...restOfProps } = props
       return (
    -    <TextInput
    -      style={[style, styles.input]}
    -      {...restOfProps}
    -    />
    +    <View style={style}>
    +      <TextInput style={styles.input} {...restOfProps} />
    +      <View style={styles.labelContainer}>
    +        <Text style={styles.label}>{label}</Text>
    +      </View>
    +    </View>
       )
     }
    
     const styles = StyleSheet.create({
    +  labelContainer: {
    +    position: 'absolute',
    +    left: 16,
    +    top: -6,
    +    paddingHorizontal: 8,
    +    backgroundColor: 'white',
    +  },
    +  label: {
    +    fontFamily: 'Avenir-Heavy',
    +    fontSize: 12,
    +  },
    
    TextField 지금 보니까 이렇게 생겼어요.

    초점 상태 기반 태그 찾기


    탭은 초점 상태에 따라 입력한 중심과 위쪽 사이를 이동해야 합니다.우선 애니메이션 없이 내부isFocused 상태에 따라 탭을 간단하게 배치합시다.
    우리는 TextInputsonBluronFocus 방법을 감청하고 그것들에 따라 isFocused 상태를 수정할 수 있다.top 상태에서 라벨을 조종하는 isFocused 양식에 따라 라벨을 재배치하기에 충분하다.또한 레이블의 글꼴 크기와 색상도 수정할 것입니다.
    const TextField: React.FC<Props> = (props) => {
       const {
         label,
         style,
    +    onBlur,
    +    onFocus,
         ...restOfProps
       } = props
    +  const [isFocused, setIsFocused] = useState(false)
    
       return (
         <View style={style}>
    -      <TextInput style={styles.input} {...restOfProps} />
    -      <View style={styles.labelContainer}>
    -        <Text style={styles.label}>{label}</Text>
    +      <TextInput
    +        style={styles.input}
    +        {...restOfProps}
    +        onBlur={(event) => {
    +          setIsFocused(false)
    +          onBlur?.(event)
    +        }}
    +        onFocus={(event) => {
    +          setIsFocused(true)
    +          onFocus?.(event)
    +        }}
    +      />
    +      <View
    +        style={[
    +          styles.labelContainer,
    +          {
    +            top: isFocused ? -6 : 24,
    +          },
    +        ]}
    +      >
    +        <Text
    +          style={[
    +            styles.label,
    +            {
    +              fontSize: isFocused ? 12 : 16,
    +              color: isFocused ? '#080F9C' : '#B9C4CA',
    +            },
    +          ]}
    +        >
    +          {label}
    +        </Text>
           </View>
         </View>
    

    태그에 애니메이션 설정하기


    우리는 지금 초점 상태에 기반한 라벨을 가지고 있다.React Native는 애니메이션을 만들 수 있는 내장Animated 구성 요소를 가지고 있습니다. 이것은 우리의 간단한 애니메이션을 지원하기에 충분합니다.포커스 상태를 표시하기 위해 Animated.Value 를 만들고, 포커스 스타일을 표시하기 위해 삽입할 것입니다.Animated.Value 숫자 매개 변수를 받아들이기 때문에 우리는 숫자로 isFocused 상태를 표시해야 한다.나는 0을 사용하여 초점이 맞지 않는 상태를 표시하고, 1은 초점이 맞지 않는 상태를 표시할 것이다.
       const [isFocused, setIsFocused] = useState(false)
    
    +  const focusAnim = useRef(new Animated.Value(0)).current
    +
    +  /*
    +  ** This effect will trigger the animation every
    +  ** time `isFocused` value changes.
    +  */
    +  useEffect(() => {
    +    Animated.timing(focusAnim, {
    +      toValue: isFocused ? 1 : 0,
    +      // I took duration and easing values
    +      // from material.io demo page
    +      duration: 150,
    +      easing: Easing.bezier(0.4, 0, 0.2, 1),
    +      // we'll come back to this later
    +      useNativeDriver: false,
    +    }).start()
    +  }, [focusAnim, isFocused])
    +
       return (
         <View style={style}>
    -      <View
    +      <Animated.View
             style={[
               styles.labelContainer,
               {
    -            top: isFocused ? -6 : 24,
    +            top: focusAnim.interpolate({
    +              inputRange: [0, 1],
    +              outputRange: [24, -6],
    +            }),
               },
             ]}
           >
    -        <Text
    +        <Animated.Text
               style={[
                 styles.label,
                 {
    -              fontSize: isFocused ? 12 : 16,
    +              fontSize: focusAnim.interpolate({
    +                inputRange: [0, 1],
    +                outputRange: [16, 12],
    +              }),
                   color: isFocused ? '#080F9C' : '#B9C4CA',
                 },
               ]}
             >
               {label}
    -        </Text>
    -      </View>
    +        </Animated.Text>
    +      </Animated.View>
         </View>
       )
     }
    

    네이티브 드라이버 사용


    우리의 애니메이션은 현재 효과가 매우 좋다.그러나 우리는 useNativeDriver 파라미터를 Animated API에 전달함으로써 저급 장치에서 더욱 매끄럽게 할 수 있다.
    다음은 React Nativedocumentation에 대한 설명입니다.

    By using the native driver, we send everything about the animation to native before starting the animation, allowing native code to perform the animation on the UI thread without having to go through the bridge on every frame. Once the animation has started, the JS thread can be blocked without affecting the animation.


    문제는 이 드라이버는 유한한 속성 집합, 예를 들어 transformopacity를 사용할 수 있다는 것이다.그래서 topfontSize 속성에 적용되지 않기 때문에 지원하는 속성으로 대체해야 합니다.Animated 설정useNativeDriver: true 시 예외 발생:

    다행히도 transform 여기서 같은 애니메이션 행동을 만들 수 있습니다.우리는 scale 속성 대체fontSize를 사용하고 translateY를 사용하여 탭을 이동할 것이다.불행하게도, scale 변환을 사용하면 라벨이 x축에서 이동할 수 있습니다.내가 찾을 수 있는 유일한 해결 방법은 추가 translateX 변환을 만들고 수동으로 x축 이동을 취소하는 것이다.
             style={[
               styles.labelContainer,
               {
    -            top: focusAnim.interpolate({
    -              inputRange: [0, 1],
    -              outputRange: [24, -6],
    -            }),
    +            transform: [
    +              {
    +                scale: focusAnim.interpolate({
    +                  inputRange: [0, 1],
    +                  outputRange: [1, 0.75],
    +                }),
    +              },
    +              {
    +                translateY: focusAnim.interpolate({
    +                  inputRange: [0, 1],
    +                  outputRange: [24, -12],
    +                }),
    +              },
    +              {
    +                translateX: focusAnim.interpolate({
    +                  inputRange: [0, 1],
    +                  outputRange: [16, 0],
    +                }),
    +              },
    +            ],
               },
             ]}
           >
    -        <Animated.Text
    +        <Text
               style={[
                 styles.label,
                 {
    -              fontSize: focusAnim.interpolate({
    -                inputRange: [0, 1],
    -                outputRange: [16, 12],
    -              }),
                   color: isFocused ? '#080F9C' : '#B9C4CA',
                 },
               ]}
             >
               {label}
    -        </Animated.Text>
    +        </Text>
           </Animated.View>
         </View>
       )
    
    이제 useNativeDriver: trueAnimated에 전달하여 이 드라이버를 사용할 수 있습니다.

    생성 오류 상태


    이것은 우리가 지원해야 할 최종 텍스트 필드 상태입니다.errorText라는 새 아이템을 간단하게 정의하고, 이 아이템이 비어 있지 않을 때 탭과 테두리 색을 수정합니다.
    type Props = React.ComponentProps<typeof TextInput> & {
       label: string
    +  errorText?: string | null
    }
    
    const TextField: React.FC<Props> = (props) => {
       const {
         label,
    +    errorText,
         style,
         onBlur,
         onFocus,
         ...restOfProps
       } = props
    
    +  let color = isFocused ? '#080F9C' : '#B9C4CA'
    +  if (errorText) {
    +    color = '#B00020'
    +  }
    +
       return (
         <View style={style}>
           <TextInput
    -        style={styles.input}
    +        style={[
    +          styles.input,
    +          {
    +            borderColor: color,
    +          },
    +        ]}
             {...restOfProps}
             onBlur={(event) => {
               setIsFocused(false)
    @@ -72,13 +83,15 @@ const TextField: React.FC<Props> = (props) => {
               style={[
                 styles.label,
                 {
    -              color: isFocused ? '#080F9C' : '#B9C4CA',
    +              color,
                 },
               ]}
             >
               {label}
    +          {errorText ? '*' : ''}
             </Text>
           </Animated.View>
    +      {!!errorText && <Text style={styles.error}>{errorText}</Text>}
         </View>
       )
     }
    
     const styles = StyleSheet.create({
    +  error: {
    +    marginTop: 4,
    +    marginLeft: 12,
    +    fontSize: 12,
    +    color: '#B00020',
    +    fontFamily: 'Avenir-Medium',
    +  },
     })
    

    마지막 윤색


    TextField는 현재 보기에는 괜찮지만, 몇 가지 작은 문제를 해결해야 한다.
    첫 번째 문제는 우리가 입력한 텍스트가 isFocused: false에서 사라진다는 것이다.따라서 입력한 값이 비어 있지 않을 때 항상 태그를 맨 위에 배치해야 합니다.
      const {
        label,
        errorText,
    +   value,
        style,
        onBlur,
        onFocus,
        ...restOfProps
      } = props
       const [isFocused, setIsFocused] = useState(false)
    
       const focusAnim = useRef(new Animated.Value(0)).current
    
       useEffect(() => {
         Animated.timing(focusAnim, {
    -      toValue: isFocused ? 1 : 0,
    +      toValue: isFocused || !!value ? 1 : 0,
           duration: 150,
           easing: Easing.bezier(0.4, 0, 0.2, 1),
           useNativeDriver: true,
         }).start()
    -  }, [focusAnim, isFocused])
    +  // make sure you are passing `value` to the dependency array
    +  // so the effect will be run anytime the value changes.
    +  }, [focusAnim, isFocused, value]
    
    두 번째 문제는 공백으로 입력한 탭을 눌렀을 때입니다.클릭한 요소는 하나Text가 아니라 하나TextInput일 뿐이므로 React Native는 키보드를 트리거하지 않습니다.따라서 탭을 단추로 만들고 입력 초점 이벤트를 수동으로 터치해야 합니다.
    // create an input ref
    const inputRef = useRef<TextInput>(null)
    
    // pass `inputRef` to the TextInput
    <TextInput ref={inputRef} />
    
    // wrap label View with `TouchableWithoutFeedback`
    <TouchableWithoutFeedback onPress={() => inputRef.current?.focus()}>
    

    출력


    다음은 TextField의gif 미리보기입니다.

    마찬가지로 Github에서 전체 버전을 찾을 수 있습니다.

    좋은 웹페이지 즐겨찾기