從零開始構建React Native數字鍵盤功能

發表於2024-02-29
首發於公眾號 前端混合開發,歡迎關注。

現代移動應用程式在入門過程中經常涉及一個步驟,你需要輸入傳送到你的電子郵件或手機號碼的驗證碼 PIN。有時,你需要使用類似於分割 OTP 輸入欄位的東西來輸入 PIN。另一種輸入驗證碼 PIN 的方式是使用撥號盤。

"OTP" 指的是 "一次性密碼" (One-Time Password)。這是一種安全機制,用於透過簡訊或電子郵件向使用者傳送一次性使用的密碼或驗證碼,以驗證使用者的身份。

在這篇文章中,我們將展示如何為 React Native 應用建立一個定製的數字鍵盤。構建一個定製的 React Native 數字鍵盤可以作為分割輸入或傳統 TextInput 元素的優秀替代品,以個性化你的移動應用設計。

你可以檢視我們的React Native專案的完整原始碼,並隨著我們一步步設定數字鍵盤進行跟蹤。讓我們開始吧。

在React Native應用中數字鍵盤的使用場景

在React Native應用中,有許多專業的數字鍵盤使用場景。

一個常見的例子是一次性密碼(OTP)輸入驗證。例如,假設你在新使用者入門過程中,向他們的手機傳送了一個OTP。傳送OTP後,使用者將被引導到一個螢幕上,使用數字鍵盤輸入並驗證它。

另一個使用場景是為你的應用新增一層安全防護,這對於包含敏感資訊的應用來說非常重要。當你的使用者重新登入你的應用時,你可以為他們展示一個數字鍵盤,他們可以在此輸入一個PIN碼,你的應用在讓他們登入前需要驗證這個PIN碼。

在我們的教程中,我們將建立這第二種用例的一個簡單示例。我們將看到如何在 React Native 中從頭開始設定一個數字鍵盤,以便使用者可以建立一個 PIN 並使用該 PIN 登入應用。

設定開發環境

執行以下命令以快速啟動一個Expo應用:

npx create-expo-app my-app

上述命令將建立我們所需的基礎React Native專案檔案。完成後,啟動iOS或Android模擬器上的開發伺服器:

//for iOS
npm run ios 

//for Android
npm run android

image.png

這是你專案資料夾中 App.js 檔案內程式碼的輸出。

建立、渲染和設計React Native數字鍵盤

在這個部分,我們將開始建立三個螢幕: LoginCustomDialpadHome

Login 螢幕將是使用者初次載入應用時看到的第一個螢幕。它將有一個按鈕,可以將使用者引導到 CustomDialpad 螢幕,在那裡他們可以輸入他們的PIN碼。一旦輸入正確的PIN碼,應用將會將使用者引導到 Home 螢幕。

我們開始構建我們的React Native應用程式,包含這三個螢幕。首先,安裝我們需要設定和配置React Native基本導航的以下包:

npx install @react-navigation/native @react-navigation/native-stack react-native-safe-area-context react-native-screens

另外,建立一個名為 screens 的資料夾,並在其中放入三個檔案: Login.jsxCustomDialPad.jsxHomeScreen.jsx

接下來,在你的 App.js 檔案中,按照下面所示實現基本的導航:

import { StyleSheet } from "react-native";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import HomeScreen from "./screens/HomeScreen";
import Login from "./screens/Login";
import CustomDialPad from "./screens/CustomDialPad";

const Stack = createNativeStackNavigator();
export default function App() {
 return (
   <NavigationContainer>
     <Stack.Navigator
       screenOptions={{
         headerShown: false,
       }}
     >
       <Stack.Screen name="Login" component={Login} />
       <Stack.Screen name="Dialpad" component={CustomDialPad} />
       <Stack.Screen name="Home" component={HomeScreen} />
     </Stack.Navigator>
   </NavigationContainer>
 );
}

const styles = StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: "#fff",
   alignItems: "center",
   justifyContent: "center",
 },
});

