react-native-gradient-wrapper
Version:
A flexible React Native gradient wrapper component with animated border and background gradients.
249 lines (248 loc) • 11.4 kB
JavaScript
import React, { useEffect, useRef, useState } from 'react';
import { View, Animated, StyleSheet, Easing } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { useColorScheme } from 'react-native';
const defaultLightBorder = ['#ccc', '#eee'];
const defaultDarkBorder = ['#4b6cb7', '#182848'];
const BorderGradient = ({ borderGradient, borderTopGradient, borderBottomGradient, borderLeftGradient, borderRightGradient, borderWidth = 2, borderRadius = 10, borderRotation = 'none', borderRotationSpeed = 3000, stopBorderAfter, borderLocations, angle, theme = 'auto', style, children, }) => {
const systemTheme = useColorScheme();
const isDark = theme === 'dark' || (theme === 'auto' && systemTheme === 'dark');
const rotateBorderAnim = useRef(new Animated.Value(0)).current;
const [borderRotationStopped, setBorderRotationStopped] = useState(false);
const spinBorder = rotateBorderAnim.interpolate({
inputRange: [0, 1],
outputRange: borderRotation === 'anticlockwise' ? ['360deg', '0deg'] : ['0deg', '360deg'],
});
useEffect(() => {
if (borderRotation !== 'none') {
let running = true;
const animateLoop = () => {
if (!running)
return;
Animated.sequence([
Animated.timing(rotateBorderAnim, {
toValue: 1,
duration: borderRotationSpeed,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(rotateBorderAnim, {
toValue: 0,
duration: borderRotationSpeed,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
]).start(() => {
animateLoop();
});
};
animateLoop();
if (stopBorderAfter) {
setTimeout(() => {
running = false;
Animated.timing(rotateBorderAnim, {
toValue: 0,
duration: 800,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}).start(() => {
setBorderRotationStopped(true);
});
}, stopBorderAfter);
}
}
}, [borderRotation, borderRotationSpeed, stopBorderAfter]);
const shouldUseIndividualBorders = () => {
if (borderGradient)
return false;
const individualBorderCount = [
borderTopGradient,
borderBottomGradient,
borderLeftGradient,
borderRightGradient,
].filter(Boolean).length;
if (individualBorderCount <= 2)
return true;
const hasVertical = borderTopGradient || borderBottomGradient;
const hasHorizontal = borderLeftGradient || borderRightGradient;
return !(hasVertical && hasHorizontal);
};
const getFullBorderGradient = () => {
if (borderGradient && borderGradient.length > 0) {
return {
colors: borderGradient,
locations: borderLocations && borderLocations.length === borderGradient.length
? borderLocations
: borderGradient.map((_, index) => index / (borderGradient.length - 1)),
};
}
if (!borderTopGradient && !borderBottomGradient && !borderLeftGradient && !borderRightGradient) {
return {
colors: isDark ? defaultDarkBorder : defaultLightBorder,
locations: [0, 1],
};
}
let gradients = [];
// Left gradient: start of the sequence
if (borderLeftGradient) {
gradients.push(...borderLeftGradient);
}
// Top gradient: 80% of top part, blending into right
if (borderTopGradient) {
gradients.push(...borderTopGradient);
if (borderRightGradient) {
// Blend top into right
gradients.push(borderRightGradient[0]);
}
}
// Right gradient: transition part
if (borderRightGradient && !borderTopGradient) {
gradients.push(borderRightGradient[0]);
}
// Bottom gradient: 80% of bottom part, blending into left
if (borderBottomGradient) {
gradients.push(...borderBottomGradient);
if (borderLeftGradient && !borderLeftGradient.every((color) => gradients.includes(color))) {
// Blend bottom into left, ensuring no color repetition
gradients.push(borderLeftGradient[0]);
}
}
// If no gradients were added, use default
if (gradients.length === 0) {
return {
colors: isDark ? defaultDarkBorder : defaultLightBorder,
locations: [0, 1],
};
}
// Calculate locations to enforce 80% top, 80% bottom, with transitions
const totalColors = gradients.length;
const topLength = borderTopGradient ? Math.ceil(totalColors * 0.8) : 0;
const bottomLength = borderBottomGradient ? Math.ceil(totalColors * 0.8) : 0;
const transitionLength = totalColors - topLength - bottomLength;
const locations = Array(totalColors).fill(0).map((_, index) => {
if (borderTopGradient && index >= (borderLeftGradient ? borderLeftGradient.length : 0) && index < topLength + (borderLeftGradient ? borderLeftGradient.length : 0)) {
// Top 80% mapped to 0.2–0.6 (after left)
const topIndex = index - (borderLeftGradient ? borderLeftGradient.length : 0);
return 0.2 + (topIndex / (topLength - 1)) * 0.4;
}
else if (borderBottomGradient && index >= totalColors - bottomLength) {
// Bottom 80% mapped to 0.6–1.0
const bottomIndex = index - (totalColors - bottomLength);
return 0.6 + (bottomIndex / (bottomLength - 1)) * 0.4;
}
else {
// Left and right transition parts
const transitionIndex = index - (borderLeftGradient ? borderLeftGradient.length : 0);
const adjustedTransitionLength = transitionLength + (borderLeftGradient ? borderLeftGradient.length : 0);
return transitionIndex / (adjustedTransitionLength || 1) * 0.2;
}
});
return { colors: gradients, locations };
};
const borderStyles = StyleSheet.create({
top: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
height: borderWidth,
borderTopLeftRadius: borderRadius,
borderTopRightRadius: borderRadius,
},
bottom: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: borderWidth,
borderBottomLeftRadius: borderRadius,
borderBottomRightRadius: borderRadius,
},
left: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
width: borderWidth,
borderTopLeftRadius: borderRadius,
borderBottomLeftRadius: borderRadius,
},
right: {
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
width: borderWidth,
borderTopRightRadius: borderRadius,
borderBottomRightRadius: borderRadius,
},
});
const angleToDirection = (angle) => {
const radians = (angle * Math.PI) / 180;
const x = Math.cos(radians);
const y = Math.sin(radians);
return {
start: { x: 0.5 - x / 2, y: 0.5 - y / 2 },
end: { x: 0.5 + x / 2, y: 0.5 + y / 2 },
};
};
const { start: borderStart, end: borderEnd } = angle !== undefined
? angleToDirection(angle)
: { start: { x: 0, y: 1 }, end: { x: 0, y: 1 } };
const useIndividualBorders = shouldUseIndividualBorders();
const fullBorderGradientResult = getFullBorderGradient();
const fullBorderGradient = fullBorderGradientResult.colors;
const calculatedBorderLocations = fullBorderGradientResult.locations;
const appliedTop = borderTopGradient !== null && borderTopGradient !== void 0 ? borderTopGradient : fullBorderGradient;
const appliedBottom = borderBottomGradient !== null && borderBottomGradient !== void 0 ? borderBottomGradient : fullBorderGradient;
const appliedLeft = borderLeftGradient !== null && borderLeftGradient !== void 0 ? borderLeftGradient : fullBorderGradient;
const appliedRight = borderRightGradient !== null && borderRightGradient !== void 0 ? borderRightGradient : fullBorderGradient;
const gradientSpan = 0.95; // 95% width for individual borders
const renderStaticBorders = () => {
var _a, _b;
return (<>
{/* LEFT: Solid Blue */}
{borderLeftGradient && (<View style={[
borderStyles.left,
{ backgroundColor: (_a = borderLeftGradient[0]) !== null && _a !== void 0 ? _a : 'transparent' },
]}/>)}
{/* TOP: Red to Orange */}
{borderTopGradient && (<LinearGradient colors={appliedTop} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={[
borderStyles.top,
{
width: `${gradientSpan * 100}%`,
alignSelf: 'center',
},
]}/>)}
{/* RIGHT: Solid Yellow */}
{borderRightGradient && (<View style={[
borderStyles.right,
{ backgroundColor: (_b = borderRightGradient[0]) !== null && _b !== void 0 ? _b : 'transparent' },
]}/>)}
{/* BOTTOM: Green Gradient */}
{borderBottomGradient && (<LinearGradient colors={appliedBottom} start={{ x: 1, y: 0 }} end={{ x: 0, y: 0 }} style={[
borderStyles.bottom,
{
width: `${gradientSpan * 100}%`,
alignSelf: 'center',
},
]}/>)}
</>);
};
return (<View style={[{ borderRadius, overflow: 'hidden' }, style]}>
{!borderRotationStopped && borderRotation !== 'none' ? (<Animated.View style={[StyleSheet.absoluteFill, { transform: [{ rotate: spinBorder }] }]}>
<LinearGradient colors={fullBorderGradient} locations={calculatedBorderLocations} start={borderStart} end={borderEnd} style={StyleSheet.absoluteFill}/>
</Animated.View>) : !useIndividualBorders ? (<View style={StyleSheet.absoluteFill}>
<LinearGradient colors={fullBorderGradient} locations={calculatedBorderLocations} start={borderStart} end={borderEnd} style={StyleSheet.absoluteFill}/>
</View>) : (renderStaticBorders())}
<View style={{
margin: borderWidth,
flex: 1,
borderRadius: borderRadius - borderWidth,
overflow: 'hidden',
}}>
{children}
</View>
</View>);
};
export default BorderGradient;