/* * @Author: 石破天惊 * @email: [email protected] * @Date: 2021-08-02 10:13:06 * @LastEditTime: 2021-08-04 12:10:44 * @LastEditors: 石破天惊 * @Description: */ import React, { useEffect, useState } from "react"; import { StyleSheet, View, Text } from "react-native"; import { PanGestureHandler, TapGestureHandler, } from "react-native-gesture-handler"; import Animated, { cancelAnimation, runOnJS, runOnUI, useAnimatedGestureHandler, useAnimatedProps, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring, } from "react-native-reanimated"; import Svg, { Path } from "react-native-svg"; const AnimatedPath = Animated.createAnimatedComponent(Path); export function PatternLock(props) { const [isError, setIsError] = useState(false); const canTouch = useSharedValue(true); const patternPoints = useSharedValue(); const selectedIndexes = useSharedValue([]); const endPoint = useSharedValue(); const containerLayout = useSharedValue({ width: 0, height: 0, min: 0 }); const R = useDerivedValue( () => (containerLayout.value.min / props.rowCount - props.patternMargin * 2) / 2 ); const cvc = useAnimatedStyle(() => ({ flexDirection: "row", flexWrap: "wrap", marginBottom: `${ Math.max( 0, containerLayout.value.height / containerLayout.value.width - 1.25 ) * 50 }%`, width: containerLayout.value.min, height: containerLayout.value.min, })); const msgX = useSharedValue(0); const msgColor = { color: isError ? props.errorColor : props.activeColor }; const msgStyle = useAnimatedStyle(() => { return { transform: [{ translateX: msgX.value }] }; }); const onContainerLayout = ({ nativeEvent: { layout: { x, y, width, height }, }, }) => (containerLayout.value = { width, height, min: Math.min(width, height), }); const onPatternLayout = ({ nativeEvent: { layout } }) => { const points = []; for (let i = 0; i < props.rowCount; i++) { for (let j = 0; j < props.columnCount; j++) { points.push({ x: layout.x + (layout.width / props.columnCount) * (j + 0.5), y: layout.y + (layout.height / props.rowCount) * (i + 0.5), }); } } patternPoints.value = points; }; const onEndJS = (res) => { if (props.onCheck) { canTouch.value = false; if (!props.onCheck(res)) { setIsError(true); const closeError = () => setIsError(false); runOnUI(() => { cancelAnimation(msgX); //修复iOS上原地spring不动的问题。 msgX.value = withSpring( msgX.value === 0 ? 0.1 : 0, { stiffness: 2000, damping: 10, mass: 1, velocity: 2000, }, (finished) => { runOnJS(closeError)(); canTouch.value = true; selectedIndexes.value = []; } ); })(); } else { setIsError(false); setTimeout(() => { selectedIndexes.value = []; canTouch.value = true; }, 1000); } } }; const panHandler = useAnimatedGestureHandler({ onStart: (evt) => { if ( canTouch.value && patternPoints.value && selectedIndexes.value.length === 0 ) { const selected = []; patternPoints.value.every((p, idx) => { if ( (p.x - evt.x) * (p.x - evt.x) + (p.y - evt.y) * (p.y - evt.y) < R.value * R.value ) { selected.push(idx); return false; } return true; }); selectedIndexes.value = selected; } }, onActive: (evt) => { if ( canTouch.value && patternPoints.value && selectedIndexes.value.length > 0 ) { patternPoints.value.every((p, idx) => { if ( (p.x - evt.x) * (p.x - evt.x) + (p.y - evt.y) * (p.y - evt.y) < R.value * R.value ) { if (selectedIndexes.value.indexOf(idx) < 0) { selectedIndexes.value = [...selectedIndexes.value, idx]; } return false; } return true; }); endPoint.value = { x: evt.x, y: evt.y }; } }, onEnd: (evt) => { if (!canTouch.value) return; endPoint.value = null; if (selectedIndexes.value.length > 0) runOnJS(onEndJS)(selectedIndexes.value.join("")); }, }); const animatedProps = useAnimatedProps(() => { let d = ""; selectedIndexes.value.forEach((idx) => { d += !d ? " M" : " L"; d += ` ${patternPoints.value[idx].x},${patternPoints.value[idx].y}`; }); if (d && endPoint.value) d += ` L${endPoint.value.x},${endPoint.value.y}`; if (!d) d = "M-1,-1"; return { d }; }); return ( <PanGestureHandler onGestureEvent={panHandler}> <Animated.View style={styles.container} onLayout={onContainerLayout}> <TapGestureHandler onGestureEvent={panHandler}> <Animated.View style={styles.container}> <View style={styles.msgc}> <Animated.Text style={[msgColor, msgStyle]}> {props.message} </Animated.Text> </View> <Animated.View style={cvc} onLayout={onPatternLayout}> {Array(props.rowCount * props.columnCount) .fill(0) .map((_, idx) => { const patternColor = useDerivedValue(() => { if (selectedIndexes.value.findIndex((v) => v === idx) < 0) { return props.inactiveColor; } else if (isError) { return props.errorColor; } else { return props.activeColor; } }); const outer = useAnimatedStyle(() => { return { borderWidth: 2, width: 2 * R.value, height: 2 * R.value, alignItems: "center", justifyContent: "center", borderColor: patternColor.value, borderRadius: 2 * R.value, margin: props.patternMargin, }; }); const inner = useAnimatedStyle(() => { return { width: R.value * 0.8, height: R.value * 0.8, borderRadius: R.value * 0.8, backgroundColor: patternColor.value, }; }); return ( <Animated.View key={idx} style={outer}> <Animated.View style={inner} /> </Animated.View> ); })} </Animated.View> <Svg style={styles.svg} width="100%" height="100%"> <AnimatedPath fill="none" strokeWidth={3} animatedProps={animatedProps} stroke={isError ? props.errorColor : props.activeColor} /> </Svg> </Animated.View> </TapGestureHandler> </Animated.View> </PanGestureHandler> ); } PatternLock.defaultProps = { message: "", rowCount: 3, columnCount: 3, patternMargin: 25, inactiveColor: "#8E91A8", activeColor: "#5FA8FC", errorColor: "#D93609", }; const styles = StyleSheet.create({ container: { flex: 1, alignSelf: "stretch", alignItems: "center", }, msgc: { flex: 1, justifyContent: "center", alignSelf: "center", }, svg: { position: "absolute", left: 0, top: 0, }, });