在這裡,我們將整個應用程式包裹在 NavigationContainer 內,並使用 Stack.Navigator 元件來管理螢幕堆疊。當使用者導航到一個螢幕時,它會被推到堆疊的頂部。然後,當使用者導航到另一個頁面時,它會從堆疊頂部彈出螢幕。

在這種情況下,堆疊頂部的初始螢幕將是 Login 螢幕。當使用者按下按鈕導航到 CustomDialpad 螢幕時, CustomDialpad 螢幕會被推到 Login 螢幕的上方,依此類推:

現在螢幕導航已經全部設定好了,我們可以開始設定數字鍵盤的邏輯和使用者介面。

設定 CustomDialpad.jsx 檔案

在根目錄中建立一個 component 資料夾,並在其中新增一個 DialpadKeypad.jsx 檔案。稍後我們將在此檔案中構建我們的數字鍵盤介面和功能。

然後,在 CustomDialpad.jsx 檔案中匯入 DialpadKeypad.jsx 元件檔案:

//CustomDialPad.js

import {
 SafeAreaView,
 StyleSheet,
 Text,
 View,
 Dimensions,
} from "react-native";
import React, { useState } from "react";
import { Ionicons } from "@expo/vector-icons";
import DialpadKeypad from "../components/DialpadKeypad";

const { width, height } = Dimensions.get("window");

我們還使用了 Dimensions.get 方法來提取使用者裝置的螢幕 widthheight 。這將幫助我們確保我們的使用者介面能夠響應不同的螢幕尺寸進行適應。

接下來,為了開始構建我們的React Native數字鍵盤,我們首先需要建立一些變數:

const dialPadContent = [1, 2, 3, 4, 5, 6, 7, 8, 9, "", 0, "X"];

const dialPadSize = width * 0.2;
const dialPadTextSize = dialPadSize * 0.4;

const pinLength = 4;

我們簡要回顧一下每個變數的目的:

  • dialPadContent — 我們將在數字鍵盤 UI 上顯示的內容。在這種情況下,我們想要顯示一個由十二個值組成的陣列,這些值被排列在一個三列四行的網格中。
  • pinLength — 使用者應輸入的PIN碼長度。我們希望使用者輸入一個四位數的PIN碼,但這可以根據你的專案需求進行調整。
  • dialPadSize — 數字鍵盤的大小,由手機螢幕的 width 乘以 0.2 得出,佔螢幕 width 的20%
  • dialPadTextSize — 顯示在數字鍵盤內的文字大小,由將 dialPadSize 值乘以 0.4 得到 dialPadSize 的40%來確定

CustomDialpad.jsx 檔案的其餘部分,我們定義了 CustomDialPad 元件,並使用 useNavigation Hook使我們能夠控制螢幕導航。我們還設定了元件結構和樣式,並匯出自定義元件,使其可以在應用的其他部分中使用:

const CustomDialPad = () => {
 const navigation = useNavigation();
 return (
   <SafeAreaView style={styles.container}>
     <View style={styles.textContainer}>
       <TouchableOpacity
         onPress={() => navigation.goBack()}
         style={{ position: "absolute", top: -5, left: 10 }}
       >
         <Ionicons name="ios-chevron-back" size={45} color="#5E454B" />
       </TouchableOpacity>
       <Text style={styles.pinText}>Create PIN</Text>
       <Text style={styles.pinSubText}>Enter your secure four-digit code</Text>
       <DialpadKeypad
         dialPadContent={dialPadContent}
         pinLength={pinLength}
         dialPadSize={dialPadSize}
         dialPadTextSize={dialPadTextSize}
       />
     </View>
   </SafeAreaView>
 );
};

export default CustomDialPad;

const styles = StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: "#FAF0E6",
 },
 textContainer: {
   justifyContent: "center",
   alignItems: "center",
   marginTop: 40,
   position: "relative",
 },
 pinText: {
   fontSize: 30,
   fontWeight: "medium",
   color: "#5E454B",
 },
 pinSubText: {
   fontSize: 18,
   fontWeight: "medium",
   color: "#5E454B",
   marginVertical: 30,
 },
}

