React Native 奇幻之旅(2)-內建組件 TextInput

React Native logo

這是我在2023第十五屆 iThome 鐵人賽發表的系列文章。

https://ithelp.ithome.com.tw/users/20136637/ironman/6408

如果要為APP設計一個共用的輸入框組件會需要有哪些功能和要注意的地方呢?

4qW2SE8
hefp2Yg

根據我常用的APP所設計的輸入框,我總結了幾個基本的需求:

  1. 需給定合適的輸入框寬度、高度和邊距
  2. 樣式類型大致分為:邊框(加圓角)型純底線型無邊框型(融入背景色)
  3. 左右 icon (右邊的 icon 常常會有點擊的需求,比如密碼的顯示和隱藏icon)
  4. 標題、placeholder、提示文字(比如:錯誤訊息)
    • 現在更多的是用 placeholder 取代標題
  5. 一般、錯誤時顯示不同的邊框顏色

調整輸入框樣式

RN內建的TextInput的樣式特點:沒有樣式(x)

  • 預設沒有邊框、底色(透明)、邊距
  • 寬度根據內容長度自動調整
  • iOS 預設的 placeholder 文字顏色很淡,需要另外設置 placeholderTextColor

要給定邊框和底色才看的見輸入框:

tThEBtf
(上面那炷香真的是輸入框沒錯…)

<TextInput
    style={{
        backgroundColor: 'white',
        borderWidth: 1,
        borderRadius: 4,
        borderColor: '#cbcbcb'
    }}
    placeholderTextColor="#676767"
/>

給定寬度後不管內容多長輸入框都會維持住固定的寬度:

<TextInput
  style={{
      width: 250,
      height: 40,
      backgroundColor: 'white',
      borderWidth: 1,
      borderRadius: 4,
      borderColor: '#cbcbcb'
  }}
  placeholderTextColor="#676767"
/>

補充:

  1. 如果要實現錯誤時顯示紅色邊框也很簡單,直接動態調整邊框顏色即可borderColor: !!error ? '#ff0000' : '#cbcbcb'
  2. 只想要底線則可以將 borderWidth 改為 borderBottomWidth

輸入框Icon

clearButtonMode(iOS)

TextInput 有一個 clearButtonMode 屬性(僅限iOS)用於顯示文字清除按鈕,點擊後可以清除輸入框文字。

T0StSKn

缺點:

  • 不能修改圖示、調整樣式
  • 該屬性僅限 iOS 使用
<TextInput
    style={{
      width: 250,
      height: 40,
      backgroundColor: 'white',
      borderWidth: 1,
      borderRadius: 4,
      borderColor: '#cbcbcb'
    }}
    placeholderTextColor="#676767"
    clearButtonMode="always"
/>

Android 沒有類似的屬性,因此為了各系統icon保持一致的樣式,通常會選擇自己刻一個而不是使用 clearButtonMode

實現左右icon

這邊以搜索輸入框和密碼輸入框為例,常見的搜索框左側會有放大鏡圖示,而密碼輸入框右側會有顯示/隱藏按鈕:

m2DTlRc

因為 TextInput 本身是沒有提供 icon 的屬性,所以如果要實現這種效果需要另外封裝 TextInput,藉由傳入的 leftIcon, rightIcon 屬性決定在輸入框左右側顯示什麼樣的icon。

import React from 'react'
import { StyleSheet, View, TextInput, type TextInputProps } from 'react-native'

interface InputProps extends TextInputProps {
  leftIcon?: JSX.Element
  rightIcon?: JSX.Element
}

export const Input = ({ style, leftIcon, rightIcon, ...restProps }: InputProps) => {
  return (
      <View style={[styles.inputContainer, style]}>
        {!!leftIcon && leftIcon}
        <TextInput
          style={styles.input}
          placeholderTextColor="#676767"
          {...restProps}
        />
        {!!rightIcon && rightIcon}
      </View>
  )
}

const styles = StyleSheet.create({
  inputContainer: {
    display: 'flex',
    flexDirection: 'row',
    alignItems: 'center',
    width: 250,
    height: 40,
    backgroundColor: 'white',
    borderWidth: 1,
    borderRadius: 4
  },
  input: {
    flex: 1,
    paddingHorizontal: 8
  }
})

使用方式:

<Input leftIcon={<Icon name="search" size={24} color="#cbcbcb" />} />
<Input
    rightIcon={
      <TouchableOpacity onPress={() => setVisible(prev => !prev)}>
        <Icon name={visible ? "visibility" : "visibility-off"} size={24} color="#cbcbcb" />
      </TouchableOpacity>
    }
    secureTextEntry={!visible}
/>

