Skip to content

Instantly share code, notes, and snippets.

@Drzaln
Created June 4, 2024 04:30
Show Gist options
  • Select an option

  • Save Drzaln/9be173a36c7f1ebbbb3697eaf398865e to your computer and use it in GitHub Desktop.

Select an option

Save Drzaln/9be173a36c7f1ebbbb3697eaf398865e to your computer and use it in GitHub Desktop.
RN - BorderLine Animated
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