此外,我們使用 TouchableOpacity 元件實現了一個返回按鈕,使使用者能夠透過 navigation.goBack() 方法返回到 Login 頁面。

設定 DialpadKeypad.jsx 檔案

現在讓我們在 DialpadKeypad.js 檔案中工作。首先,我們將匯入所有必要的模組和元件:

import {
 StyleSheet,
 Text,
 View,
 FlatList,
 TouchableOpacity,
} from "react-native";
import React from "react";
import { Feather } from "@expo/vector-icons";

接下來,讓我們拿到我們在 CustomDialPad.js 檔案中傳遞給元件的屬性,並用它們來構建鍵盤的使用者介面。然後,我們將使用 Flatlist 來渲染我們之前定義的 dialPadContent 陣列。

const DialpadKeypad = ({
 dialPadContent,
 pinLength,
 navigation,
 dialPadSize,
 dialPadTextSize,
}) => {
 return (
   <FlatList
     data={dialPadContent}
     numColumns={3} // set number of columns
     keyExtractor={(_, index) => index.toString()}
     renderItem={({ item }) => {
       return (
         <TouchableOpacity
           disabled={item === ""} // make the empty space on the dialpad content unclickable
          >
           <View
             style={[
               {
                 backgroundColor: item === "" ? "transparent" : "#fff",
                 width: dialPadSize,
                 height: dialPadSize,
               },
               styles.dialPadContainer,
             ]}
           >
             {item === "X" ? (
               <Feather name="delete" size={24} color="#3F1D38" />
             ) : (
               <Text
                 style={[{ fontSize: dialPadTextSize }, styles.dialPadText]}
               >
                 {item}
               </Text>
             )}
           </View>
         </TouchableOpacity>
       );
     }}
   />
 );
};

我們將 numColumns 屬性設定為 3 ,以便在三列中渲染我們的 dialPadContent 陣列。陣列中的空白 "" 值使我們可以使渲染的三列四行數字鍵盤在視覺上更加平衡。

在數字鍵盤上,我們使空白按鈕不能被按壓,並移除了它的背景色。我們還為陣列中對應 X 值的按鈕渲染了一個刪除圖示。對於數字鍵盤上的其餘按鈕,我們渲染了陣列中的數字。

我們還將 View 元件包裹在 TouchableOpacity 元件內,以渲染 dialpadContent

我們在這個檔案中的最後一步是定義我們元件的樣式:

export default DialpadKeypad;

const styles = StyleSheet.create({
 dialPadContainer: {
   justifyContent: "center",
   alignItems: "center",
   margin: 10,
   borderRadius: 50,
   borderColor: "transparent",
 },
 dialPadText: {
   color: "#3F1D38",
 },
});

我們看看我們目前擁有的React Native數字鍵盤:

image.png

整合並限制點選功能

我們設定在鍵盤上按下按鈕時的功能。我們使用一個初始資料型別為陣列的狀態來跟蹤鍵盤上每個按鈕按下的值。然後,這將作為一個屬性傳遞給 DialpadKeypad 元件。

DialpadKeypad 檔案中,我們將採用 codesetCode 屬性,並使用它們來實現所需的功能。當點選 Keypad 內容時,我們將首先呼叫 onPress 屬性進行檢查:

  • 如果按下的按鈕的值為 X 。如果是這樣,它應該刪除陣列中的最後一個專案——換句話說,刪除最後選擇的PIN值。
  • 如果按下的按鈕的值是除了 X 之外的任何值。如果是,它應該使用 setCode 屬性將選中的專案新增到程式碼陣列中。
  • 如果程式碼陣列的長度等於 pinLength - 1 。如果是這樣,應該將使用者導航到 Home 螢幕。

我們使用 pinLength - 1code 屬性的長度進行對比,是因為所需的 pinLength 被指定為 4 。

如果 code 狀態陣列中有四個專案,長度將為 3 ,因為陣列中的索引值從 0 開始。因此,一旦將四位數的PIN輸入到 code 陣列中,我們就使用 pinLength -1 來導航到 Home 螢幕。

為了實現所有這些,我們需要像這樣更新 CustomDialPad.js 檔案中的程式碼:

const CustomDialPad = () => {
 const navigation = useNavigation();
 const [code, setCode] = useState([]);

// rest of the code 

  <DialpadKeypad
         dialPadContent={dialPadContent}
         pinLength={pinLength}
         setCode={setCode}
         code={code}
         dialPadSize={dialPadSize}
         dialPadTextSize={dialPadTextSize}
       />

同樣地,像這樣更新 DialpadKeypad.js 檔案:

const DialpadKeypad = ({
 dialPadContent,
 pinLength,
 code,
 setCode,
 navigation,
 dialPadSize,
 dialPadTextSize,
}) => {

// rest of the code
    <TouchableOpacity
           disabled={item === ""} // make the empty space on the dialpad content unclickable
           onPress={() => {
             if (item === "X") {
               setCode((prev) => prev.slice(0, -1));
             } else {
               if (code.length === pinLength - 1) {
                 navigation.navigate("Home");
               }
               setCode((prev) => [...prev, item]);
             }
           }}
         >

為輸入的PIN新增一個 MultiView

在這一部分,我們將新增一個 MultiView。在這個例項中,這是一個檢視,允許我們檢視所選輸入 — 換句話說,就是輸入的 PIN 碼。

首先,在元件資料夾中建立一個 DialpadPin.js 檔案,並在 CustomDialPad 元件中渲染它。然後,我們將 pinLengthpinSizecodedialPadContent 屬性傳遞給 DialpadPin.js 檔案。

DialpadPin.js 檔案中,我們將根據我們之前設定的 4 的PIN長度渲染一個 View 。我們希望在 CustomDialpad 螢幕上將其作為四個均勻分佈的圓形排列在輸入PIN的提示和數字鍵盤之間顯示。

在渲染的檢視內部,我們還將渲染 PIN 值,這將讓我們知道是否已選擇了一個值。如果從鍵盤上選擇了一個值,我們將在 MultiView 中顯示它,這樣使用者就知道他們當前在輸入中選擇了多少位數字。

要實現所有這些,請按照以下方式更新 CustomDialPad.js 檔案:

const dialPadContent = [1, 2, 3, 4, 5, 6, 7, 8, 9, "", 0, "X"];

const dialPadSize = width * 0.2;
const dialPadTextSize = dialPadSize * 0.4;

const pinLength = 4;
const pinContainerSize = width / 2;
const pinSize = pinContainerSize / pinLength;
const CustomDialPad = () => {
 const fontsLoaded = useCustomFonts();
 const navigation = useNavigation();
 const [code, setCode] = useState([])

// rest of the code 

       <DialpadPin
         pinLength={pinLength}
         pinSize={pinSize}
         code={code}
         dialPadContent={dialPadContent}
       />

然後,也更新 DialpadPin.js 檔案:

import { StyleSheet, Text, View } from "react-native";
import React from "react";

const DialpadPin = ({ pinLength, pinSize, code, dialPadContent }) => {
 return (
   <View style={styles.dialPadPinContainer}>
     {Array(pinLength)
       .fill()
       .map((_, index) => {
         const item = dialPadContent[index];
         const isSelected =
           typeof item === "number" && code[index] !== undefined;
         return (
           <View
             key={index}
             style={{
               width: pinSize,
               height: pinSize,
               borderRadius: pinSize / 2,
               overflow: "hidden",
               margin: 5,
             }}
           >
             <View
               style={[
                 {
                   borderRadius: pinSize / 2,
                   borderColor: !isSelected ? "lightgrey" : "#3F1D38",
                 },
                 styles.pinContentContainer,
               ]}
             >
               {isSelected && (
                 <View
                   style={[
                     {
                       width: pinSize * 0.5,
                       height: pinSize * 0.5,
                       borderRadius: pinSize * 0.35,
                     },
                     styles.pinContent,
                   ]}
                 />
               )}
             </View>
           </View>
         );
       })}
   </View>
 );
};

export default DialpadPin;

const styles = StyleSheet.create({
 dialPadPinContainer: {
   flexDirection: "row",
   marginBottom: 30,
   alignItems: "flex-end",
 },
 pinContentContainer: {
   flex: 1,
   backgroundColor: "#fff",
   borderWidth: 1,
   justifyContent: "center",
   alignItems: "center",
 },
 pinContent: {
   backgroundColor: "#5E454B",
 },
});

現在,讓我們看看我們有什麼:

我們可以進一步去動畫化從數字鍵盤中選中的點狀引腳。在 DialpadPin.jsx 檔案中,匯入 Animated 庫,這是React Native提供的開箱即用的。然後,用 Animated.View 包裹顯示點狀選擇的 View 。

 {isSelected && (
    <Animated.View
     style={[
         {
           width: pinSize * 0.5,
           height: pinSize * 0.5,
           borderRadius: pinSize * 0.35,
          },
     styles.pinContent,
    ]}
   />
  )}

現在我們將建立一個 useEffect 鉤子,每當程式碼的值發生變化時都會觸發。每當使用者在鍵盤上輸入一個數字,都會使用 Animation.timing 方法觸發動畫。 animatedValue 將從其當前值動畫過渡到 code.length 值,過程持續 300 毫秒。

const DialpadPin = ({ pinLength, pinSize, code, dialPadContent }) => {
 const animatedValue = useRef(new Animated.Value(0)).current;

 useEffect(() => {
   Animated.timing(animatedValue, {
     toValue: code.length,
     duration: 300,
     useNativeDriver: true,
   }).start();
 }, [code]);

接下來,我們將使用 animatedStyle 樣式物件在鍵盤上選擇數字時應用縮放轉換:

 const animatedStyle = {
   transform: [
     {
       scale: animatedValue.interpolate({
         inputRange: [index, index + 1],
         outputRange: [1, 1.3],
         extrapolate: "clamp",
       }),
     },
   ],
 };

我們在這裡使用了 interpolate 方法將輸入值對映到輸出值,確保動畫的流暢。 inputRangeoutputRange 屬性定義了插值的值。

最後, extrapolate 屬性定義了輸出值的行為。它的 clamp 值表示輸出值在定義的範圍內被限制。將 animatedStyle 物件新增到 Animated.View 的樣式輸入中:

  {isSelected && (
   <Animated.View
     style={[
       {
         width: pinSize * 0.5,
         height: pinSize * 0.5,
         borderRadius: pinSize * 0.35,
       },
       styles.pinContent,
       animatedStyle,
     ]}
   />
  )}

我們新增動畫後的最終結果應如下所示:

如你所見,彩色的點首先以稍微小一些的形式出現在 MultiView 氣泡中,然後擴大以更完全地填充氣泡。這使我們的數字鍵盤功能在不過分分散注意力的情況下,以一種微妙的方式變得更具視覺吸引力。

附加說明和建議

為了在真實的React Native應用中改進這個數字鍵盤的實現,我們需要設定一個後端服務來與我們的前端實現進行通訊。讓我們回顧一下這對我們每個用例會涉及到什麼。

我們討論的第一個用例是在新使用者註冊過程中,使用數字鍵盤驗證傳送到使用者手機或電子郵件的一次性密碼。因此,當有新使用者註冊你的應用時,你需要:

  • 驗證他們用來註冊的電子郵件
  • 從你的後端服務傳送一次性密碼
  • 指導他們到一個包含數字鍵盤的螢幕,他們可以在那裡輸入你傳送到他們郵箱的一次性密碼

現在,使用者需要使用數字鍵盤輸入他們收到的OTP。理想情況下,當他們輸入完整的OTP後,你應該能夠向後端的 verify 端點傳送請求,以驗證你傳送給該使用者的OTP是否與他們在前端輸入的匹配

  • 如果匹配,將他們導航至 Home 螢幕
  • 如果不匹配,顯示一個定製的錯誤資訊,告訴他們輸入的PIN碼錯誤,他們應該輸入傳送到他們郵箱的正確PIN碼

在我們當前的專案中,我們沒有驗證PIN,因為我們沒有設定後端服務。然而,如果你在一個真實的專案中設定這個, verify 端點應該在 DialpadKeypad.js 檔案中被呼叫,我們在那裡檢查 code.lengthpinLength

<TouchableOpacity
  disabled={item === ""} // 使撥號盤內容上的空白區域不可點選
  onPress={() => {
  if (item === "X") {
   setCode((prev) => prev.slice(0, -1));
   } else {
    if (code.length === pinLength - 1) {
     // 一旦使用者輸入了所需的 PIN 長度,在這裡呼叫端點
     navigation.navigate("Home");
    }
    setCode((prev) => [...prev, item]);
    }
    }}
   >

我們討論的第二個用例是使用數字鍵盤進行登入安全。就像第一個用例一樣,你可以在你的應用程式中自定義數字鍵盤,顯示在你的登入頁面上。

使用者在註冊時可以輸入一個PIN碼。然後,當使用者重新輸入他們的PIN碼以重新登入應用時,你可以讓你的後端端點驗證在註冊期間建立的密碼是否與正在輸入的密碼匹配。

如果你的後端端點驗證了匹配,你可以允許使用者登入。如果沒有,你可以顯示一個定製的警告訊息 - 例如, Pin does not match

這個用例確保使用者在沒有必要的安全檢查的情況下,不會僅僅進入應用程式。

比較建立自定義數字鍵盤的方法

React Native支援幾種不同的建立數字鍵盤的方法。例如,我們可以使用 TextInput 元件,並將鍵盤型別作為 numeric 來設定我們的數字鍵盤。然而,這種方法存在一些已知的問題:

  • 點選元件外部時無法消除:這個問題意味著即使你在 TextInput 外部點選,數字鍵盤仍然保持開啟狀態。解決這個問題的可能方法是使用 TouchableWithoutFeedback API元件,在你點選它外部時消除 TextInput 鍵盤。
  • 按返回鍵未能消除:這個問題意味著當你按下返回鍵時,數字鍵盤不會自動消失

也有一些現有的開源庫提供數字鍵盤功能,包括 React Native NumpadReact Native Numeric Pad。然而,這些庫在功能和可定製性方面有些限制。

在許多情況下,你的React Native應用可能有獨特的設計和特定的需求,關於數字鍵盤功能應該如何構建和實施。構建自定義功能意味著你不會受到庫的能力的限制。

此外,在你的React Native應用程式中安裝過多的包會使其變得臃腫。自行構建功能並減少安裝的包可以幫助減小應用程式的大小。

最後,庫可能不會持續活躍地維護,甚至可能完全被遺棄,這可能會導致你的應用崩潰。如果你選擇使用第三方庫,始終嘗試使用穩定且維護良好的選項。

你選擇的方法取決於你的專案需求。例如,使用庫可以幫助你節省大量的開發時間。然而,如果你需要特定的功能或定製,那麼投入時間來構建你自己的可能會更好。

總結

在這篇文章中,我們學習瞭如何在React Native中建立自定義數字鍵盤。我們還將我們的方法與其他選項進行了比較,比如內建的 TextInput 元件和第三方開源庫,以更好地理解何時以及為什麼要從頭開始構建這個功能。

自定義數字鍵盤是一款出色的移動應用功能,適用於像使用一次性密碼驗證使用者或讓他們使用PIN登入等情況。你可以在這個倉庫中找到我們演示專案的完整原始碼。

交流

首發於公眾號 大遷世界,歡迎關注。📝 每週一篇實用的前端文章 🛠️ 分享值得關注的開發工具 ❓ 有疑問?我來回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章