@shanshang/react-native-pattern-lock
Version:
A smooth pattern lock component for react native.(iOS, android & web)
266 lines (259 loc) • 7.88 kB
JavaScript
/*
* @Author: 石破天惊
* @email: shanshang130@gmail.com
* @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,
},
});