Instantly share code, notes, and snippets.
Created
June 4, 2024 04:30
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save Drzaln/9be173a36c7f1ebbbb3697eaf398865e to your computer and use it in GitHub Desktop.
RN - BorderLine Animated
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useEffect } from "react"; | |
| import { View, StyleSheet, Pressable, Text } from "react-native"; | |
| import Svg, { Path, Defs, LinearGradient, Stop } from "react-native-svg"; | |
| import Animated, { | |
| Easing, | |
| useAnimatedProps, | |
| useAnimatedStyle, | |
| useSharedValue, | |
| withRepeat, | |
| withTiming, | |
| } from "react-native-reanimated"; | |
| const AnimatedPath = Animated.createAnimatedComponent(Path); | |
| const runningBorderPath = (width, height, radius) => { | |
| // Ensure radius does not exceed half of the smallest dimension | |
| const maxRadius = Math.min(width, height) / 2; | |
| radius = Math.min(radius, maxRadius); | |
| const offset = 1.2; | |
| return ` | |
| M ${radius + offset}, ${offset} | |
| L ${width - radius - offset}, ${offset} | |
| Q ${width - offset}, ${offset} ${width - offset}, ${radius + offset} | |
| L ${width - offset}, ${height - radius - offset} | |
| Q ${width - offset}, ${height - offset} ${width - radius - offset}, ${height - offset} | |
| L ${radius + offset}, ${height - offset} | |
| Q ${offset}, ${height - offset} ${offset}, ${height - radius - offset} | |
| L ${offset}, ${radius + offset} | |
| Q ${offset}, ${offset} ${radius + offset}, ${offset} | |
| `; | |
| }; | |
| const Button = ({ | |
| width = 160, | |
| height = 50, | |
| radius = 8, // Adjusted to be more round by default | |
| color = "#F6F6F6", | |
| loading = false, | |
| borderColor = "purple", | |
| onPress, | |
| }) => { | |
| const runningBorderData = runningBorderPath(width, height, radius); | |
| const perimeter = 2 * (width + height) - 8 * radius + 2 * Math.PI * radius; | |
| const segmentLength = perimeter / 8; | |
| const gapLength = perimeter - segmentLength; | |
| const offset = useSharedValue(0); | |
| const translateX = useSharedValue(0); | |
| useEffect(() => { | |
| if (loading) { | |
| translateX.value = withTiming(-6); | |
| offset.value = withRepeat( | |
| withTiming(perimeter, { | |
| duration: 2500, | |
| easing: Easing.linear, | |
| }), | |
| -1, | |
| false | |
| ); | |
| } else { | |
| offset.value = 0; | |
| translateX.value = withTiming(0); | |
| } | |
| }, [loading]); | |
| const animatedProps = useAnimatedProps(() => { | |
| return { | |
| strokeDashoffset: offset.value, | |
| }; | |
| }); | |
| const labelAnimatedStyle = useAnimatedStyle(() => { | |
| return { | |
| transform: [{ translateX: translateX.value }], | |
| }; | |
| }); | |
| return ( | |
| <View style={styles.container}> | |
| <Pressable | |
| onPress={onPress} | |
| style={[ | |
| styles.pressable, | |
| { backgroundColor: color, width, height, borderRadius: radius }, | |
| ]} | |
| > | |
| <Animated.View style={[styles.labelContainer, labelAnimatedStyle]}> | |
| <Text | |
| style={[styles.label, { color: loading ? "#A5A5AC" : "#7B7B82" }]} | |
| > | |
| {loading ? "Uploading..." : "Upload"} | |
| </Text> | |
| </Animated.View> | |
| {loading && ( | |
| <Svg width={width} height={height}> | |
| <AnimatedPath | |
| d={runningBorderData} | |
| strokeLinecap="round" | |
| fill="none" | |
| stroke={borderColor} | |
| strokeWidth={1.2} | |
| strokeDasharray={`${segmentLength},${gapLength}`} | |
| animatedProps={animatedProps} | |
| /> | |
| </Svg> | |
| )} | |
| </Pressable> | |
| </View> | |
| ); | |
| }; | |
| const styles = StyleSheet.create({ | |
| container: { | |
| justifyContent: "center", | |
| alignItems: "center", | |
| marginVertical: 8, | |
| }, | |
| pressable: { | |
| justifyContent: "center", | |
| alignItems: "center", | |
| position: "relative", | |
| }, | |
| labelContainer: { | |
| position: "absolute", | |
| justifyContent: "center", | |
| alignItems: "center", | |
| }, | |
| label: { | |
| fontSize: 16, | |
| }, | |
| }); | |
| export default Button; | |
| // -------------------------------------------------------------- | |
| // How to use | |
| // function App() { | |
| // const [loadingButton, setLoadingButton] = useState(false); | |
| // return ( | |
| // <SafeAreaView style={styles.container}> | |
| // <Button | |
| // loading={loadingButton} | |
| // onPress={() => setLoadingButton(!loadingButton)} | |
| // /> | |
| // </SafeAreaView> | |
| // ); | |
| // } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment