├── .npmignore ├── .gitignore ├── .prettierrc ├── screenshot.gif ├── index.d.ts ├── package.json ├── LICENSE ├── source ├── helper.js ├── circle.js ├── line.js └── index.js ├── example ├── check_password.js ├── set_password.js └── prefer_ES5.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | example/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | node_modules 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel", 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spikef/react-native-gesture-password/master/screenshot.gif -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ViewStyle, TextStyle } from 'react-native'; 3 | 4 | export type GesPasswordStates = 'normal' | 'right' | 'wrong' 5 | 6 | export interface GesturePasswordProps { 7 | style?: ViewStyle, 8 | transparentLine?: boolean, 9 | textStyle?: TextStyle, 10 | message?: string, 11 | normalColor?: string, 12 | rightColor?: string, 13 | wrongColor?: string, 14 | status?: GesPasswordStates, 15 | interval?: number, 16 | allowCross?: boolean, 17 | innerCircle?: boolean, 18 | outerCircle?: boolean 19 | onStart?: () => void, 20 | onEnd?: (password: string) => void, 21 | onReset?: () => void 22 | } 23 | 24 | declare class GesturePassword extends React.Component { } 25 | export default GesturePassword; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-gesture-password", 3 | "version": "0.4.0", 4 | "description": "A gesture password component for React Native (web). It supports both iOS and Android since it's' written in pure JavaScript.", 5 | "main": "source/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Spikef/react-native-gesture-password.git" 12 | }, 13 | "keywords": [ 14 | "react-component", 15 | "react-native", 16 | "ios", 17 | "android", 18 | "gesture password" 19 | ], 20 | "author": { 21 | "name": "Spikef", 22 | "email": "Spikef@Foxmail.com" 23 | }, 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/spikef/react-native-gesture-password/issues" 27 | }, 28 | "peerDependencies": { 29 | "react-native": ">=15.5" 30 | }, 31 | "dependencies": { 32 | "prop-types": "^15.6.0" 33 | }, 34 | "devDependencies": { 35 | "prettier": "^1.19.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /source/helper.js: -------------------------------------------------------------------------------- 1 | export function isPointInCircle(point, center, radius) { 2 | let d = getDistance(point, center); 3 | 4 | return d <= radius; 5 | } 6 | 7 | export function getDistance(pt1, pt2) { 8 | let a = Math.pow(pt1.x - pt2.x, 2); 9 | let b = Math.pow(pt1.y - pt2.y, 2); 10 | let d = Math.sqrt(a + b); 11 | 12 | return d; 13 | } 14 | 15 | export function getTransform(pt1, pt2) { 16 | let d = getDistance(pt1, pt2); 17 | 18 | let c = (pt2.x - pt1.x) / d; 19 | let a = Math.acos(c); // 旋转角度 20 | if (pt1.y > pt2.y) a = 2 * Math.PI - a; 21 | 22 | let c1 = { 23 | x: pt1.x + d / 2, 24 | y: pt1.y, 25 | }; 26 | let c2 = { 27 | x: (pt2.x + pt1.x) / 2, 28 | y: (pt2.y + pt1.y) / 2, 29 | }; 30 | let x = c2.x - c1.x; 31 | let y = c2.y - c1.y; 32 | 33 | return { d, a, x, y }; 34 | } 35 | 36 | export function isEquals(pt1, pt2) { 37 | return pt1.x === pt2.x && pt1.y === pt2.y; 38 | } 39 | 40 | export function getMiddlePoint(pt1, pt2) { 41 | return { 42 | x: (pt2.x + pt1.x) / 2, 43 | y: (pt2.y + pt1.y) / 2, 44 | }; 45 | } 46 | 47 | export function getRealPassword(str) { 48 | return str.replace(/\d/g, function($0) { 49 | return Number($0) + 1; 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /source/circle.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo } from "react"; 2 | import { StyleSheet, View } from "react-native"; 3 | import PropTypes from "prop-types"; 4 | 5 | const Circle = memo(({ color, normalColor, fill, x, y, r, inner, outer }) => { 6 | const _styleContainer = useMemo( 7 | () => [ 8 | styles.outer, 9 | { 10 | left: x - r, 11 | top: y - r, 12 | width: 2 * r, 13 | height: 2 * r, 14 | borderRadius: r, 15 | }, 16 | { borderColor: normalColor }, 17 | fill && { borderColor: color }, 18 | !outer && { borderWidth: 0 }, 19 | ], 20 | [x, r, y, normalColor, color, fill, outer], 21 | ); 22 | 23 | const _styleIner = useMemo( 24 | () => [ 25 | !outer && styles.inner, 26 | { width: (2 * r) / 3, height: (2 * r) / 3, borderRadius: r / 3 }, 27 | fill && { backgroundColor: color }, 28 | ], 29 | [r, outer, fill, color], 30 | ); 31 | 32 | return ( 33 | {inner && } 34 | ); 35 | }); 36 | 37 | Circle.propTypes = { 38 | color: PropTypes.string, 39 | fill: PropTypes.bool, 40 | x: PropTypes.number, 41 | y: PropTypes.number, 42 | r: PropTypes.number, 43 | inner: PropTypes.bool, 44 | outer: PropTypes.bool, 45 | }; 46 | 47 | Circle.defaultProps = { 48 | inner: true, 49 | outer: true, 50 | }; 51 | 52 | const styles = StyleSheet.create({ 53 | outer: { 54 | position: "absolute", 55 | borderColor: "#8E91A8", 56 | borderWidth: 1, 57 | alignItems: "center", 58 | justifyContent: "center", 59 | }, 60 | inner: { 61 | backgroundColor: "#8E91A8", 62 | }, 63 | }); 64 | 65 | export default Circle; 66 | 67 | module.exports = Circle; // for compatible with require only 68 | -------------------------------------------------------------------------------- /example/check_password.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | AppRegistry, 4 | StyleSheet, 5 | Text, 6 | View 7 | } from 'react-native'; 8 | 9 | import PasswordGesture from 'react-native-gesture-password'; 10 | 11 | var Password1 = '123'; 12 | 13 | class AppDemo extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | message: 'Please input your password.', 19 | status: 'normal' 20 | } 21 | } 22 | 23 | onEnd(password) { 24 | if (password == Password1) { 25 | this.setState({ 26 | status: 'right', 27 | message: 'Password is right, success.' 28 | }); 29 | 30 | // your codes to close this view 31 | } else { 32 | this.setState({ 33 | status: 'wrong', 34 | message: 'Password is wrong, try again.' 35 | }); 36 | } 37 | } 38 | 39 | onStart() { 40 | this.setState({ 41 | status: 'normal', 42 | message: 'Please input your password.' 43 | }); 44 | } 45 | 46 | onReset() { 47 | this.setState({ 48 | status: 'normal', 49 | message: 'Please input your password (again).' 50 | }); 51 | } 52 | 53 | render() { 54 | return ( 55 | this.onStart()} 60 | onEnd={(password) => this.onEnd(password)} 61 | innerCircle={true} 62 | outerCircle={true} 63 | /> 64 | ); 65 | } 66 | } 67 | 68 | AppRegistry.registerComponent('AppDemo', () => AppDemo); -------------------------------------------------------------------------------- /source/line.js: -------------------------------------------------------------------------------- 1 | import { isEquals, getTransform } from "./helper"; 2 | import React, { PureComponent } from "react"; 3 | import { StyleSheet, View } from "react-native"; 4 | import PropTypes from "prop-types"; 5 | export default class Line extends PureComponent { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { ...props }; 10 | } 11 | 12 | setNativeProps(props) { 13 | this.setState(props); 14 | } 15 | 16 | static getDerivedStateFromProps(props, state) { 17 | if (props.color !== state.color) { 18 | return { 19 | color: props.color, 20 | }; 21 | } 22 | return null; 23 | } 24 | 25 | render() { 26 | let { start, end, color, transparentLine } = this.state; 27 | 28 | if (isEquals(start, end)) return null; 29 | 30 | let transform = getTransform(start, end); 31 | let length = transform.d; 32 | let angle = transform.a + "rad"; 33 | let moveX = transform.x; 34 | let moveY = transform.y; 35 | 36 | //for transparent line 37 | color = transparentLine ? "#00000000" : color; 38 | 39 | return ( 40 | 59 | ); 60 | } 61 | } 62 | 63 | Line.propTypes = { 64 | color: PropTypes.string, 65 | start: PropTypes.shape({ 66 | x: PropTypes.number, 67 | y: PropTypes.number, 68 | }), 69 | end: PropTypes.shape({ 70 | x: PropTypes.number, 71 | y: PropTypes.number, 72 | }), 73 | }; 74 | 75 | Line.defaultProps = { 76 | color: "#8E91A8", 77 | start: { x: 0, y: 0 }, 78 | end: { x: 0, y: 0 }, 79 | }; 80 | 81 | const styles = StyleSheet.create({ 82 | line: { 83 | position: "absolute", 84 | height: 1, 85 | }, 86 | }); 87 | 88 | module.exports = Line; // for compatible with require only 89 | -------------------------------------------------------------------------------- /example/set_password.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | AppRegistry, 4 | StyleSheet, 5 | Text, 6 | View 7 | } from 'react-native'; 8 | 9 | import PasswordGesture from 'react-native-gesture-password'; 10 | 11 | var Password1 = ''; 12 | 13 | class AppDemo extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | message: 'Please input your password.', 19 | status: 'normal' 20 | } 21 | } 22 | 23 | onEnd(password) { 24 | if ( Password1 === '' ) { 25 | // The first password 26 | Password1 = password; 27 | this.setState({ 28 | status: 'normal', 29 | message: 'Please input your password secondly.' 30 | }); 31 | } else { 32 | // The second password 33 | if ( password === Password1 ) { 34 | this.setState({ 35 | status: 'right', 36 | message: 'Your password is set to ' + password 37 | }); 38 | 39 | Password1 = ''; 40 | // your codes to close this view 41 | } else { 42 | this.setState({ 43 | status: 'wrong', 44 | message: 'Not the same, try again.' 45 | }); 46 | } 47 | } 48 | } 49 | 50 | onStart() { 51 | if ( Password1 === '') { 52 | this.setState({ 53 | message: 'Please input your password.' 54 | }); 55 | } else { 56 | this.setState({ 57 | message: 'Please input your password secondly.' 58 | }); 59 | } 60 | } 61 | 62 | render() { 63 | return ( 64 | this.onStart()} 69 | onEnd={(password) => this.onEnd(password)} 70 | innerCircle={true} 71 | outerCircle={true} 72 | /> 73 | ); 74 | } 75 | } 76 | 77 | AppRegistry.registerComponent('AppDemo', () => AppDemo); -------------------------------------------------------------------------------- /example/prefer_ES5.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var { 3 | AppRegistry, 4 | StyleSheet, 5 | Text, 6 | View 7 | } = require('react-native'); 8 | 9 | var PasswordGesture = require('react-native-gesture-password'); 10 | 11 | var Password1 = ''; 12 | var AppDemo = React.createClass({ 13 | // Example for check password 14 | onEnd: function(password) { 15 | if (password == '123') { 16 | this.setState({ 17 | status: 'right', 18 | message: 'Password is right, success.' 19 | }); 20 | 21 | // your codes to close this view 22 | } else { 23 | this.setState({ 24 | status: 'wrong', 25 | message: 'Password is wrong, try again.' 26 | }); 27 | } 28 | }, 29 | onStart: function() { 30 | this.setState({ 31 | status: 'normal', 32 | message: 'Please input your password.' 33 | }); 34 | }, 35 | onReset: function() { 36 | this.setState({ 37 | status: 'normal', 38 | message: 'Please input your password (again).' 39 | }); 40 | }, 41 | // Example for set password 42 | /* 43 | onEnd: function(password) { 44 | if ( Password1 === '' ) { 45 | // The first password 46 | Password1 = password; 47 | this.setState({ 48 | status: 'normal', 49 | message: 'Please input your password secondly.' 50 | }); 51 | } else { 52 | // The second password 53 | if ( password === Password1 ) { 54 | this.setState({ 55 | status: 'right', 56 | message: 'Your password is set to ' + password 57 | }); 58 | 59 | Password1 = ''; 60 | // your codes to close this view 61 | } else { 62 | this.setState({ 63 | status: 'wrong', 64 | message: 'Not the same, try again.' 65 | }); 66 | } 67 | } 68 | }, 69 | onStart: function() { 70 | if ( Password1 === '') { 71 | this.setState({ 72 | message: 'Please input your password.' 73 | }); 74 | } else { 75 | this.setState({ 76 | message: 'Please input your password secondly.' 77 | }); 78 | } 79 | }, 80 | */ 81 | getInitialState: function() { 82 | return { 83 | message: 'Please input your password.', 84 | status: 'normal' 85 | } 86 | }, 87 | render: function() { 88 | return ( 89 | this.onStart()} 94 | onEnd={(password) => this.onEnd(password)} 95 | /> 96 | ); 97 | } 98 | }); 99 | 100 | AppRegistry.registerComponent('AppDemo', () => AppDemo); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-gesture-password 2 | 3 | A gesture password component for React Native (web). It supports both iOS, Android and Web since it's written in pure JavaScript. 4 | 5 | 一个React Native的手势密码组件,纯JavaScript实现,因此同时支持iOS、安卓和Web平台。 6 | 7 | ![image](https://github.com/Spikef/react-native-gesture-password/raw/master/screenshot.gif) 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm install react-native-gesture-password --save 13 | npm install prop-types --save 14 | ``` 15 | 16 | ## Properties 17 | 18 | All properties bellow are optional. 19 | 20 | ### message (string) 21 | 22 | The message text you want to show. NOTE: If you leave this blank, no message appears for any state changes. 23 | 24 | ### status (string) 25 | 26 | Can be 'normal', 'right' or 'wrong'. 27 | 28 | The gesture password don't validate your password. You should do that yourself, and tell the result by status. 29 | 30 | ### style (string) 31 | 32 | Styles for the gesture password view. 33 | 34 | ### textStyle (string) 35 | Style for the text element in the view. 36 | 37 | ### normalColor (string) 38 | 39 | Use this color to render the default circle color. 40 | 41 | ### rightColor (string) 42 | 43 | Use this color to render when status !== 'wrong'. 44 | 45 | ### wrongColor (string) 46 | 47 | Use this color to render when status === 'wrong'. 48 | 49 | ### transparentLine (boolean) 50 | 51 | True for transparent line. 52 | 53 | ### interval (number) 54 | 55 | The active circles will be reset automatically after you give an interval. 56 | 57 | ### allowCross (boolean) 58 | 59 | Allow cross the circles(eg: 1 -> 7 -> 4), default is false. 60 | 61 | ### onStart (function) 62 | 63 | Event raised when user touch a number circle. 64 | 65 | ### onEnd (function) 66 | 67 | Event raised when user finish input a password. 68 | 69 | ### onReset (function) 70 | 71 | Event raised after the reset interval has cleared circles. Can be used to reset message. 72 | 73 | ### children 74 | 75 | Other components that you want to display. 76 | 77 | ### outerCircle and innerCircle (boolean) 78 | 79 | Control whether to draw outer and inner circle, true default. 80 | 81 | ## Examples 82 | 83 | ```jsx harmony 84 | 85 | var React = require('react'); 86 | var { 87 | AppRegistry, 88 | } = require('react-native'); 89 | 90 | var PasswordGesture = require('react-native-gesture-password'); 91 | 92 | var Password1 = ''; 93 | var AppDemo = React.createClass({ 94 | // Example for check password 95 | onEnd: function(password) { 96 | if (password == '123') { 97 | this.setState({ 98 | status: 'right', 99 | message: 'Password is right, success.' 100 | }); 101 | 102 | // your codes to close this view 103 | } else { 104 | this.setState({ 105 | status: 'wrong', 106 | message: 'Password is wrong, try again.' 107 | }); 108 | } 109 | }, 110 | onStart: function() { 111 | this.setState({ 112 | status: 'normal', 113 | message: 'Please input your password.' 114 | }); 115 | }, 116 | onReset: function() { 117 | this.setState({ 118 | status: 'normal', 119 | message: 'Please input your password (again).' 120 | }); 121 | }, 122 | // Example for set password 123 | /* 124 | onEnd: function(password) { 125 | if ( Password1 === '' ) { 126 | // The first password 127 | Password1 = password; 128 | this.setState({ 129 | status: 'normal', 130 | message: 'Please input your password secondly.' 131 | }); 132 | } else { 133 | // The second password 134 | if ( password === Password1 ) { 135 | this.setState({ 136 | status: 'right', 137 | message: 'Your password is set to ' + password 138 | }); 139 | 140 | Password1 = ''; 141 | // your codes to close this view 142 | } else { 143 | this.setState({ 144 | status: 'wrong', 145 | message: 'Not the same, try again.' 146 | }); 147 | } 148 | } 149 | }, 150 | onStart: function() { 151 | if ( Password1 === '') { 152 | this.setState({ 153 | message: 'Please input your password.' 154 | }); 155 | } else { 156 | this.setState({ 157 | message: 'Please input your password secondly.' 158 | }); 159 | } 160 | }, 161 | */ 162 | 163 | getInitialState: function() { 164 | return { 165 | message: 'Please input your password.', 166 | status: 'normal' 167 | } 168 | }, 169 | render: function() { 170 | return ( 171 | this.onStart()} 176 | onEnd={(password) => this.onEnd(password)} 177 | /> 178 | ); 179 | } 180 | }); 181 | 182 | AppRegistry.registerComponent('AppDemo', () => AppDemo); 183 | 184 | ``` 185 | ## Change Logs 186 | 187 | #### 0.4.0 188 | 189 | - Prettier: best practices for Format documents 190 | - Performance: Increase performance by using React hooks 191 | - Declaration 192 | - Readme: this package work with react-native-web well 193 | 194 | (@hosseinmd) 195 | 196 | #### v0.2.0 197 | 198 | Rewrite with ES6 for React-Native@v0.25+ support 199 | 200 | Add outerCircle and innerCircle properties 201 | 202 | #### v0.1.5 203 | 204 | TextStyle and onReset event. (@caledhwa) 205 | 206 | #### v0.1.4 207 | 208 | Manage the adaptation to landscape orientation. (@jujumoz) 209 | 210 | #### v0.1.3 211 | 212 | Add the allowCross property. 213 | 214 | #### v0.1.2 215 | 216 | Improve the performance for real device. 217 | 218 | #### v0.1.0 219 | 220 | Rewrite in pure javascript, for Android support. 221 | 222 | ## Notes 223 | 224 | This old version(<0.1.0) is at the branch native. I won't update that unless fix bugs. 225 | 226 | If you have suggestions or bug reports, feel free to send pull request or [create new issue](https://github.com/spikef/react-native-gesture-password/issues/new). 227 | -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | import * as helper from "./helper"; 2 | import React, { Component, memo, useMemo } from "react"; 3 | import { 4 | StyleSheet, 5 | PanResponder, 6 | View, 7 | Text, 8 | Dimensions, 9 | I18nManager, 10 | } from "react-native"; 11 | import Line from "./line"; 12 | import Circle from "./circle"; 13 | import PropTypes from "prop-types"; 14 | 15 | const Width = Dimensions.get("window").width; 16 | const Height = Dimensions.get("window").height; 17 | const isVertical = Height > Width; 18 | const Top = isVertical ? ((Height - Width) / 2.0) * 1.25 : 10; 19 | const Radius = isVertical ? Width / 10 : Width / 25; 20 | 21 | export default class GesturePassword extends Component { 22 | constructor(props) { 23 | super(props); 24 | 25 | this.timer = null; 26 | this.lastIndex = -1; 27 | this.sequence = ""; // 手势结果 28 | this.isMoving = false; 29 | 30 | // getInitialState 31 | let circles = []; 32 | let Margin = Radius; 33 | for (let i = 0; i < 9; i++) { 34 | let p = i % 3; 35 | let q = parseInt(i / 3); 36 | circles.push({ 37 | isActive: false, 38 | x: p * (Radius * 2 + Margin) + Margin + Radius, 39 | y: q * (Radius * 2 + Margin) + Margin + Radius, 40 | }); 41 | } 42 | 43 | this.state = { 44 | circles: circles, 45 | lines: [], 46 | }; 47 | } 48 | _panResponder = PanResponder.create({ 49 | // 要求成为响应者: 50 | 51 | onStartShouldSetPanResponder: (event, gestureState) => true, 52 | onStartShouldSetPanResponderCapture: (event, gestureState) => true, 53 | onMoveShouldSetPanResponder: (event, gestureState) => true, 54 | onMoveShouldSetPanResponderCapture: (event, gestureState) => true, 55 | 56 | // 开始手势操作 57 | onPanResponderGrant: (e, g) => this.onStart(e, g), 58 | // 移动操作 59 | onPanResponderMove: (e, g) => this.onMove(e, g), 60 | // 释放手势 61 | onPanResponderRelease: (e, g) => this.onEnd(e, g), 62 | }); 63 | 64 | render() { 65 | let color = 66 | this.props.status === "wrong" 67 | ? this.props.wrongColor 68 | : this.props.rightColor; 69 | 70 | const { 71 | textStyle, 72 | style, 73 | status, 74 | message, 75 | normalColor, 76 | wrongColor, 77 | rightColor, 78 | innerCircle, 79 | outerCircle, 80 | transparentLine, 81 | children, 82 | } = this.props; 83 | 84 | return ( 85 | 95 | 104 | 111 | 112 | 113 | ); 114 | } 115 | 116 | setActive(index) { 117 | this.state.circles[index].isActive = true; 118 | 119 | let circles = [...this.state.circles]; 120 | this.setState({ circles }); 121 | } 122 | 123 | resetActive() { 124 | this.state.lines = []; 125 | for (let i = 0; i < 9; i++) { 126 | this.state.circles[i].isActive = false; 127 | } 128 | 129 | let circles = [...this.state.circles]; 130 | this.setState({ circles }); 131 | this.props.onReset && this.props.onReset(); 132 | } 133 | 134 | getTouchChar(touch) { 135 | let x = touch.x; 136 | let y = touch.y; 137 | 138 | for (let i = 0; i < 9; i++) { 139 | if (helper.isPointInCircle({ x, y }, this.state.circles[i], Radius)) { 140 | return String(i); 141 | } 142 | } 143 | 144 | return false; 145 | } 146 | 147 | getCrossChar(char) { 148 | let middles = "13457", 149 | last = String(this.lastIndex); 150 | 151 | if (middles.indexOf(char) > -1 || middles.indexOf(last) > -1) return false; 152 | 153 | let point = helper.getMiddlePoint( 154 | this.state.circles[last], 155 | this.state.circles[char], 156 | ); 157 | 158 | for (let i = 0; i < middles.length; i++) { 159 | let index = middles[i]; 160 | if (helper.isEquals(point, this.state.circles[index])) { 161 | return String(index); 162 | } 163 | } 164 | 165 | return false; 166 | } 167 | 168 | onStart = (e, g) => { 169 | this.sequence = ""; 170 | this.lastIndex = -1; 171 | this.isMoving = false; 172 | 173 | let x = isVertical 174 | ? e.nativeEvent.pageX 175 | : e.nativeEvent.pageX - Width / 3.4; 176 | let y = isVertical 177 | ? e.nativeEvent.pageY - Top / 1.25 178 | : e.nativeEvent.pageY - 30; 179 | 180 | let lastChar = this.getTouchChar({ x, y }); 181 | 182 | if (lastChar) { 183 | this.isMoving = true; 184 | this.lastIndex = Number(lastChar); 185 | this.sequence = lastChar; 186 | this.resetActive(); 187 | this.setActive(this.lastIndex); 188 | 189 | let point = { 190 | x: this.state.circles[this.lastIndex].x, 191 | y: this.state.circles[this.lastIndex].y, 192 | }; 193 | 194 | this.refs.line.setNativeProps({ start: point, end: point }); 195 | 196 | this.props.onStart && this.props.onStart(); 197 | 198 | if (this.props.interval > 0) { 199 | clearTimeout(this.timer); 200 | } 201 | } 202 | }; 203 | 204 | onMove = (e, g) => { 205 | if (this.isMoving) { 206 | let x = isVertical 207 | ? e.nativeEvent.pageX 208 | : e.nativeEvent.pageX - Width / 3.4; 209 | let y = isVertical 210 | ? e.nativeEvent.pageY - Top / 1.25 211 | : e.nativeEvent.pageY - 30; 212 | 213 | this.refs.line.setNativeProps({ end: { x, y } }); 214 | 215 | let lastChar = null; 216 | 217 | if ( 218 | !helper.isPointInCircle( 219 | { x, y }, 220 | this.state.circles[this.lastIndex], 221 | Radius, 222 | ) 223 | ) { 224 | lastChar = this.getTouchChar({ x, y }); 225 | } 226 | 227 | if (lastChar && this.sequence.indexOf(lastChar) === -1) { 228 | if (!this.props.allowCross) { 229 | let crossChar = this.getCrossChar(lastChar); 230 | 231 | if (crossChar && this.sequence.indexOf(crossChar) === -1) { 232 | this.sequence += crossChar; 233 | this.setActive(Number(crossChar)); 234 | } 235 | } 236 | 237 | let lastIndex = this.lastIndex; 238 | let thisIndex = Number(lastChar); 239 | 240 | this.state.lines = [ 241 | ...this.state.lines, 242 | { 243 | start: { 244 | x: this.state.circles[lastIndex].x, 245 | y: this.state.circles[lastIndex].y, 246 | }, 247 | end: { 248 | x: this.state.circles[thisIndex].x, 249 | y: this.state.circles[thisIndex].y, 250 | }, 251 | }, 252 | ]; 253 | 254 | this.lastIndex = thisIndex; 255 | this.sequence += lastChar; 256 | 257 | this.setActive(this.lastIndex); 258 | 259 | let point = { 260 | x: this.state.circles[this.lastIndex].x, 261 | y: this.state.circles[this.lastIndex].y, 262 | }; 263 | 264 | this.refs.line.setNativeProps({ start: point }); 265 | } 266 | } 267 | 268 | if (this.sequence.length === 9) this.onEnd(); 269 | }; 270 | 271 | onEnd = (e, g) => { 272 | const password = helper.getRealPassword(this.sequence); 273 | 274 | this.sequence = ""; 275 | this.lastIndex = -1; 276 | this.isMoving = false; 277 | 278 | let origin = { x: 0, y: 0 }; 279 | this.refs.line.setNativeProps({ start: origin, end: origin }); 280 | 281 | this.props.onEnd && this.props.onEnd(password); 282 | 283 | if (this.props.interval > 0) { 284 | this.timer = setTimeout(() => this.resetActive(), this.props.interval); 285 | } 286 | }; 287 | } 288 | 289 | GesturePassword.propTypes = { 290 | message: PropTypes.string, 291 | normalColor: PropTypes.string, 292 | rightColor: PropTypes.string, 293 | wrongColor: PropTypes.string, 294 | status: PropTypes.oneOf(["right", "wrong", "normal"]), 295 | onStart: PropTypes.func, 296 | onEnd: PropTypes.func, 297 | onReset: PropTypes.func, 298 | interval: PropTypes.number, 299 | allowCross: PropTypes.bool, 300 | innerCircle: PropTypes.bool, 301 | outerCircle: PropTypes.bool, 302 | }; 303 | 304 | GesturePassword.defaultProps = { 305 | message: "", 306 | normalColor: "#5FA8FC", 307 | rightColor: "#5FA8FC", 308 | wrongColor: "#D93609", 309 | status: "normal", 310 | interval: 0, 311 | allowCross: false, 312 | innerCircle: true, 313 | outerCircle: true, 314 | }; 315 | 316 | const Container = memo( 317 | ({ 318 | textStyle, 319 | style, 320 | status, 321 | message, 322 | wrongColor, 323 | rightColor, 324 | panHandlers, 325 | children, 326 | userAddedChildren, 327 | }) => { 328 | let color = status === "wrong" ? wrongColor : rightColor; 329 | 330 | const _styleContainer = useMemo(() => [styles.frame, style], [style]); 331 | 332 | const _styleText = useMemo( 333 | () => [styles.msgText, textStyle, { color: color }], 334 | [textStyle, color], 335 | ); 336 | 337 | return ( 338 | 339 | 340 | {message} 341 | 342 | 343 | {children} 344 | 345 | {userAddedChildren} 346 | 347 | ); 348 | }, 349 | ); 350 | 351 | const Lines = memo( 352 | ({ lines, status, wrongColor, rightColor, transparentLine }) => { 353 | let color; 354 | 355 | return lines.map(function(l, i) { 356 | color = status === "wrong" ? wrongColor : rightColor; 357 | color = transparentLine ? "#00000000" : color; 358 | 359 | return ; 360 | }); 361 | }, 362 | ); 363 | 364 | const Circles = memo( 365 | ({ 366 | circles, 367 | status, 368 | normalColor, 369 | wrongColor, 370 | rightColor, 371 | innerCircle, 372 | outerCircle, 373 | }) => { 374 | let fill, color, inner, outer; 375 | 376 | return circles.map(function(c, i) { 377 | fill = c.isActive; 378 | color = status === "wrong" ? wrongColor : rightColor; 379 | inner = !!innerCircle; 380 | outer = !!outerCircle; 381 | 382 | return ( 383 | 394 | ); 395 | }); 396 | }, 397 | ); 398 | 399 | const styles = StyleSheet.create({ 400 | frame: { 401 | flex: 1, 402 | flexDirection: I18nManager.isRTL ? "row" : "row-reverse", 403 | backgroundColor: "#292B38", 404 | }, 405 | board: { 406 | flexDirection: I18nManager.isRTL ? "row-reverse" : "row", 407 | position: "absolute", 408 | left: isVertical ? 0 : Width / 3.4, 409 | top: isVertical ? Top / 1.5 : 30, 410 | width: Width, 411 | height: Height, 412 | }, 413 | message: { 414 | position: "absolute", 415 | left: 0, 416 | top: 20, 417 | width: Width, 418 | height: Top / 3, 419 | alignItems: "center", 420 | justifyContent: "center", 421 | }, 422 | msgText: { 423 | fontSize: 14, 424 | }, 425 | }); 426 | 427 | module.exports = GesturePassword; 428 | --------------------------------------------------------------------------------