注意

  1. 因為 TextInput 本身是沒有樣式,而 TextInput 加上左右側 icon 後才能算是一個完整的輸入框,所以邊框、寬高都要設置在最外層的 View 而不是 TextInput
  2. 建議將基本的 Input 和帶有 icon 的 Input 拆分成不同的組件,以避免組件在不同情境中使用產生的副作用。(SOLID 中的 Open-Closed Principle)

鍵盤類型

TextInput 有兩個關於鍵盤類型的屬性:inputMode, keyboardType,這兩個屬性用於決定鍵盤上面顯示的內容(例如設為 numeric 就是純數字鍵盤)

  • inputMode: 'decimal', 'email', 'none', 'numeric', 'search', 'tel', 'text', 'url'
  • keyboardType: 'default', 'email-address', 'numeric', 'phone-pad', 'ascii-capable', 'numbers-and-punctuation', 'url', 'number-pad', 'name-phone-pad', 'decimal-pad', 'twitter', 'web-search', 'visible-password'
<TextInput inputMode="numeric" />
<TextInput inputMode="decimal" />
<TextInput keyboardType="phone-pad" />

keyboardType 和 inputMode 基本上沒有什麼區別,只不過 keyboardType 比 inputMode 多了幾個僅Android和僅iOS的類型,實作上我個人更偏向使用 keyboardType,不過兩個都可以滿足基本需求。

下圖為 keyboardType 各值所顯示的鍵盤內容:

Eh4EdSN

圖片來源於:https://lefkowitz.me/visual-guide-to-react-native-textinput-keyboardtype-options/

注意事項

多行文本框

因為 TextInput 本身並沒有樣式,所以要實作 TextArea 也滿簡單的,只需要調整樣式…而已嗎?
其實不是。TextInput 預設只能輸入一行內容,所以不管你輸入多長的內容它都會一直往後顯示,不會換行也不能換行,因此除了樣式之外,還需要設置 TextInput 中的 multiline 屬性來允許換行輸入。

Android

TextInput 還有一個 numberOfLines 屬性用於設置輸入框預設顯示幾列高度,但這個屬性僅限 Android 使用,在 iOS 無效。

<TextInput
  style={{
    width: 220,
    paddingVertical: 8,
    paddingHorizontal: 12
  }}
  placeholderTextColor="#676767"
  multiline={true}
  numberOfLines={4}
/>

注意事項

  1. 雖然 Android 設置 numberOfLines={4} 後輸入框預設高度變成四列了,但因為沒有設置高度的關係,當內容超過四列時輸入框就會自動延伸,最後變成巨無霸(x)
  2. 輸入框內容預設是垂直置中顯示,所以要設 textAlignVertical="top" 才會從最上方開始顯示。
    fm0pJk3
<TextInput
  style={{
    width: 220,
    height: 100, // 不給 height 的話輸入框會無限延長
    paddingVertical: 8,
    paddingHorizontal: 12
  }}
  placeholderTextColor="#676767"
  multiline={true}
  numberOfLines={4}
  textAlignVertical="top"
/>

iOS

iOS 沒有 numberOfLines 屬性,但只需要自行設置輸入框高度就行:

<TextInput
  style={{
    width: 220,
    height: 100,
    paddingVertical: 8,
    paddingHorizontal: 10
  }}
  placeholderTextColor="#676767"
  multiline={true}
/>

固定位置的Icon

有時候會遇到給 TextArea 加上 icon 的需求,比如像是 chatGPT 的輸入框右下角會有一個送出的 icon,不管輸入多少行內容它永遠固定在輸入框的右下方:

WkfTYhR

實現方式也很簡單,就跟封裝單行輸入框是一樣的方式,只不過右側 icon 的樣式需要加上 position: 'absolute'right, bottom,將 icon 固定在右下角:

export const TextArea = ({ style, height = 100, icon, ...restProps }: TextAreaProps) => {
  return (
    <View style={styles.container}>
        <TextInput
          style={[styles.input, style, { height }]}
          placeholderTextColor="#676767"
          multiline={true}
          numberOfLines={4}
          textAlignVertical="top"
          {...restProps}
        />
        {!!icon && (
          <Icon name={icon} style={styles.icon} onPress={//...} />
        )}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    width: 250,
    borderWidth: 1,
    backgroundColor: 'white',
    marginVertical: 5,
    borderRadius: 4,
  },
  input: {
    width: 220,
    paddingVertical: 8,
    paddingHorizontal: Platform.OS === 'android' ? 12 : 10
  },
  icon: {
    position: 'absolute',
    right: 6,
    bottom: 10
  }
})
ZQqOW2r

參考資料

I2W2sO0

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *