react-native登录之手势登录篇
手势可能是我们使用最多的一种手机操作了,无论是手势解锁,还是手势登录,不知道大家有没有想过,我们每次在九个圆圈上画各种奇奇怪怪的连接线时,手机是怎么记录的呢?难道是记录我们天马行空的线条吗,其实答案简单的令人发指:九个圆圈分别代表数字123456789,我们连线的上下圆圈也就是数字密码的上下文数字,举个栗子,常见的Z型密码,对应的其实是数字密码1235789,所以,无论我们连的多么花里胡哨,手机存储的,永远是9个数字的排列组合。
明白了手势登录的原理,接下来让我们来做一个手势登陆的页面,老规矩先上效果图:
简单分析一下,要实现手势登录的功能,我们需要
- 持久化存储,用来记录我们设置的手势密码
- 一个变量,用来判断当前登录账户是不是第一次使用手势登录,如果是话,设置密码并登录;不是的话,直接输入密码并做校验
- 文本提示信息,用户在输入过程中需要有文本提示当前处于什么状态,引导用户操作
持久化存储方案使用AsyncStorage即可,它相当于浏览器上的LocalStorage,可以在本地长久存储信息,存储方式为key-value结构,另外它只能存储字符串数据,所以如果有数组或者对象需要存储得先转一下字符串;
手势登录组件使用react-native-gesture-password,525个star,想来是不错的(之后被狠狠打脸。。),地址在:https://github.com/Spikef/react-native-gesture-password。
引入组件后,发现了一系列严重的问题:
- 不管怎么滑都不显示轨迹,研究了一番issues后才发现,这个手势组件不能与react-navagation的组件共存,必须把它放到一个单独的页面去。
- 滑动过一次手势后,轨迹显示在页面不会消失。。。最后发现是作者的代码有一个地方写错了。。。
- 安卓上滑动很不灵敏,经常选中位置和手指划过的位置不同
- 原本大圆圈内部滑动显示的小圆圈变成了小方块
- 这个项目已经有四五年了,是一个比较老的项目,而且作者已经说明不再维护了
简直是槽点满满啊,正常情况的话,这样一个组件肯定是要被弃用的,但研究了一下作者的源码,是几个独立的js文件,发现还是可以改造拯救一下的,接下来,我们把整个组件从node_nodules下复制到自己的component文件夹下,开始我们的改造大业。
可以看到核心就是source下的四个js文件,circle.js是圆圈组件、line.js是连接线组件、index.js是整合圆圈与连接线的父组件,也就是我们调用的手势组件,helper.js里写了几个手指滑动角度、方向判断的的辅助函数。
针对上面的问题我们来一一解决:
-
滑动后不消失的问题
我们定位到index.js文件下,**resetActive()**方法是专门用来重置组件的函数,第一句就赫然写着:
this.state.lines = [];
很明显了,state是不能直接赋值的,只能通过setState赋值,我们把它改成
this.setState({
lines:[]})
另外,组件提供了一个interval属性用来设置多长时间后进行重置组件的操作,在index.js文件中onEnd方法
写明了:
if (this.props.interval > 0) {
this.timer = setTimeout(() => this.resetActive(), this.props.interval);
}
只有interval大于0才会执行resetActive()方法,而interval默认值为0,所以我们要在组件中给interval赋值,写个1000吧,一秒后消失比较符合使用习惯。
export function isPointInCircle(point, center, radius) {
let d = getDistance(point, center);
return d <= radius * 2;
}
当radius*2
比较大时,就会造成即使手指在圈外划过也会被识别为选中圆圈的情况,经过试验,将2改为1.5的显示效果是比较好的。
const _styleIner = useMemo(
() => [
!outer && styles.inner,
{
width: (2 * r) / 3,
height: (2 * r) / 3,
borderRadius: r / 3,
},
fill && {
backgroundColor: color },
],
[r, outer, fill, color],
);
高宽相同,边框圆角为高宽的一半,没毛病,圆形就是这么写的啊。但是有一点可能大家会忽略,backgroundColor,如果背景色在初始状态时没有设置,View不会是圆形,而是正方形,即使后续状态加入背景色设置也会无效,依旧是正方形。这一点我查了很多资料都没有相关记载,只是我在实际使用中发现了这样的问题。大家可以写个demo测试一下:
let [pressImg,setPressImg] = useState(false);
return(
<TouchableOpacity onPress={
()=>setPressImg(!pressImg)}>
<View style={
[
{
height: 30,
width:30,
borderRadius:15,
},
pressImg ? {
backgroundColor: '#871241'} : null
]}/>
</TouchableOpacity>
)
在这个demo中pressImg初始状态为false时,View始终为正方形,如果pressImg初始状态为true,则View始终为圆形。
所以在上面的问题中内部圆圈刚开始不显示背景色,只有手指划过才变成蓝色,相当于demo的初始状态为false,所以我们要修改的话就是为内部圆圈在初始状态就赋一个背景颜色值,如透明色:
const _styleIner = useMemo(
() => [
!outer && styles.inner,
{
width: (3 * r) / 3,
height: (3 * r) / 3,
borderRadius: (1.5 * r) / 3,
backgroundColor: fill ? color : "transparent",
},
],
[r, outer, fill, color],
);
这样,手势滑动组件就满足了我们的需要,这时我们已经把该组件复制到了自己的component中,所以记得删掉node_nodules中的该组件,还有package.json文件中的组件引用也要删掉,免得让同事还去下载它~
接下来我们在页面设置判断变量和文本提示信息,完成手势登录页面,关键代码均有注释:
import React, {
useState} from 'react';
import {
View, StyleSheet} from 'react-native';
import PasswordGesture from "@/pages/Account/GestureUnlock/component/react-native-gesture-password/index";
import {
useFocusEffect, useIsFocused} from '@react-navigation/native';
import {
connector, ModelState} from '@/models/connect';
import {
clearStorage, getGesturePassword, deleteGesturePassword, setGesturePassword} from "@/utils/storage";
import LoginSheet from "@/pages/Account/components/LoginSheet";
interface Props extends ModelState {
}
const GestureUnlock: React.FC<Props> = React.memo(props => {
const {
dispatch} = props;
let [isFirstLogin, setIsFirstLogin] = useState(true);
let [gestureInfo, setGestureInfo] = useState({
message: '请输入您的手势密码',
status: 'normal'
});
useFocusEffect(
React.useCallback(() => {
// deleteGesturePassword();
//进入页面首先判断当前AsyncStorage有没有存储手势密码
getGesturePassword().then((gePassword) => {
console.log("一进页面", gePassword);
//如果有手势密码代表之前已经设置过,那么设置文本信息为"请输入您的手势密码"
if (gePassword) {
setIsFirstLogin(false);
setGestureInfo({
message: '请输入您的手势密码',
status: 'normal'
});
} else {
//如果没有,则代表第一次登陆,需要先设置手势密码
setIsFirstLogin(true);
setGestureInfo({
status: 'normal',
message: '第一次登陆,请设置您的手势密码'
});
}
})
return () => {
//卸载组件后需要将手势组件置为待输入状态
setGestureInfo({
message: '请输入您的手势密码',
status: 'normal'
});
};
}, [])
);
//成功后的回调事件
const successVerify = () => {
dispatch({
type: 'user/login',
payload: {
username: "张三",
password: '123456',
}
});
}
//手势输入结束后的回调
const onEnd = (password) => {
console.log("onEnd中password====", password);
/*将当前手势密码和AsyncStorage中存储的手势密码做对比,如果匹配正确代表验证成功;
匹配失败代表输入的密码有误,需要重新输入
* */
getGesturePassword().then((gePassword) => {
console.log("OnEnd=====", gePassword);
if (password == gePassword) {
setGestureInfo({
status: 'right',
message: '密码正确,即将登陆...'
});
successVerify();
} else {
setGestureInfo({
status: 'wrong',
message: '密码错误,请重新输入'
});
return;
}
})
};
//第一次登录设置手势密码输入结束的回调
const settingOnEnd = (password) => {
console.log("password====", password);
getGesturePassword().then((gePassword) => {
console.log("settingOnEnd=====", gePassword);
if (gePassword) {
if (gePassword == password) {
setGestureInfo({
status: 'right',
message: '已确认密码,即将登陆...'
});
successVerify();
} else {
setGestureInfo({
status: 'wrong',
message: '两次输入密码不一致,请重新设置密码'
});
//两次输入不一致把AsyncStorage中存的手势密码清除掉,重新录入
deleteGesturePassword();
}
} else {
setGestureInfo({
status: 'normal',
message: '请再次输入密码并确认'
});
//第一次输入将手势密码存入AsyncStorage,方便和第二次输入的确认密码对比
setGesturePassword(password);
}
})
}
//用户触摸圈圈时触发的事件
const onStart = () => {
setGestureInfo({
status: 'normal',
message: '请输入您的手势密码'
});
};
return (
<View style={
styles.container}>
<PasswordGesture
style={
{
backgroundColor: 'fff'}}
status={
gestureInfo.status}
message={
gestureInfo.message}
allowCross
//设置一次验证后手势连接线多久后消失,不设置会不消失
interval={
1000}
onStart={
() => onStart()}
onEnd={
(password) => isFirstLogin ? settingOnEnd(password) : onEnd(password)}
/>
<LoginSheet/>
</View>
);
});
export default connector(GestureUnlock);
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
说实话,使用这个组件不难,但圆圈变方块的bug让我着实查找、试验了很久,不知道还有没有小伙伴遇到这个情况,请留言与我交流~
最后,这里是一凡的公众号,记录日常的学习与工作思考,大家一起努力!